架构篇-快速渲染设计原理之PageFrame|小程序原理专题
# PageFrame
还记得在前面文章中我们寻找渲染层的时候,找到了个奇怪的webview。

pageFrame这个webview是干嘛的呢?我们这章节就来探索一下。
我们在写小程序页面视图时,貌似并不关心webview中的html结构,这些都是小程序底层帮我们实现,我们只需要写页面ui和业务逻辑即可。下面我们来看看view视图层小程序帮我们做了什么。先来看一下视图层pageframe.html的模板:

为了讲解便利,中间部分代码由标识进行替代。从上图上看来是不是和渲染层的HTML非常像。这个webview其实就是一个用来新渲染webview的模板。
我们已知在小程序中使用 WXML 文件描述页面的结构,WXSS 文件描述页面的样式,WXML文件会被编译为虚拟DOM,WXSS会被编译为js。那么每个独立的页面都会经过这样的编译,如何快速的生成webview,或者说如何快速的打开一个页面。会成为一个问题。我们有了虚拟DOM和样式描述表就可以渲染页面了吗。还远远不够。
先分析一下pageFrame的html结构中注入的js资源:
-
./__dev__/wxconfig.js:小程序默认总配置项,包括用户自定义与系统默认的整合结果。在控制台输入
__wxConfig可以看出打印结果
-
./__dev__/devtoolsconfig.js小程序开发者配置,包括
navigationBarHeight,标题栏的高度,状态栏高度,等等,控制台输入__devtoolsconfig可以看到其对应的信息

-
./__dev__/deviceinfo.js设备信息,包含
尺寸/像素点pixelRatio -
__dev__/jsdebug.jsdebug工具。
-
./__dev__/WAWebview.js渲染层底层基础库
-
./__dev__/hls.js优秀的视频流处理工具。
-
./__dev__/WARemoteDebug.js底层基础库调试工具
可以看到pageFrame注入的脚本与我们分析pages/index渲染层webview是一样的。正式因为pageFrame快速启动技术,就像一个工厂一样,可以快速生成webview的基础格式。在这其中pageFrame就是业务webview的模板。
我们继续往下分析,有一些不一样的地方。
<!-- wxappcode -->
可以发现pageFrame中包含一些注释的单词,这些注释的单词为注释占位符,如果你细心对比就会发现,在前面分析渲染层代码的时候,我们见到过这些注释占位符编译后的样子。
比如wxappcode.js在渲染层中为如下形式(老版本基础库)
<script src="./__dev__/wxappcode.js" chartset="UTF-8"></script>
或者是这样(新版本基础库)
无论新老版本,其中的代码都是一样的。我们先看一下内部结构。

文件中包含了所有页面的编译路径,这个编译路径有什么作用呢,小册后面章节virtualDOM章节中,编译WXML后生成的$gwx函数需要的参数就是这个路径。此文件中就进行了保存。
再接着看的话会看到WXSS章节讲到的编译后的WXSS,就跟我们在渲染层看到的一样,eval(setCssToHead....)。
内部记录的属性有:
decodePathName.json配置.wxml编译后的$gwx函数。$gwx函数会在virtualDOM章节重点讲解。.wxss编译后的eval函数。
这里就可以想象出来,如果小程序需要打开某个页面的时候,只需要从这里提取出页面特有的这几个属性,配合pageFrame模板就可以快速生成一个新的webview。
那么具体快速启动的方案是怎样的呢?我们继续探索
# 如何快速启动
我们看一下官方给予的一段描述:
在视图层内,小程序的每一个页面都独立运行在一个页面层级上。小程序启动时仅有一个页面层级,每次调用wx.navigateTo,都会创建一个新的页面层级;相对地,wx.navigateBack会销毁一个页面层级。 对于每一个新的页面层级,视图层都需要进行一些额外的准备工作。在小程序启动前,微信会提前准备好一个页面层级用于展示小程序的首页。除此以外,每当一个页面层级被用于渲染页面,微信都会提前开始准备一个新的页面层级,使得每次调用wx.navigateTo都能够尽快展示一个新的页面。 页面层级的准备工作分为三个阶段。第一阶段是启动一个WebView,在iOS和Android系统上,操作系统启动WebView都需要一小段时间。第二阶段是在WebView中初始化基础库,此时还会进行一些基础库内部优化,以提升页面渲染性能。第三阶段是注入小程序WXML结构和WXSS样式,使小程序能在接收到页面初始数据之后马上开始渲染页面(这一阶段无法在小程序启动前执行)。
对于wx.redirectTo,这个调用不会打开一个新的页面层级,而是将当前页面层级重新初始化:重新传入页面的初始数据、路径等,视图层清空当前页面层级的渲染结果然后重新渲染页面。

