前言
闲言少叙,先来瞅瞅小程序的双线程架构图:
今天的分享主要聚焦于渲染层。
小程序是如何实现快速渲染的?
在回答这个问题之前,我们首先要清楚,小程序不是像vue和react那样的单页应用,小程序的每一个页面都是一个webview控件,它是一种多页应用程序。当我们调用完navigateTo后,前一个页面并没有被卸载掉,这也是为什么我们可以通过左右滑动手机屏幕来切换路由。
上面提到的webview,这里简单说明一下:它是一个精简的浏览器,是native层的一个控件,只是它去掉了浏览器的工具栏。当我们知道小程序每打开一个页面,就相当于打开了一个小型浏览器的时候,可以想象,这种打开速度不可能会很快。因此想要体现小程序的优势,快速打开一个页面的架构设计是必须的,微信使用了一种叫做pageFrame的预渲染方式来解决此问题。
小程序承载容器:webview
我们可以通过微信开发这工具来找到小程序代码编译之后的执行代码。

开发者工具调试器截图,我们可以看到其实就是html页面。

我们在开发者工具的控制台,通过搜索webview,可以找到所有的webview。

开发者工具中的webview,包含以下4种:
- 视图层webview
- 逻辑层webview
- 调试器webview
- 编辑区webview
我们主要看视图层和逻辑层:
视图层:
- 已打卡页面的webview,如:pages/index/index,每打开一个页面会创建一个webview,最多打开10个页面,否则会占用庞大内存。
- 预渲染的pageframe:第一次创建预渲染pageframe时会做一些初始化动作:启动webview、初始化基础库、其他的优化措施,之后会将内容缓存起来,以供后面的pageframe使用。
逻辑层:
- 逻辑层的webview,id=appservice,永远只有一个。
路由的跳转方式有以下几种,每种路由跳转方法对webview页面层级的处理略有不同:
- navigateTo:每次调用时,都会用旧的pageFrame模版进行页面渲染,然后创建新的pageFrame。会触发离开页面的onHide事件
- navigateBack: 会删除当前页面层级的webview
- redirectTo:在当前页面层级的webview中进行重新渲染
navigateTo跳转渲染示例:

webView内部结构
webview的内部结构无法直接查看,但我们可以借助debug控制台查看。
预渲染webview结构分析
首先,在开发者工具调试界面输入“document.getElementsByTagName(‘webview’)”打印出所有的webview。

在控制台输入“document.getElementsByTagName(‘webview’)[0].showDevTools(true, null)”,打开预渲染webview页面。

通过查看dom结构,可以找到pageFrame初始化时加载的js资源:
- wxconfig.js – 小程序默认配置项;
- deviceinfo.js – 设备信息;
- jsdebig.js – debug工具;
- WAWebview.js – 渲染层的底层基础库;
- hjs.js – 优秀的视频流处理工具;
- WARemoteDebug.js – 底层基础库调试工具
此外我们还在预渲染的webview中发现了下面一段代码:

这段代码用于监听页面加载完成,并通过alert方法发出页面的“DOCUMENT_READY”信息。基础库底层通过给webview控件绑定dialog事件,就能收到信息,此时可以确定预渲染环境准备完毕。当我们要打开新页面的时候,就可以将页面代码注入到pageFrame。
已渲染页面webview结构
我们可以用同样的方法打开pages/index/index的webview页面。

pages/index/index 对应的调试界面

我们看到页面body中的内容很熟悉,这是经过编译后的wxml页面内容。
我们在script标签中找到一下代码:

