微信小程序架构深度解析
微信小程序架构深度解析
微信小程序作为一种介于Web App和原生App之间的应用形态,近年来在移动互联网领域得到了广泛应用。本文将深入分析微信小程序的架构设计,包括其运行环境、双线程模型、启动机制、视图和逻辑层的通信方式,以及开发和优化建议。
一、微信小程序概述
微信小程序本质上是一种混合应用,介于Web App与原生App之间,既具备丰富的调用手机功能的接口,又保持了灵活性和跨平台的特点。
1. 运行环境差异
微信小程序运行在三端:iOS、Android 和 用于调试的开发者工具。三端的脚本执行环境以及用于渲染非原生组件的环境是各不相同的:
- 在 iOS 上,小程序的 JavaScript 代码是运行在 JavaScriptCore 中,是由 WKWebView 来渲染的,环境有 iOS8、iOS9、iOS10 。
- 在 Android 上,小程序的 JavaScript 代码是通过 X5 JSCore来解析,是由 X5 基于 Mobile Chrome 53/57 内核来渲染的 。
- 在开发工具上,小程序的 JavaScript 代码是运行在 nwjs 中,是由 Chrome Webview 来渲染的。
2. 与 H5 页面的区别
从技术的发展角度来看,微信小程序是从微信中的 webView 和 JS-SDK 进化到了今天的形态。
- 运行环境:小程序基于浏览器内核重构的内置解析器,而 H5 的宿主环境是浏览器。所以小程序中没有 DOM 和 BOM 的相关 API,jQuery 和一些 NPM 包都不能在小程序中使用。
- 系统权限:小程序能获得更多的系统权限,如网络通信状态、数据缓存能力等。
- 渲染机制:小程序的逻辑层和渲染层是分开的,双线程同时运行。渲染层的界面使用 WebView 进行渲染;逻辑层采用 JSCore 运行 JavaScript 代码。而 H5 页面 UI 渲染跟 JavaScript 的脚本执行都在一个单线程中,互斥。所以 h5 页面中长时间的脚本运行可能会导致页面失去响应。
3. 小程序目录结构
project
├── pages
| ├── index
| | ├── index.json index 页面配置
| | ├── index.js index 页面逻辑
| | ├── index.wxml index 页面结构
| | └── index.wxss index 页面样式表
| └── log
| ├── log.json log 页面配置
| ├── log.wxml log 页面逻辑
| ├── log.js log 页面结构
| └── log.wxss log 页面样式表
├── app.js 小程序逻辑
├── app.json 小程序公共设置
└── app.wxss 小程序公共样式表
4. 为什么小程序比较快
- 安装包缓存
- 分包加载
- 独立渲染线程
- Webview 预加载
- Native 组件
二、小程序架构
1. 双线程模型
微信小程序的框架包含两部分:View 视图层(可能存在多个)和 App Service 逻辑层(一个)。View 层用来渲染页面结构,AppService 层用来逻辑处理、数据请求、接口调用,它们在两个线程里运行。视图层使用 WebView 渲染,逻辑层使用 JSCore 运行。
视图层和逻辑层通过系统层的 WeixinJsBridage 进行通信,逻辑层把数据变化通知到视图层,触发视图层页面更新,视图层把触发的事件通知到逻辑层进行业务处理。
页面渲染的具体流程是:在渲染层,宿主环境会把 WXML 转化成对应的 JS 对象,在逻辑层发生数据变更的时候,我们需要通过宿主环境提供的 setData 方法把数据从逻辑层传递到渲染层,再经过对比前后差异,把差异应用在原来的 Dom 树上,渲染出正确的 UI 界面。
双线程模型是小程序框架与业界大多数前端 Web 框架不同之处。基于这个模型,可以更好地管控以及提供更安全的环境。缺点是带来了无处不在的异步问题(任何数据传递都是线程间的通信,也就是都会有一定的延时),不过小程序在框架层面已经封装好了异步带来的时序问题。
2. 视图和逻辑通信
多 WebView 模式下,每一个 WebView 都有一个独立的 JSContext,那视图和逻辑是如何进行通讯?如下图双线程生命周期所示。
相对于浏览器双线程模型:
- 更加安全,因为微信小程序阻止开发者使用一些浏览器提供的一些功能,如操作 DOM、动态执行脚本等
- 不用等待浏览器主线程去下载并解析 html,遇到 JS 脚本还会阻塞,影响视图渲染,造成白屏
- 缺点是双线程如果频繁的通信,操作 setData 更新视图,对性能消耗特别严重,例如拖拽、滚动等
三、小程序启动加载
1. 运行机制
小程序启动会有两种情况,一种是「冷启动」,一种是「热启动」。假如用户已经打开过某小程序,然后在一定时间内再次打开该小程序,此时无需重新启动,只需将后台态的小程序切换到前台,这个过程就是热启动;冷启动指的是用户首次打开或小程序被微信主动销毁后再次打开的情况,此时小程序需要重新加载启动。
2. 更新机制
小程序冷启动时如果发现有新版本,将会异步下载新版本的代码包,并同时用客户端本地的包进行启动,即新版本的小程序需要等下一次冷启动才会应用上。如果需要马上应用最新版本,可以使用 wx.getUpdateManager API 进行处理。
checkNewVersion() {
const updateManager = wx.getUpdateManager();
updateManager.onCheckForUpdate((res) => {
console.log('hasUpdate', res.hasUpdate);
// 请求完新版本信息的回调
if (res.hasUpdate) {
updateManager.onUpdateReady(() => {
this.setData({
hasNewVersion: true
});
});
}
});
}
3. 运行机制
- 小程序没有重启的概念
- 当小程序进入后台,客户端会维持一段时间的运行状态,超过一定时间后(目前是5分钟)会被微信主动销毁
- 当短时间内(5s)连续收到两次以上收到系统内存告警,会进行小程序的销毁
四、View(页面视图)
视图层由 WXML 与 WXSS 编写,由组件来进行展示。将逻辑层的数据反应成视图,同时将视图层的事件发送给逻辑层。
- View - WXML
wcc 编译器负责将 wxml 编译成 js 文件
- View - WXSS
- wcsc 编译 wxss 得到一个 js 文件
- 添加尺寸单位 rpx 转换,可根据屏幕宽度自适应
- 提供 setCssToHead 方法将转换后的 css 内容添加到 header
- View - Component
- 小程序的组件基于 Exparser 框架。Exparser 基于 WebComponents 的 ShadowDOM 模型,但是不依赖浏览器的原生支持,而且可在纯 JS 环境中运行。
- 小程序中,所有节点树相关的操作都依赖于 Exparser,包括 WXML 到页面最终节点树的构建、CreateSelectorQuery 调用和自定义组件特性等。
- View - Native Component
- 目前 Native 实现的组件有
- Native 组件渲染的层级比在 WebView 层渲染的普通组件要高。
- 引入原生组件的优点是:
- 扩展 Web 的能力
- 体验更好,减轻 WebView 的渲染工作
- 绕过 setData、数据通信和重渲染流程,性能更好
五、WebView 预加载
每次小程序进入除了当前页面,Native 预先额外加载一个 WebView。当打开指定页面时,用默认数据直接渲染,请求数据回来时局部更新。返回显示历史 View。退出小程序,View 状态不销毁。
六、App Service(逻辑层)
逻辑层将数据进行处理后发送给视图层,同时接受视图层的事件反馈。
- App( ) 小程序的入口;Page( ) 页面的入口
- 提供丰富的 API,如微信用户数据,扫一扫,支付等微信特有能力。
- 每个页面有独立的作用域,并提供模块化能力。
- 数据绑定、事件分发、生命周期管理、路由管理
运行环境
- IOS - JSCore
- Android - X5 JS 解析器
- DevTool - nwjs Chrome 内核
- App Service - Binding
- 数据绑定使用 Mustache 语法(双大括号)将变量包起来,动态数据均来自对应 Page 的 data,可以通过 setData 方法修改数据。
- 事件绑定的写法同组件的属性,以 key、value 的形式,key 以 bind 或 catch 开头,然后跟上事件的类型,如 bindtap, catchtouchstart,value 是一个字符串,需要在对应的 Page 中定义同名的函数。
- App Service - Life Cycle
- App Service - API
API 通过 WeixinJSBridge 和 Native 进行通信
- App Service - Router
- navigateTo(OBJECT) - 保留当前页面,跳转到应用内的某个页面,使用 navigateBack 可以返回到原页面。页面路径只能是五层
- redirectTo(OBJECT) - 关闭当前页面,跳转到应用内的某个页面。
- navigateBack(OBJECT) - 关闭当前页面,返回上一页面或多级页面。可通过 getCurrentPages()) 获取当前的页面栈,决定需要返回几层。
七、小程序开发经验
- 小程序存在的问题
- 小程序仍然使用 WebView 渲染,并非原生渲染
- 需要独立开发,不能在非微信环境运行 。
- 开发者不可以扩展新组件。
- 依赖浏览器环境的 js 库不能使用,因为是 JSCore 执行的,没有 window、document 对象。
- WXSS 中无法使用本地(图片、字体等)。
- WXSS 转化成 js 而不是 css。
- WXSS 不支持级联选择器。
- 小程序无法打开页面,无法拉起 APP。
- 小程序的优点
- 提前新建 WebView,准备新页面渲染。
- View 层和逻辑层分离,通过数据驱动,不直接操作 DOM。
- 使用 Virtual DOM,进行局部更新。
- 全部使用 https,确保传输中安全。
- 加入 rpx 单位,隔离设备尺寸,方便开发。
rpx(responsive pixel): 可以根据屏幕宽度进行自适应。规定屏幕宽为 750rpx。如在 iPhone6 上,屏幕宽度为 375px,共有 750 个物理像素,则 750rpx = 375px = 750 物理像素,1rpx = 0.5px = 1 物理像素。
设备 rpx 换算 px (屏幕宽度/750) px 换算 rpx (750/屏幕宽度)
iPhone5 1rpx = 0.42px 1px = 2.34rpx
iPhone6 1rpx = 0.5px 1px = 2rpx
iPhone6Plus 1rpx = 0.552px 1px = 1.81rpx
八、代码运行
运行时,外面包裹 define,代码作为回调,当调用回调时,只传入前面三个值,由于后面的变量都是局部定义的变量,就屏蔽了 window,document 等这些变量。其中 O 就是上面 define('app.js',callback),的回调,回调值传入了三个参数,屏蔽了其他属性。
九、优化建议(官方建议)
1. setData 工作原理
小程序的视图层目前使用 WebView 作为渲染载体,而逻辑层是由独立的 JavascriptCore 作为运行环境。在架构上,WebView 和 JavascriptCore 都是独立的模块,并不具备数据直接共享的通道。当前,视图层和逻辑层的数据传输,实际上通过两边提供的 evaluateJavascript 所实现。即用户传输的数据,需要将其转换为字符串形式传递,同时把转换后的数据内容拼接成一份 JS 脚本,再通过执行 JS 脚本的形式传递到两边独立环境。
而 evaluateJavascript 的执行会受很多方面的影响,数据到达视图层并不是实时的。
常见的 setData 操作错误
- 频繁的去 setData
在我们分析过的一些案例里,部分小程序会非常频繁(毫秒级)的去 setData,其导致了两个后果:
- Android 下用户在滑动时会感觉到卡顿,操作反馈延迟严重,因为 JS 线程一直在编译执行渲染,未能及时将用户操作事件传递到逻辑层,逻辑层亦无法及时将操作处理结果及时传递到视图层。
- 渲染有出现延时,由于 WebView 的 JS 线程一直处于忙碌状态,逻辑层到页面层的通信耗时上升,视图层收到的数据消息时距离发出时间已经过去了几百毫秒,渲染的结果并不实时。
- 每次 setData 都传递大量新数据
由 setData 的底层实现可知,我们的数据传输实际是一次 evaluateJavascript 脚本过程,当数据量过大时会增加脚本的编译执行时间,占用 WebView JS 线程。
- 后台态页面进行 setData
当页面进入后台态(用户不可见),不应该继续去进行 setData,后台态页面的渲染用户是无法感受的,另外后台态页面去 setData 也会抢占前台页面的执行。
2. 图片资源
- 目前图片资源的主要性能问题在于大图片和长列表图片上,这两种情况都有可能导致 iOS 客户端内存占用上升,从而触发系统回收小程序页面。
- 在 iOS 上,小程序的页面是由多个 WKWebView 组成的,在系统内存紧张时,会回收掉一部分 WKWebView。从过去我们分析的案例来看,大图片和长列表图片的使用会引起 WKWebView 的回收。
3. 代码包大小的优化
开发者在实现业务逻辑同时也有必要尽量减少代码包的大小,因为代码包大小直接影响到下载速度,从而影响用户的首次打开体验。除了代码自身的重构优化外,还可以从这两方面着手优化代码大小:
- 分包加载对小程序进行分包,可以优化小程序首次启动的下载时间
- 清理没有使用到的代码和资源
目前小程序打包是会将工程下所有文件都打入代码包内,也就是说,这些没有被实际使用到的库文件和资源也会被打入到代码包里,从而影响到整体代码包的大小。
4. 预先加载数据
原理
小程序在启动时,会直接加载所有页面逻辑代码进内存,即便 page2 可能都不会被使用。在 page1 跳转至 page2 时,page1 的逻辑代码 Javascript 数据也不会从内存中消失。page2 甚至可以直接访问 page1 中的数据。
小程序的这种机制差异正好可以更好的实现预加载。通常情况下,我们习惯将数据拉取写在 onLoad 事件中。但是小程序的 page1 跳转到 page2,到 page2 的 onLoad 是存在一个 300ms ~ 400ms 的延时的。如下图:
因为小程序的特性,完全可以在 page1 中预先拿取数据,然后在 page2 中直接使用数据,这样就可以避开 redirecting 的 300ms ~ 400ms 了。如下图:
渲染 view 线程和 App Servcie 是相互独立的,对于 App Servcie 中 js 运行不会阻塞 view 的渲染
官方的示例也是采用这种方式:先 App 中请求数据,在 index.js 使用数据