我们对官方的这段描述进行一个解释:
视图层内,每个页面我们已经知道了都是一个webview,当小程序启动的时候只有一个首页webview,我们前面的例子中为pages/index页面。
我们在去寻找index页面的渲染层的时候做过这样一件事情,寻找webview标签,进入渲染层的控制台。在寻找webview渲染层的时候,我们找到了四个webview

- pages/index
- pages/pageFrame
- appService.js // 逻辑层
- 微信开发者工具
wx.navigateTo为新开一个页面,在微信小程序中的表现形式为从右往左划出来的一个页面,为什么如此的丝滑,想必与pageFrame这层webview密不可分。在执行wx.navigateTo新开一个页面的时候,就是创建一个新的webview并插入到视图层中。这里忘记的可以返回到webview章节,有一个部分专门解释了五个webview的部分,并且创建新webview并插入这个操作是有最大值限制的,有关于性能方面的考虑。
wx.navigateBack则为销毁webview。
我们在打开pages/logs/logs视图页面时,发现dom中多加载了一个__pageframe__/pageframe.html的视图层,其模板内容正如上方描述的。这个视图层的作用正是为了小程序提前为一个新的页面层准备的。
小程序每个视图层页面内容都是通过pageframe.html模板来生成的,包括小程序启动的首页;下面来看看小程序为快速打开小程序页面做的技术优化:
- 首页启动时,即第一次通过
pageframe.html生成内容后,后台服务会缓存pageframe.html模板首次生成的html内容。 - 非首次新打开页面时,页面请求的
pageframe.html内容直接走后台缓存 - 非首次新打开页面时,
pageframe.html页面引入的外链js资源(如上图所示)走本地缓存
这样在后续新打开页面时,都会走缓存的pageframe的内容,避免重复生成,快速打开一个新页面。
那么视图层新打开页面的流程是怎样的呢?
其实在小程序开发者工具实现中,在创建每个视图层页的webview时,都会为其绑定了onLoadCommit事件(它会在页面加载完成后触发,包含当前文档的导航和副框架的文档加载)。初始时webview的src会被指定为空页面地址http://127.0.0.1:${global.proxyPort}/aboutblank?${c},其中c为对应webview的id。webview从空页面到具体页面视图的过程如下:
- 空页面地址webview加载完毕后执行事件中的
reload方法,即设置webview的src为pageframe地址 - 加载完成后,设置其src为
pageframe.html, 新的src内容加载完成后再次触发onLoadCommit事件但根据条件不会执行reload方法。 pageframe.html页面在dom ready之后触发注入并执行具体页面相关的代码,此时通过history.pushState方法修改webview的src但是webview并不会发送页面请求。

pageframe.html模板生成的内容除小程序基础库视图层的底层功能之外,还包括小程序所有页面的模板信息、配置信息以及样式内容,这些在上方讲解的wxappcode.js文件中已经了解过了。
那么,既然每个视图层页面由pageframe模板生成,那么小程序每个页面独有的页面内容如dom和样式等如何生成呢,这主要是利用nw.js的executeScript方法来执行一段js脚本来注入只与当前页面相关的代码,包括当前页面的配置,注入当前页的css以及当前页面的virtual dom的生成.
最终生成的js代码(拿pages/index/index为例)如下图:

其中:
history.pushState('','', 'http://127.0.0.1:63444/__pageframe__/pages/index/index') 这句代码的作用修改当前webview的src,因为视图层的webview的src为pageframe.html,通过这句代码将其变更为具体的页面地址。
另外,需要注意的是nw.js的executeScript方法注入的代码是需要时机的,需要等到视图层的初始化工作准备ready之后才行,那么这个时机如何知道呢?细心的读者可能发现,在pageframe模板的最后一个script的内容:

这个从字面意思可以看出此时应该是页面dom ready的一个时机,通过alert来进行通知。
alert能通知消息?
当然可以的,在nw.js的webview中alert、prompt对应的弹框是被会阻止的,那么通过为webview绑定dialog事件来知道是那种弹框类型,以及提示内容.恍然大悟了没有?
如果你尝试直接打开在webview层的src地址,你会发现只有一个弹窗出现,你会很疑惑。

这样方法loadPage就会触发nw注入并执行页面相关的代码。最终生成的页面视图对应dom结构如下图:

可以看出,视图页面生成的dom结构中,document.body已无pageframe.html模板中对应body中的script内容,这是因为视图层的WAWebview.js在通过virtual dom生成真实dom过程中,它会挂载到页面的document.body上,覆盖掉pageframe.html模板中对应document.body的内容。
这样的话就完成了一套快速渲染过程。