其中“history.pushState(”, ”, ‘http://127.0.0.1:56539/__pageframe__/pages/index/index‘)”,这句代码的作用修改当前webview的src,因为预渲染webview的src为instanceframe.html
,通过这句代码将其变更为具体的页面地址。紧接着,向小程序配置项(wxConfig)对象中追加了页面的配置信息和页面组配置。
渲染层文件编译分析
wxss的处理
用来编译wxss的工具叫WCSC。我们可以在微信开发者工具的控制台输入“openVender()”打开如下文件:

index.wxss被编译后的生成一个js文件:wxss.js,我们先来看wxss.js的内容。
wxss.js的第一部分是获取和处理设备的基本信息,如:设备高度,设备宽度,设备像素比等。

有了设备信息之后,就是对rpx的转换处理。

最后一部分内容比较长,但是其最终的目的是setCssToHead,也就是将css内容插入到dom的head中。

wxml的处理
用来编译wxml的工具叫WCC,它与WCSC在同一个文件夹下:

index.wxml被编译后生成:wxml.js文件,我们看一下它的代码结构。
wxml.js中的内容进行了代码混淆,无法清晰分析代码逻辑,但是我们仍然可以看出整个文件的核心是一个自执行函数,变量“gwx是一个可以接收两个变量的函数。



那么$gwx函数返回的结果是什么呢?

通过实验我们可以看出,$gwx在接收wxml文件路径之后,同样会返回一个函数:generateFunc,generateFunc执行的结果返回的就是我们常说的虚拟DOM!
那为什么$gwx函数不直接返回虚拟DOM树呢,而是先返回generateFunc?是因为需要注入动态数据。
渲染层编译结果储存容器:wxAppCode
我们在webview的head中,找到了一个script标签,里面执行了如下代码:

我们看到这段代码定义了一个全局变量__wxAppCode__。
我们通过在控制台打印,可以看到wxAppCode的内容。

我们从打印结果可以看到,“xxxx/index.wxml”和”xxxx/index.wxss” 保存的都是函数。

- xxxx/index.wxml:generateFunc,执行之后返回虚拟dom;
- xxxx/index.wxss:调用后最终会执行setCssToHead;
从虚拟DOM到页面渲染,需要经过底层基础库的处理。底层基础库使用了Exparser框架来组织和管理内置组件和自定义组件。
Exparser的组件模型与WebComponents标准中的Shadow DOM高度相似。
Exparser会维护整个页面的节点树相关信息,包括节点的属性、事件绑定等,相当于一个简化版的Shadow DOM实现。Exparser的主要特点包括以下几点:
- 基于Shadow DOM模型:模型上与WebComponents的Shadow DOM高度相似,但不依赖浏览器的原生支持,也没有其他依赖库;实现时,还针对性地增加了其他API以支持小程序组件编程。
- 可在纯JS环境中运行:这意味着逻辑层也具有一定的组件树组织能力。
- 高效轻量:性能表现好,在组件实例极多的环境下表现尤其优异,同时代码尺寸也较小。
渲染层基础库:WAWebview.js模块介绍
core-js模块: core-js负责初始化框架js代码,编译js,加载业务逻辑js等功能。
Fundation: Foundation是基础模块
- 事件管理器:eventEmiter
- 监听和处理:配置的
Ready
事件、基础库Ready时间,js bridge Ready事件,初始化global和环境变量
WeixinJsBridge: 通讯模块,负责渲染层,Native层,逻辑层,三者之间的通讯管理
Reporter: 这个模块负责上报统计信息,包括错误信息、性能数据等。它帮助小程序开发者和微信团队了解和优化小程序的性能。
Exparser: 组件系统模块。
我们前面看到 view 组件被编译后,在dom中显示为 ‘wx-view’,其实就是Exparser 将虚拟dom转换成了web可识别的html自定义标签(webCompoent)。

virtualDom:和Vue、React中virtualDOM实现相似,但这里它主要模拟了DOM 接口上面的element() 对象。
默认样式注入:最后的模块就是默认样式注入。

总结
本次分享主要介绍了,渲染层页面之所以能够快速渲染的逻辑以及wxml和wxss的编译结果分析。小程序的底层架构还有很多内容值得我们去探索,比如:通信模块的设计,事件体系的设计,逻辑层的功能分析等。
意外发现:
这难道就是开发者工具常常崩溃的原因?
