详细揭秘⼩程序框架技术——Mpx
与⽬前业内的⼏个⼩程序框架相⽐较⽽⾔,mpx 开发设计的出发点就是基于原⽣的⼩程序去做功能增强。所以从开发框架的⾓度来说,是没有任何“包袱”,围绕着原⽣⼩程序这个 core 去做不同功能的 patch ⼯作,使得开发⼩程序的体验更好。
于是我挑了⼀些我⾮常感兴趣的点去学习了下 mpx 在相关功能上的设计与实现。
编译环节
动态⼊⼝编译
不同于 web 规范,我们都知道⼩程序每个 page/component 需要被最终在 webview 上渲染出来的内容是需要包含这⼏个独⽴的⽂件的:js/json/wxml/wxss。为了提升⼩程序的开发体验,mpx 参考 vue 的 S
FC(single file component)的设计思路,采⽤单⽂件的代码组织⽅式进⾏开发。既然采⽤这种⽅式去组织代码的话,那么模板、逻辑代码、json配置⽂件、style样式等都放到了同⼀个⽂件当中。那么 mpx 需要做的⼀个⼯作就是如何将 SFC 在代码编译后拆分为 js/json/wxml/wxss 以满⾜⼩程序技术规范。熟悉 vue ⽣态的同学都知道,vue-loader ⾥⾯就做了这样⼀个编译转化⼯作。具体有关 vue-loader 的⼯作流程可以参见我写的⽂章。
这⾥会遇到这样⼀个问题,就是在 vue 当中,如果你要引⼊⼀个页⾯/组件的话,直接通过import语法去引⼊对应的 vue ⽂件即可。但是在⼩程序的标准规范⾥⾯,它有⾃⼰⼀套组件系统,即如果你在某个页⾯/组件⾥⾯想要使⽤另外⼀个组件,那么需要在你的 json 配置⽂件当中去声明usingComponents这个字段,对应的值为这个组件的路径。
在 vue ⾥⾯ import ⼀个 vue ⽂件,那么这个⽂件会被当做⼀个 dependency 去加⼊到 webpack 的编译流程当中。但是 mpx 是保持⼩程序原有的功能,去进⾏功能的增强。因此⼀个 mpx ⽂件当中如果需要引⼊其他页⾯/组件,那么就是遵照⼩程序的组件规范需要
在usingComponents定义好组件名:路径即可,mpx 提供的 webpack 插件来完成确定依赖关系,同时将被引⼊的页⾯/组件加⼊到编译构建的环节当中。代码转换
接下来就来看下具体的实现,mpx webpack-plugin 暴露出来的插件上也提供了静态⽅法去使⽤ loader。
这个 loader 的作⽤和 vue-loader 的作⽤类似,⾸先就是拿到 mpx 原始的⽂件后转化⼀个 js ⽂本的⽂件。例如⼀个 list.mpx ⽂件⾥⾯有关 json 的配置会被编译为:
require("!!../../node_modules/@mpxjs/webpack-plugin/lib/extractor?type=json&index=0!../../node_modules/@mpxjs/webpack-
plugin/lib/json-compiler/index?root=!../../node_modules/@mpxjs/webpack-plugin/lib/selector?type=json&index=0!./list.mpx")复制代码
这样可以清楚的看到 list.mpx 这个⽂件⾸先 selector(抽离list.mpx当中有关 json 的配置,并传⼊到 json-compiler 当中) --->>> json-compiler(对 json 配置进⾏处理,添加动态⼊⼝等) --->>> extractor(利⽤ child compiler 单独⽣成 json 配置⽂件)
其中动态添加⼊⼝的处理流程是在 json-compiler 当中去完成的。例如在你的 page/home.mpx ⽂件当中的json配置中使⽤了局部组
件 components/list.mpx:
这⾥需要解释说明下有关 webpack 提供的 SingleEntryPlugin 插件。这个插件是 webpack 提供的⼀个内置插件,当这个插件被挂载到webpack 的编译流程的过程中是,会绑定compiler.hooks.make.tapAsync hook,当这个 hook 触发后会调⽤这个插件上
的 ateDependency 静态⽅法去创建⼀个⼊⼝依赖,然后调⽤compilation.addEntry将这个依赖加⼊到编译的流程当中,这个是单⼊⼝⽂件的编译流程的最开始的⼀个步骤。
Mpx 正是利⽤了 webpack 提供的这样⼀种能⼒,在遵照⼩程序的⾃定义组件的规范的前提下,解析 mpx json 配置⽂件的过程中,⼿动的调⽤ SingleEntryPlugin 相关的⽅法去完成动态⼊⼝的添加⼯作。这样也就串联起了所有的 mpx ⽂件的编译⼯作。
Render Function
Render Function 这块的内容我觉得是 Mpx 设计上的⼀⼤亮点内容。Mpx 引⼊ Render Function 主要解决的问题是性能优化⽅向相关的,因为⼩程序的架构设计,逻辑层和渲染层是2个独⽴的。
这⾥直接引⽤ Mpx 有关 Render Function 对于性能优化相关开发⼯作的描述:
作为⼀个接管了⼩程序setData的数据响应开发框架,我们⾼度重视Mpx的渲染性能,通过⼩程序官⽅
⽂档中提到的性能优化建议可以得知,setData对于⼩程序性能来说是重中之重,setData优化的⽅向主要有两个:
将组件的静态模板编译为可执⾏的render函数,通过render函数收集模板数据依赖,只有当render函数中的依赖数据发⽣变化时才会触发⼩程序组件的setData,同时通过⼀个异步队列确保⼀个tick中最多只会进⾏⼀次setData,这个机制和Vue中的render机制⾮常类似,⼤⼤降低了setData的调⽤频次;
将模板编译render函数的过程中,我们还记录输出了模板中使⽤的数据路径,在每次需要setData时会根据这些数据路径与上⼀次的数据进⾏diff,仅将发⽣变化的数据通过数据路径的⽅式进⾏setData,这样确保了每次setData传输的数据量最低,同时避免了不必要的setData操作,进⼀步降低了setData的频次。
接下来我们看下 Mpx 是如何实现 Render Function 的。这⾥我们从⼀个简单的 demo 来说起:
.mpx ⽂件经过 loader 编译转换的过程中。对于 template 模块的处理和 vue 类似,⾸先将 template 转化为 AST,然后再将 AST 转化为 code 的过程中做相关转化的⼯作,最终得到我们需要的 template 模板代码。
packages/webpack-plugin/lib/template-compiler.js模板处理 loader 当中:
在render⽅法内部,创建renderData局部变量,调⽤Node(ast)⽅法完成 Render Function 核⼼代码的⽣成⼯作,最终将这个 renderData 返回。例如在上⾯给出来的 demo 实例当中,通过Node(ast)⽅法最终⽣成的代码为:
mpx ⽂件当中的 template 模块被初步处理成上⾯的代码后,可以看到这是⼀段可执⾏的js代码。那么这段 js 代码到底是⽤作何处呢?可以看到Node⽅法是被包裹⾄bindThis⽅法当中的。即
这段 js 代码还会被bindThis⽅法做进⼀步的处理。打开 bind-this.js ⽂件可以看到内部的实现其实就是⼀个 babel 的 transform plugin。在处理上⾯这段 js 代码的 AST 的过程中,通过这个插件对 js 代码做进⼀步的处理。最终这段 js 代码处理后的结果是:
bindThis ⽅法对于 js 代码的转化规则就是:
⼀个变量的访问形式,改造成 的形式;
对象属性的访问形式,改造成 this.__get(object, property) 的形式(this.__get⽅法为运⾏时 mpx runtime 提供的⽅法)
这⾥的 this 为 mpx 构造的⼀个代理对象,在你业务代码当中调⽤ createComponent/createPage ⽅法传⼊的配置项,例如 data,都会通过这个代理对象转化为响应式的数据。
需要注意的是不管哪种数据形式的改造,最终需要达到的效果就是确保在 Render Function 执⾏的过程当中,这些被模板使⽤到的数据能被正常的访问到,在访问的阶段中,这些被访问到的数据即被加⼊到 mpx 构建的整个响应式的系统当中。
只要在 template 当中使⽤到的 data 数据(包括衍⽣的 computed 数据),最终都会被 renderData 所记录,⽽记录的数据形式是例如:
renderData['xxx'] = [, 'xxx'] // 数组的形式,第⼀项为这个数据实际的值,第⼆项为这个数据的 firstKey(主要⽤以数据 diff 的⼯作)复制代码
以上就是 mpx ⽣成 Render Function 的整个过程。总结下 Render Function 所做的⼯作:
执⾏ render 函数,将渲染模板使⽤到的数据加⼊到响应式的系统当中;
返回 renderData ⽤以接下来的数据 diff 以及调⽤⼩程序的 setData ⽅法来完成视图的更新
Wxs Module
Wxs 是⼩程序⾃⼰推出的⼀套脚本语⾔。给出的⽰例,wxs 模块必须要声明式的被 wxml 引⽤。和 js 在 jsCore 当中去运⾏不同的是 wxs 是在渲染线程当中去运⾏的。因此 wxs 的执⾏便少了⼀次从 jsCore 执⾏的线程和渲染线程的通讯,从这个⾓度来说是对代码执⾏效率和性能上的⽐较⼤的⼀个优化⼿段。
有关官⽅提到的有关 wxs 的运⾏效率的问题还有待论证:
“在 android 设备中,⼩程序⾥的 wxs 与 js 运⾏效率⽆差异,⽽在 ios 设备中,⼩程序⾥的 wxs 会⽐ js 快 2~20倍。”
因为mpx 是对⼩程序做渐进增强,因此 wxs 的使⽤⽅式和原⽣的⼩程序保持⼀致。在你的.mpx⽂件当中的 template block 内通过路径直接去引⼊ wxs 模块即可使⽤:
在template模块经过template-compiler 处理的过程中。模板编译器 compiler 在解析模板的 AST 过程中会针对 wxs 标签缓存⼀份 wxs 模块的映射表:
当 compiler 对 template 模板解析完后,template-compiler 接下来就开始处理 wxs 模块相关的内容:
template/script/style/json 模块单⽂件的⽣成:
不同于 Vue 借助 webpack 是将 Vue 单⽂件最终打包成单独的 js chunk ⽂件。⽽⼩程序的规范是每个页⾯/组件需要对应的
wxml/js/wxss/json 4个⽂件。因为 mpx 使⽤单⽂件的⽅式去组织代码,所以在编译环节所需要做的⼯作之⼀就是将 mpx 单⽂件当中不同 block 的内容拆解到对应⽂件类型当中。在动态⼊⼝编译的⼩节⾥⾯我们了解到 mpx 会分析每个 mpx ⽂件的引⽤依赖,从⽽去给这个⽂件创建⼀个 entry 依赖(SingleEntryPlugin)并加⼊到 webpack 的编译流程当中。我们还是继续看下 mpx loader 对于 mpx 单⽂件初步编译转化后的内容:
接下来可以看下 styles/json/template 这3个 block 的处理流程是什么样。
⾸先来看下 json block 的处理流程:list.mpx -> json-compiler -> extractor。第⼀个阶段 list.mpx ⽂件经由 json-compiler 的处理流程在前⾯的章节已经讲过,主要就是分析依赖增加动态⼊⼝的编译过程。当所有的依赖分析完后,调⽤ json-compiler loader 的异步回调函数:
这⾥我们可以看到经由 json-compiler 处理后,通过nativeCallback⽅法传⼊下⼀个 loader 的⽂本内容形如:
即这段⽂本内容会传递到下⼀个 loader 内部进⾏处理,即 extractor。接下来我们来看下 extractor ⾥
⾯主要是实现了哪些功能:
稍微总结下上⾯的处理流程:
1. 构建⼀个以当前模块路径及 content-loader 的 resource 路径
2. 以这个 resource 路径作为⼊⼝模块,创建⼀个 childCompiler
3. childCompiler 启动后,创建 loaderContext 的过程中,将 content ⽂本内容挂载⾄ loaderContext.mpx 上,这样在 content-
loader 在处理⼊⼝模块的时候仅仅就是取出这个 content ⽂本内容并返回。实际上这个⼊⼝模块经过 loader 的过程不会做任何的处理⼯作,仅仅是将⽗ compilation 传⼊的 content 返回出去。
4. loader 处理模块的环节结束后,进⼊到 module.build 阶段,这个阶段对 content 内容没有太多的处理
5. createAssets 阶段,输出 chunk。
6. 将输出的 chunk 构建为⼀个原⽣的 node.js 模块并执⾏,获取从这个 chunk 导出的内容。也就是模块通过ports导出的内
容。
所以上⾯的⽰例 demo 最终会输出⼀个 json ⽂件,⾥⾯包含的内容即为:
{"usingComponents": {"list": "/components/list397512ea/list" }}复制代码
运⾏时环节
以上⼏个章节主要是分析了⼏个 Mpx 在编译构建环节所做的⼯作。接下来我们来看下 Mpx 在运⾏时环节做了哪些⼯作。
响应式系统
⼩程序也是通过数据去驱动视图的渲染,需要⼿动的调⽤setData去完成这样⼀个动作。同时⼩程序的视图层也提供了⽤户交互的响应事件系统,在 js 代码中可以去注册相关的事件回调并在回调中去更改相关数据的值。Mpx 使⽤ Mobx 作为响应式数据⼯具并引⼊到⼩程序当中,使得⼩程序也有⼀套完成的响应式的系统,让⼩程序的开发有了更好的体验。
还是从组件的⾓度开始分析 mpx 的整个响应式的系统。每次通过createComponent⽅法去创建⼀个新的组件,这个⽅法将原⽣的⼩程序创造组件的⽅法Component做了⼀层代理,例如在 attched 的⽣命周期钩⼦函数内部会注⼊⼀个 mixin:
在这个⽅法内部⾸先调⽤transformApiForProxy⽅法对组件实例上下⽂this做⼀层代理⼯作,在 context 上下⽂上去重置⼩程序的 setData ⽅法,同时拓展 context 相关的属性内容:
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论