微信小程序的技术架构与运行原理解析
小程序作为微信生态中的轻量级应用,极大地简化了开发者的开发流程,同时保证了用户的良好使用体验。本文将深入解析小程序的技术架构,从逻辑层与渲染层的角度详细剖析小程序的运行原理,帮助开发者更好地理解小程序的底层机制。
1. webview 的弊端
通过操作 DOM,JavaScript 可以直接访问小程序中的一些敏感数据,如用户信息、商家信息等,从而导致小程序的安全性严重受损。
2. 如何解决传统 h5 安全管控问题
为了解决安全管控问题,小程序阻止开发者使用一些浏览器提供的比如页面跳转、操作 DOM、动态执行脚本等功能。由于 JavaScript 具有高度的灵活性,且浏览器接口繁多,因此很容易忽略某些潜在的安全风险。即便禁用了所有已知的危险接口,浏览器内核的更新仍可能带来新的漏洞。
所以,要彻底解决这个问题,必须提供一个沙箱环境来运行开发者的 js 代码。这个沙箱环境没有任何浏览器相关接口,只提供纯 js 的解释执行环境。
3. 小程序解决了什么问题?
-
性能优于传统 H5 应用
-
提供统一的微信入口,方便用户管理
-
集成微信登录功能,简化用户身份验证
-
支持微信分享,提升用户获取和传播效率
-
提供快速加载机制
-
提供接近原生应用的用户体验
-
安全性高,支持微信的数据开放接口
-
简化开发流程,提高开发效率
小程序的基本架构
小程序的架构主要分为逻辑层与渲染层,这两者通过微信提供的桥梁(WeixinJSBridge)进行通信,确保数据和视图能够实时交互。
1. 逻辑层
逻辑层负责处理小程序的业务逻辑,包括页面的生命周期管理、事件处理、数据的加载与处理等。它通过 JavaScript 实现,iOS 系统使用的是 JSC 引擎,Android 则使用的是 V8 引擎,二者通过单独的线程运行,保证了逻辑和渲染互不干扰。
核心结构
- Page() 和 App():分别对应页面和整个小程序的生命周期管理。
- setData():用于更新页面的数据,这些数据通过通信机制传递给渲染层。
- WeixinJSBridge:微信提供的内部桥梁,主要用于逻辑层与渲染层的通信。
逻辑层执行的过程:
- 加载小程序的基础库(
WAService.js
),这包括了微信提供的 API。 - 加载所有的页面文件,并构建应用的全局配置。
- 处理数据和事件,触发生命周期函数,并通过
setData
更新渲染层的数据。
2. 渲染层
渲染层负责显示用户界面,主要通过WXML和WXSS来定义页面的结构和样式。WXML 类似于 HTML,而 WXSS 则与 CSS 相似。渲染层通过 Webkit 内核的iframe来运行,确保其与逻辑层的分离。
核心结构
- WXML:页面结构,类似于 HTML。
- WXSS:页面样式,类似于 CSS。
- WeixinJSBridge:同样在渲染层提供了通信机制,用于接收逻辑层的数据并更新页面。
- exparser 组件系统:微信自定义的一套组件系统,基于 WebComponent,通过虚拟 DOM(VNode)来实现渲染和更新。
渲染层的执行过程:
- 加载默认的配置文件与基础库,解析 WXML 和 WXSS 文件。
- 动态计算页面的 CSS 样式,并结合设备分辨率(通过
wcsc
编译 WXSS),确保适配不同的设备。 - 通过
VNode
机制对比新旧数据,实现diff 算法,确保高效更新页面。
3. 逻辑层与渲染层的通信
逻辑层与渲染层之间的通信是通过微信内部的桥梁WeixinJSBridge
实现的。它不仅能够在逻辑层和渲染层之间传递数据,还可以与 Native 原生线程进行通信。通过这个桥梁,开发者可以使用setData()
来将逻辑层的数据同步到渲染层,达到动态更新视图的效果。
-
小程序微双线程架构,分别有渲染层与逻辑层管理,渲染层界面使用 webview 进行渲染;逻辑层采用 JSCore 运行 JavaScript 代码。
-
两个线程之间有 Native 层进行统一处理。无论是线程之间的通讯、数据的传递、网路请求都由 Native 层做转发。
-
渲染层存在多个 webview,更加接近原生应用 APP 的用户体验。
小程序的渲染引擎与编译器
1.WXSS 编译-加载
WXSS 文件编译后成 wxss.js 文件,index.wxss 文件会先通过 WCSC 可执行程序文件编译成 js 文件。并不是直接编译成 css 文件。
在渲染层的一个 script 标签中, 编译后的代码是通过 eval 方法注入执行的。
2. WXML - - VirtualDOM 渲染流程
generateFunc 就是接受动态数据,并生成虚拟 DOM 树的函数
- 如果没有有 generateFun 那么 body 标记内部展示 decodeName + “not found”,并输处错误日志
- 如果有,检查 window 或++global 环境中自定义事件 CustomEvent 是否存在。
- document.dispatchEvent 触发自定义事件 将 generateFunc 当作参数传递给底层渲染库
- 在触发自定义事件的时候,添加当前时间节点,可以理解为生命周期 pageFrame_generateFunc_ready
3. 小程序事件
- 事件是视图层到逻辑层的通讯方式。
- 事件可以将用户的行为反馈到逻辑层进行处理。
- 事件可以绑定在组件上,当达到触发事件,就会执行逻辑层中对应的事件处理函数。
- 事件对象可以携带额外信息,如 id, dataset, touches 等等。
小程序的事件都是和 js 原生事件相互转换的,小程序的 tap 事件底层是由 web 的 mouseup 事件转换来的。小程序 tap 事件的触发分为几个过程,首先底层实现是用 web 的 mouseup 事件触发了 tap 事件,底层为 window 绑定捕获阶段的 mouseup 事件。
通信系统的设计
内置组件中有部分组件是利用到客户端原生提供的能力,既然需要客户端原生提供的能力,那就会涉及到渲染层与客户端的交互通信。这层通信机制在 iOS 和安卓系统的实现方式并不一样,iOS 是利用了 WKWebView 的提供 messageHandlers 特性,而在安卓则是往 WebView 的 window 对象注入一个原生方法,最终会封装成 WeiXinJSBridge 这样一个兼容层。在微信开发者工具中则是使用了 websocket 进行了封装。
在微信小程序执行过程中,Native 层,也就是客户端层分别向渲染层与逻辑层注入 WeixinJSBridge 以达到线程通讯的目的,WeixinJSBridge 的 script 标记注入。
WeixinJSBridge 提供了如下几个方法:
- invoke - 调用 Native API,以 api 方式调用开发工具提供的基础能力,并提供对应 api 执行后的回调。
- invokeCallbackHandler - Native 传递 invoke 方法回调结果
- on - 用来收集小程序开发者工具触发的事件回调
- publish - 渲染层发布消息,用来向逻辑业务层发送消息,也就是说要调用逻辑层的事件方法
- subscribe - 订阅逻辑层消息
- subscribeHandler - 视图层和逻辑层消息订阅转发
- setCustomPublishHandler - 自定义消息转发
生命周期
每个页面对应一个独立的 webview 实例,因此页面数量理论上与 webview 实例数量一致。
在逻辑层就不一样了,内部只有 一个 APP 实例,所有页面里面的写的逻辑都在一个逻辑线程中执行
小程序路由设计
采用多个 webview 这种方式类似于多页面应用,因为多页面应用可以保留前一个页面的状态,所以路由的内部是基于多页应用的架构实现的。
在逻辑层中有打开新页面 navigateTo、重定向 redirectTo、页面返回 navigateBack 等,开发者通过官网提供的 API 触发。
无论是渲染层用户触发的行为,还是逻辑层 API 触发的行为,这个行为都会被发送到 Native 层,有 Native 层统一控制路由。
对于 webview 的添加或删除都会有一个 路由栈 来维护。
小程序场景中路由变化相对应的栈变化:
- 小程序初始化的时候需要推入首页,新页面入栈。
- 打开新页面对应 navigateTo,新页面入栈
- 页面重定向 redirectTo,当前页面出栈,而后新页面入栈。
- 页面回退 navigateBack,页面一直出栈,到达指定页面停止。
- Tab 切换 switchTab,页面全部出栈,只留下新的 Tab 页面。
- 重新加载 reLaunch,页面全部出栈,只留下新的页面。
渲染层基础库 WAWebview
- core-js 模块:负责初始化框架 js 代码,编译 js,加载业务逻辑 js 等功能
- Foundation:基础模块,包含多个 API,如 EventEmitter(事件发布与订阅)、Ready 事件、基础库 Ready 事件、Bridge Ready 事件,以及环境变量(env 和 global)。
- WeixinJSBridge:通讯模块,包含有 on、publish、invoke、subscribe、invokeCallbackHandler、subscribeHandler。只是对 Native 注入通讯 api 的封装,便于内部调用。
- 异常监听模块:基础库内针对 promise 或者 js 等异常事件的监听处理
- 日志打印模块:包含 wxNativeConsole、webviewConsole、wxConsole、wxPerfConsole 等
- 系统函数和第三方函数模块:调用系统函数、包装系统函数、调用小程序或插件函数
- Report 信息上报模块:内部包含了非常多种类的上报 api 及异常监听 api
- Exparser 组件系统模块: WXML 文件经过 WCC 编译器编译成 js 文件,生成$gwx()函数, $gwx()函数接收文件路径和动态数据生成 virtualDOM,Exparser 组件系统就是将 virtualDOM 转化成 HTML 标记
- VirtualDOM 模块:主要模拟了 DOM 接口上面的 element() 对象
- 默认样式注入模块
逻辑层基础库 WAService
- core-js 模块
- Foundation:基础模块
- WeixinJSBridge:通讯模块
- NativeBuffer 模块:javascript 语言自身只有字符串数据类型,没有二进制数据类型。 但在处理像 TCP 流或文件流时,必须使用到二进制数据。因此在微信小程序中,定义了一个 NativeBuffer 模块,该模块用来创建一个专门存放二进制数据的缓存区。
- 日志打印模块
- WerxinWorker 模块:包含创建 worker、结束当前 workder、发送数据请求、监听回调等方法
- JSContext 模块:JsContext 是 js 代码执行的上下文对象,相当于一个 webview 中的 window 对象
- appServiceEngine 模块:提供了 App、Page、Component、Behavior、getApp、getCurrentPages 等框架的基本对外接口
WebComponent 原理
抽离自定义组件为了提高复用率,提升开发效率
自定义元素类必须继承自 window 内置的 HTML 相关类, 这些类位于 window.HTML*Element ,他们都继承自 HTMLElement 类。
小程序的组件系统
微信的小程序组件系统基于exparser,它是一套类似于 WebComponent 的实现,允许开发者定义自定义组件。组件通过虚拟 DOM(VNode)进行渲染,当数据发生变化时,系统会自动对比新旧节点,计算出需要更新的部分,从而实现页面的高效更新。
Exparser 的主要 特点 包括以下几点:
- 基于 Shadow DOM 模型:模型上与 WebComponents 的 Shadow DOM 高度相似,但不依赖浏览器的原生支持,也没有其他依赖库;实现时,还针对性地增加了其他 API 以支持小程序组件编程。
- 可在纯 JS 环境中运行:这意味着逻辑层也具有一定的组件树组织能力。
- 高效轻量:性能表现好,在组件实例极多的环境下表现尤其优异,同时代码尺寸也较小。
在自定义组件的概念基础上,我们可以把所有组件都进行分离,这样,各个组件也将具有各自独立的逻辑空间。每个组件都分别拥有自己的独立的数据、setData 调用。
整个页面节点树实质上被拆分成了若干个 ShadowTree(页面的 body 实质上也是一个组件,因而也是一个 ShadowTree)最终组成了小程序中的 Composed Tree。 小程序中,所有节点树相关的操作都依赖于 Exparser,包括 WXML 到页面最终节点树的构建、createSelectorQuery 调用和自定义组件特性等。
4. 补充与问题
双线程对比单线程的优势?
双线程架构相比单线程架构的最大优势在于性能和用户体验的提升。通过渲染层和逻辑层的分离,小程序可以在保证高性能和流畅性的前提下处理复杂的业务逻辑,同时维护较好的用户界面响应。
-
性能提升与流畅度
- 双线程:在小程序的双线程架构中,渲染层与逻辑层分别运行在不同的线程中,渲染层通过 Webview 负责页面的显示,逻辑层通过 JSCore 引擎或 V8 引擎处理 JavaScript 业务逻辑。由于这两个线程分开执行,逻辑处理的复杂度不会影响界面渲染的流畅性,避免了 UI 卡顿的问题。
- 单线程:在传统的单线程架构中,渲染与逻辑处理共享同一线程。当业务逻辑复杂或进行耗时的操作时,容易阻塞渲染,导致界面卡顿甚至无响应,影响用户体验。
-
提高用户体验
- 双线程:由于渲染层和逻辑层独立运行,小程序可以在界面渲染时保持较高的响应速度,尤其是在用户与 UI 频繁交互时,保持界面的流畅度和即时响应。逻辑层的复杂计算不会影响用户看到的界面效果。
- 单线程:渲染和逻辑处理相互依赖,较长的逻辑处理可能导致页面渲染延迟,甚至阻塞整个应用的响应,这会显著降低用户体验。
为什么不用 HTML 语法和 WebComponents 来实现渲染,而是选择自定义?
- 管控与安全:web 技术可以通过脚本获取修改页面敏感内容或者随意跳转其它页面
- 能力有限:会限制小程序的表现形式
- 标签众多:增加理解成本
所以,小程序不能直接使用 html 标签渲染页面,其提供了 10 多个内置组件来收敛 web 标签,并且提供一个 JavaScript 沙箱环境来避免 js 访问任何浏览器 api。
为什么要实现 wxs 页面快速计算实时性 ?
如果业务场景为手势识别之类的,监听事件不断的触发,数据不断的改变。这样的业务场景中,可以想像,如果坐标值不断改变的话,在逻辑与视图分开的双线程架构中,线程与线程之间的通讯是非常频繁的,会有很大的性能问题。所以微信开放了一个标记 WXS,可以在渲染层写部分 js 逻辑。这样话就可以在渲染层单独处理频繁改变的数据,这样的话就避免了线程与线程之间频繁通讯导致的性能和延时问题。
Native 层在双线程架构中起到怎样的作用?
双线程的好处不仅仅是一分为二而已,还有强大的 Native 层做背后支撑
Native 层除了做一些资源的动态注入,还负责着很多的事情,请求的转发,离线存储,组件渲染等等。界面主要由成熟的 Web 技术渲染,辅之以大量的接口提供丰富的客户端原生能力。同时,每个小程序页面都是用不同的 WebView 去渲染,这样可以提供更好的交互体验,更贴近原生体验,也避免了单个 WebView 的任务过于繁重。此外,界面渲染这一块还定义了一套内置组件以统一体验,并且提供一些基础和通用的能力,进一步降低开发者的学习门槛。值得一提的是,内置组件有一部分较复杂组件是用客户端原生渲染的,以提供更好的性能。
webview-pageFrame 设计原理
pageFrame 注入的脚本与 pages/index 渲染层 webview 是一样的。正式因为 pageFrame 快速启动技术,就像一个工厂一样,可以快速生成 webview 的基础格式。在这其中 pageFrame 就是业务 webview 的模板。
pageframe.html 快速打开新页面
小程序每个视图层页面内容都是通过 pageframe.html 模板来生成的,包括小程序启动的首页
- 首页启动时,即第一次通过 pageframe.html 生成内容后,后台服务会缓存 pageframe.html 模板首次生成的 html 内容。
- 非首次新打开页面时,页面请求的 pageframe.html 内容直接走后台缓存
- 非首次新打开页面时,pageframe.html 页面引入的外链 js 资源,走本地缓存
这样在后续新打开页面时,都会走缓存的 pageframe 的内容,避免重复生成,快速打开一个新页面。
既然每个视图层页面由 pageframe 模板生成,那么小程序每个页面独有的页面内容如 dom 和样式等如何生成呢?
这主要是利用 nw.js 的 executeScript 方法来执行一段 js 脚本来 注入 当前页面相关的代码,包括当前页面的配置,注入当前页的 css 以及当前页面的 virtual dom 的生成。
视图页面生成的 dom 结构中,document.body 已无 pageframe.html 模板中对应 body 中的 script 内容,这是因为视图层的 WAWebview.js 在通过 virtual dom 生成真实 dom 过程中,它会挂载到页面的 document.body 上,覆盖掉 pageframe.html 模板中对应 document.body 的内容。
结语
微信小程序通过将逻辑层与渲染层分离、引入双线程架构和组件化系统,极大提升了开发效率和用户体验。通过了解其背后的技术架构,开发者可以更灵活地优化代码结构,充分发挥小程序的性能潜力。在日常开发中,理解渲染与逻辑层的通信机制、掌握编译器的工作原理,将帮助开发者更加深入地掌握小程序的开发技巧。