浅析微信小程序的底层架构原理
一、小程序基础知识
小程序是基于WEB规范,采用HTML、CSS和JS等搭建的一套框架,微信官方给它们取的名字:WXML、WXSS,但本质上还是在整个WEB体系之下构建的。WXML说到底就是xml的一个子集。WXML采用微信自定义的少量标签WXSS,大家可以理解为就是自定义的CSS。实现逻辑部分的JS还是通用的ES规范,并且runtime还是Webview(IOS WKWEBVIEW、ANDROID X5)
1、小程序的组成结构
一个完整的小程序主要由以下几部分组成:
一个入口文件:app.js
一个全局样式:app.wxss
一个全局配置:app.json
页面:pages下,每个页面再按文件夹划分,每个页面4个文件
(1)视图层:wxml,wxss
(2)逻辑层:js,json(页面配置,不是必须)
注:pages里面还可以再根据模块划分子目录,孙子目录,只需要在app.json里注册时填写路径就行。
2、小程序项目打包:
编辑器它本身也是基于WEB技术体系实现的,nwjs+react,nwjs简单是说就是node+webkit,node提供给我们本地api能力,而webkit提供给我们web能力,两者结合就能让我们使用JS+HTML实现本地应用程序。既然有nodejs,那上面的打包选项里的功能就好实现了。
(1)ES6转ES5:引入babel-core的node包
(2)CSS补全:引入postcss和autoprefixer的node包(postcss和autoprefixer的原理看这里)
(3)代码压缩:引入uglifyjs的node包
打包后目录结构:
所有的小程序基本都最后都被打成上面的结构:
(1)WAService.js 框架JS库,提供逻辑层基础的API能力
(2)WAWebview.js 框架JS库,提供视图层基础的API能力
(3)WAConsole.js 框架JS库,控制台
(4)app-config.js 小程序完整的配置,包含我们通过app.json里的所有配置,综合了默认配置型
(5)app-service.js 我们自己的JS代码,全部打包到这个文件
(6)page-frame.html 小程序视图的模板文件,所有的页面都使用此加载渲染,且所有的WXML都拆解为JS实现打包到这里
(7)pages 所有的页面,这个不是我们之前的wxml文件了,主要是处理WXSS转换,使用js插入到header区域。
3、与H5页面的区别
小程序和普通的 h5
页面到底有什么区别呢?
(1)运行环境:小程序基于浏览器内核重构的内置解析器,而 h5
的宿主环境是浏览器。所以小程序中没有 DOM
和 BOM
的相关 API
, jQuery
和一些 NPM
包都不能在小程序中使用;
普通网页开发可以使用各种浏览器提供的 DOM API,进行 DOM 操作,而小程序的逻辑层和渲染层是分开的,逻辑层运行在 JSCore 中,并没有一个完整浏览器对象,因而缺少相关的DOM API和BOM API。
(2)系统权限:小程序能获得更多的系统权限,如网络通信状态、数据缓存能力等;
(3)渲染机制:小程序的逻辑层和渲染层是分开的,而 h5
页面 UI
渲染跟 JavaScript
的脚本执行都在一个单线程中,互斥。所以 h5
页面中长时间的脚本运行可能会导致页面失去响应。
普通网页开发渲染线程和脚本线程是互斥的,这也是为什么长时间的脚本运行可能会导致页面失去响应,而在小程序中,二者是分开的,分别运行在不同的线程中。
此外,小程序面对的是 iOS
和 Android
微信客户端和辅助开发的小程序开发者工具。根据官方文档,这三大运行环境也是有所区别的:
所以微信小程序介于 web
端和原生 App
之间,能够丰富调用功能接口,同时又跨平台。
二、小程序架构
1、双线程模型
微信小程序的框架包含两部分:View视图层、App Service逻辑层。View层用来渲染页面结构,App Service层用来逻辑处理、数据请求、接口调用,它们在两个进程(两个Webview)里运行。
视图层和逻辑层通过系统层的JSBridage进行通信,逻辑层把数据变化通知到视图层,触发视图层页面更新,视图层把触发的事件通知到逻辑层进行业务处理。
小程序的渲染层和逻辑层分别由2个线程管理:
(1)视图层:界面渲染相关的任务全都在 WebView
线程里执行。一个小程序存在多个界面,所以渲染层存在多个 WebView
线程。
(2)逻辑层:采用 JsCore
线程运行JS脚本。
视图层和逻辑层通过系统层的 WeixinJsBridage
进行通信:逻辑层把数据变化通知到视图层,触发视图层页面更新,视图层把触发的事件通知到逻辑层进行业务处理。
2、渲染流程
把开发者的 JS 逻辑代码放到单独的线程去运行,但在 Webview 线程里,开发者就没法直接操作 DOM。
那要怎么去实现动态更改界面呢?
如上图所示,逻辑层和试图层的通信会由 Native (微信客户端)做中转,逻辑层发送网络请求也经由 Native 转发。
这也就是说,我们可以把 DOM 的更新通过简单的数据通信来实现。
Virtual DOM 相信大家都已有了解,大概是这么个过程:用 JS 对象模拟 DOM 树 -> 比较两棵虚拟 DOM 树的差异 -> 把差异应用到真正的 DOM 树上。
页面渲染的具体流程是:在渲染层,宿主环境会把 WXML
转化成对应的 JS
对象,在逻辑层发生数据变更的时候,我们需要通过宿主环境提供的 setData
方法把数据从逻辑层传递到渲染层,再经过对比前后差异,把差异应用在原来的Dom树上,渲染出正确的UI界面。
(1)在渲染层把 WXML 转化成对应的 JS 对象。
(2)在逻辑层发生数据变更的时候,通过宿主环境提供的 setData 方法把数据从逻辑层传递到 Native,再转发到渲染层。
(3)经过对比前后差异,把差异应用在原来的 DOM 树上,更新界面。
我们通过把 WXML 转化为数据,通过 Native 进行转发,来实现逻辑层和渲染层的交互和通信。
3、双线程模型设计的好处
双线程模型是小程序框架与业界大多数前端 Web
框架不同之处。基于这个模型,可以更好地管控以及提供更安全的环境。缺点是带来了无处不在的异步问题(任何数据传递都是线程间的通信,也就是都会有一定的延时),不过小程序在框架层面已经封装好了异步带来的时序问题。
为什么要这样设计呢,前面也提到了管控和安全,为了解决这些问题,我们需要阻止开发者使用一些,例如浏览器的window对象,跳转页面、操作DOM、动态执行脚本的开放性接口。
我们可以使用客户端系统的 JavaScript 引擎(iOS 下的 JavaScriptCore 框架,安卓下腾讯 x5 内核提供的 JsCore 环境),这个沙箱环境只提供纯 JavaScript 的解释执行环境,没有任何浏览器相关接口,这就是小程序双线程模型的由来。
三、组件系统
我们知道小程序是有自己的组件的,这些基本组件就是基于 Exparser
框架。 Exparser
基于 WebComponents
的 ShadowDOM
模型,但是不依赖浏览器的原生支持,而且可在 纯 JS
环境中运行。
1、Exparser框架
Exparser是微信小程序的组件组织框架,内置在小程序基础库中,为小程序的各种组件提供基础的支持。小程序内的所有组件,包括内置组件和自定义组件,都由Exparser组织管理。
Exparser的主要特点包括以下几点:
(1)基于Shadow DOM模型:模型上与WebComponents的ShadowDOM高度相似,但不依赖浏览器的原生支持,也没有其他依赖库;实现时,还针对性地增加了其他API以支持小程序组件编程。
(2)可在纯JS环境中运行:这意味着逻辑层也具有一定的组件树组织能力。
(3)高效轻量:性能表现好,在组件实例极多的环境下表现尤其优异,同时代码尺寸也较小。
小程序中,所有节点树相关的操作都依赖于Exparser,包括WXML到页面最终节点树的构建、createSelectorQuery调用和自定义组件特性等。
2、内置组件
基于Exparser框架,小程序内置了一套组件,提供了视图容器类、表单类、导航类、媒体类、开放类等几十种组件。有了这么丰富的组件,再配合WXSS,可以搭建出任何效果的界面。在功能层面上,也满足绝大部分需求。
3、原生组件
在内置组件中,有一些组件并不完全在 Exparser
的渲染体系下,而是由客户端原生参与组件的渲染。比如说 Map
组件,它渲染的层级比在 WebView
层渲染的普通组件要高。
四、运行机制
(1)启动
热启动:假如用户已经打开过某小程序,然后在一定时间内再次打开该小程序,此时无需重新启动,只需将后台态的小程序切换到前台,这个过程就是热启动;
冷启动:用户首次打开或小程序被微信主动销毁后再次打开的情况,此时小程序需要重新加载启动,即冷启动。
小程序没有重启的概念
当小程序进入后台,客户端会维持一段时间的运行状态,超过一定时间后(目前是5分钟)会被微信主动销毁
当短时间内(5s)连续收到两次以上收到系统内存告警,会进行小程序的销毁
(2)销毁
只有当小程序进入后台一定时间,或者系统资源占用过高,才会被真正的销毁。
(3)更新机制
开发者在后台发布新版本之后,无法立刻影响到所有现网用户,但最差情况下,也在发布之后 24 小时之内下发新版本信息到用户。
小程序每次冷启动时,都会检查是否有更新版本,如果发现有新版本,将会异步下载新版本的代码包,并同时用客户端本地的包进行启动,即新版本的小程序需要等下一次冷启动才会应用上。
所以如果想让用户使用最新版本的小程序,可以利用 wx.getUpdateManager
做个检查更新的功能:
checkNewVersion() {
const updateManager = wx.getUpdateManager();
updateManager.onCheckForUpdate((res) => {
console.log('hasUpdate', res.hasUpdate);
// 请求完新版本信息的回调
if (res.hasUpdate) {
updateManager.onUpdateReady(() => {
this.setData({
hasNewVersion: true
});
});
}
});
}
五、小程序的技术实现
小程序的UI视图和逻辑处理是用多个webview实现的,逻辑处理的JS代码全部加载到一个Webview里面,称之为AppService,整个小程序只有一个,并且整个生命周期常驻内存,而所有的视图(wxml和wxss)都是单独的Webview来承载,称之为AppView。
所以一个小程序打开至少就会有2个webview进程,正式因为每个视图都是一个独立的webview进程,考虑到性能消耗,小程序不允许打开超过5个层级的页面,当然同是也是为了体验更好。
1、AppService
可以理解AppService即一个简单的页面,主要功能是负责逻辑处理部分的执行,底层提供一个WAService.js的文件来提供各种api接口,主要是以下几个部分:
消息通信封装为WeixinJSBridge(开发环境为window.postMessage, IOS下为WKWebview的window.webkit.messageHandlers.invokeHandler.postMessage,android下用WeixinJSCore.invokeHandler)
日志组件Reporter封装
wx对象下面的api方法
全局的App,Page,getApp,getCurrentPages等全局方法
还有就是对AMD模块规范的实现
然后整个页面就是加载一堆JS文件,包括小程序配置config,上面的WAService.js(调试模式下有asdebug.js),剩下就是我们自己写的全部的js文件,一次性都加载。
2、线上环境
而在上线后是应用部分会打包为2个文件,名称app-config.json和app-service.js,然后微信会打开webview去加载。线上部分应该是微信自身提供了相应的模板文件,在压缩包里没有找到。
WAService.js(底层支持)
app-config.json(应用配置)
app-service.js(应用逻辑)
然后运行在JavaScriptCore引擎里面。
3、AppView
这里可以理解为h5的页面,提供UI渲染,底层提供一个WAWebview.js来提供底层的功能,具体如下:
消息通信封装为WeixinJSBridge(开发环境为window.postMessage, IOS下为WKWebview的window.webkit.messageHandlers.invokeHandler.postMessage,android下用WeixinJSCore.invokeHandler)
日志组件Reporter封装
wx对象下的api,这里的api跟WAService里的还不太一样,有几个跟那边功能差不多,但是大部分都是处理UI显示相关的方法
小程序组件实现和注册
VirtualDOM,Diff和Render UI实现
页面事件触发
在此基础上,AppView有一个html模板文件,通过这个模板文件加载具体的页面,这个模板主要就一个方法,$gwx,主要是返回指定page的VirtualDOM,而在打包的时候,会事先把所有页面的WXML转换为ViirtualDOM放到模板文件里,而微信自己写了2个工具wcc(把WXML转换为VirtualDOM)和wcsc(把WXSS转换为一个JS字符串的形式通过style标签append到header里)。
4、Service和View通信
使用消息publish和subscribe机制实现两个Webview之间的通信,实现方式就是统一封装一个WeixinJSBridge对象,而不同的环境封装的接口不一样,具体实现的技术如下:
(1)windows环境
通过window.postMessage实现(使用chrome扩展的接口注入一个contentScript.js,它封装了postMessage方法,实现webview之间的通信,并且也它通过chrome.runtime.connect方式,也提供了直接操作chrome native原生方法的接口)
发送消息:window.postMessage(data, ‘*’); // data里指定 webviewID
接收消息:window.addEventListener(‘message’, messageHandler);// 消息处理并分发,同样支持调用nwjs的原生能力。
(2)IOS
通过 WKWebview的window.webkit.messageHandlers.NAME.postMessage实现微信navite代码里实现了两个handler消息处理器:
invokeHandler: 调用原生能力
publishHandler: 消息分发
六、性能优化
主要的优化策略可以归纳为三点:
(1)精简代码,降低WXML结构和JS代码的复杂性;
(2)合理使用setData调用,减少setData次数和数据量;
(3)必要时使用分包优化。
1、setData 工作原理
小程序的视图层目前使用 WebView 作为渲染载体,而逻辑层是由独立的 JavascriptCore 作为运行环境。
在架构上,WebView 和 JavascriptCore 都是独立的模块,并不具备数据直接共享的通道。
当前,视图层和逻辑层的数据传输,实际上通过两边提供的 evaluateJavascript 所实现。即用户传输的数据,需要将其转换为字符串形式传递,同时把转换后的数据内容拼接成一份 JS 脚本,再通过执行 JS 脚本的形式传递到两边独立环境。
而 evaluateJavascript 的执行会受很多方面的影响,数据到达视图层并不是实时的。
2、常见的 setData 操作错误
(1)频繁的去 setData
在我们分析过的一些案例里,部分小程序会非常频繁(毫秒级)的去setData,其导致了两个后果:Android下用户在滑动时会感觉到卡顿,操作反馈延迟严重,因为 JS 线程一直在编译执行渲染,未能及时将用户操作事件传递到逻辑层,逻辑层亦无法及时将操作处理结果及时传递到视图层;渲染有出现延时,由于 WebView 的 JS 线程一直处于忙碌状态,逻辑层到页面层的通信耗时上升,视图层收到的数据消息时距离发出时间已经过去了几百毫秒,渲染的结果并不实时;
(2)每次 setData 都传递大量新数据
由setData的底层实现可知,我们的数据传输实际是一次 evaluateJavascript 脚本过程,当数据量过大时会增加脚本的编译执行时间,占用 WebView JS 线程
(3)后台态页面进行setData
当页面进入后台态(用户不可见),不应该继续去进行setData,后台态页面的渲染用户是无法感受的,另外后台态页面去setData也会抢占前台页面的执行。
参考文章: