前言
本章内容分解:
- 小程序框架都有哪些
- 框架之间的原理有什么不同
- 具体实现方式是怎样的
第三方框架
为什么会出现第三方框架呢,换一种方式来说的话就是小程序在基于当前的情况下还有什么弊端?
这一章我们就来聊一聊这个方面。
首先webpack
工程化小程序就不支持。还有预编译也是不可以的,原来我们开发的时候可以使用less
、sass
写起来很方便,规避一些问题,做一些嵌套、类的选择,集中处理一些问题。
如果直接使用小程序规则开发,对于开发效率方面会有一些影响,也不方便定制工程化的流程。会有一些回归到最原始的html+css+js
的开发模式年代的感觉。
为了解决上面的问题,出现了很多的小程序第三方框架。这些第三方框架基本上都是围绕着两种语言来的,vue & react
。或者说类vue
、类react
。
那么我们就来聊一下框架。
三种框架
预编译
什么是预编译的框架呢?还记得我们讲解WXSS
的时候,WXSS
的文件会编译成js再执行。像这种执行前就进行编译的手段就叫做预编译。这种框架就是预编译框架。wepy
、taro
就是这样的框架。
我们先用taro
的代码来举一个例子。
代码上我添加了一些备注,预编译框架正是分开编译了这三个部分,在执行之前就将小程序需要的文件编译出来,比如return
内容编译为WXML
,less
编译为WXSS
,生命周期及方法等编译为js
。
预编译框架自身定义了一套DSL(语法规则)
,这里taro的语法规则就是类似react的语法规则,也一直是taro
主打的优势对吧。然后DSL解析编译为抽象语法树AST进行词法分析和语法分析。最后还原为小程序的代码。
可以看出预编译框架的核心思想就是DSL
+ 语法解析。刚看了一下taro最新的文档,已经支持react hooks
这样的写法了。
如果taro
支持react hooks
类似的写法的话,那么taro
团队一定需要写一套关于hooks的语法解析,把hooks的逻辑转换为小程序的js逻辑。换句话说,taro已经把react的语法解析写的差不多了。现在还支持了vue
及Nerv
版本。
可以看的出来的是上面的截图中有一句话:
本篇文档只会介绍在 Taro 中可用的 Hooks API 和部分与 React 不一致的行为,其它内容大体的内容和 Hooks Reference 相同。
与React有不一致是很正常的事情,自己写语法解析终究会有一些环境掣肘,毕竟是两个平台之间的兼容,我认为主要是小程序平台的规则较为简陋导致解析不过来。
不过taro自身也推出了一些类似hooks的api可以使用。这样的话就可以弥补一些与react hooks
不一致的情况,业务场景覆盖还是很全的。
我们再换成wepy来看一下。wepy也是预编译框架,看一下wepy的模版是怎么样的。
可以看到模版中的语法与vue模版语法相似,只不过有些许地方不一样,比如page的声明,template
中的结构,小程序独有的config
模块。
与taro一致,只不过wepy
定制的是类似vue语法结构的DSL
,模版中的四个模块分别会编译为小程序的四个文件。style模块编译为WXSS,template模块编译为WXML,script模块编译为js,config模块编译为小程序配置json。
小程序预编译框架的原理就如上述讲解,我们可以想象一下预编译框架的坏处有什么呢?
- react或者vue后期再出一些新特性的话,预编译框架都需要在进行语法解析扩展编写。
- 兼容问题,比如小程序不支持的一些属性,如果不支持,预编译框架要进行兼容。
因为如上因素,半编译半运行
框架随之推出。
半编译半运行框架
半编译半运行框架有什么呢,美团开发的mpvue
。
虽然还没讲到运行时框架,但是要了解到是,基本上运行时的框架都是基于vue的框架才可以达到运行时的目的。可以看一下mpvue
的Github
简介
这个项目就是直接从vue
项目fork
过来修改的。那么它改了什么东西呢,我们接着往后看。
首先看一下vue的渲染框架:
最后的node就是web端渲染真实node节点了,达到页面更新渲染的目的。
我们可以想象一下一个问题,如果想让vue运行在小程序中,需要做哪些工作?
通过前面章节的讲解我们可知微信小程序的视图渲染与逻辑是分开的,逻辑层通过setData
更新视图渲染。
那么就可以修改patch
流程不直接生成真实node
,而是触发setData
来更新视图层。
可以想象一下,如果把vue
的template
编译成WXML
就变为了小程序的视图层。vue
本身预编译的代码为js
,这个js是可以在逻辑层中运行由于js-core
,然后当数据变动的时候走vue的渲染流程,patch
流程改为setData
来触发视图层更新。这样的话是完全没问题的。
所以为什么这个框架的名称叫半编译半运行框架,半编译讲的是vue
的template
需要单独编译为wxml
,半运行讲的是vue
整体的特性都会在逻辑层中运行。为了符合小程序的渲染框架,修改了vue的框架,最终达到了这个目的。
看一下mpvue的官网介绍。
可以看到介绍中提到的compiler
实现就是讲vue中的template
抽离并且编译为wxml
的模块。mpvue
的源码中可以看到具体实现。
备注都是中文还是很友好的。
另一处修改就是vue的runtime
下的patch
模块。
可以看到触发了一个this.$updateDataToMP()
方法。这个方法在runtime/render.js
中。
在this.$updateDataToMP()
方法中就进行了setData
的一个调用。还可以看到有另一个方法为initDataToMP
初始化方法。
在初始化中setData
中data是全部的,然而在updateDataToMP
阶段可以看到做了一个diffData
数据的比对,有修改的data
才会触发setData。
并且随后的是throttle
函数,减少setData
的次数。优化双线程通讯的性能。在函数上方备注的也很清楚。
这两点也是印证了上面我们所阐述的,核心修改就是这两处,当然还需要一些额外的工作才可以,比如说生命周期的对称、监听等等,都是修改runtime下的文件。
大家可以看一下mpvue
的源码,并且备注十分齐全。
运行时框架
可以借鉴一下半编译半运行时框架的原理思考,如果要把半运行时框架变成为运行时框架需要做什么?
首先为什么mpvue
需要compiler
模块将vue
中的template
编译为WXML
?为什么不直接生成真实node
。
我们需要缕一下思路。
首先vue
或者react
如果生成真实node
后,需要插入到HTML
中去,怎么插入呢,一般是通过操作DOM
的api比如innerHTML
类似。
在小程序双线程架构中,渲染层是没有开放任何操作DOM的api给逻辑层的。逻辑层是没有办法通过操作DOM来改变视图的。所以我们看到了半编译半运行时框架通过半编译,把vue的template
模版提前编译为wxml,然后通过setData把data数据传输过去。
然而纯运行时框架就是要解决这个半编译的问题。
届时有几个问题需要解决:
- 通讯方式只能通过setData到渲染层
- vue、react最终需要操作DOM
看似无解的两个问题。如果vue运行时在逻辑层,那么逻辑层终究没有操作DOM的api。
首先需要解决的问题就是动态地渲染DOM问题。
针对这块的话有一个契机,小程序的template
模版机制。我们先看一下什么是小程序的template
模版。
小程序模版template
WXML提供模板(template),可以在模板中定义代码片段,然后在不同的地方调用。
定义模板:
使用 name 属性,作为模板的名字。然后在<template/>
内定义代码片段,如:
<!--
index: int
msg: string
time: string
-->
<template name="msgItem">
<view>
<text> {{index}}: {{msg}} </text>
<text> Time: {{time}} </text>
</view>
</template>
使用模板:
<template is="msgItem" data="{{...item}}"/>
Page({
data: {
item: {
index: 0,
msg: 'this is a template',
time: '2016-09-15'
}
}
})
is 属性可以使用Mustache
语法(一个logic-less(轻逻辑)模板解析引擎,下方例子中的{{}}
就是Mustache
语法),来动态决定具体需要渲染哪个模板:
<template name="odd">
<view> odd </view>
</template>
<template name="even">
<view> even </view>
</template>
<block wx:for="{{[1, 2, 3, 4, 5]}}">
<template is="{{item % 2 == 0 ? 'even' : 'odd'}}"/>
</block>
上面就是template
的一些基本使用方式,下面我们一步一步来实现一个动态的template
模板渲染结构。
动态template
首先我们新建一个空白的页面,在WXML
中添加一个基础的template
,其中渲染一个image
。
然后在js中添加响应data:
root是一个对象类型,有属性src。
这个时候,如果我们想在图片的下方添加一段文字。按照正常的数据结构是这样实现的:
js相应的响应数据中添加text字段。
此时的页面中:
渲染方面是没有问题的,但是这种数据结构的扩展性不是很好,我们希望每一种标记都可以自行搭配。
像如上的数据结构怎么动态搭配呢?首先肯定是要把text组件拆分出去,先拆分出去看一下template
的数据结构。
这里把text
标记单独拆分为template
,并且命名tpl-text
。然后替换引用位置。
这个时候有个问题,如果我们希望template
是可复用的,那么插入的位置最好不是固定的。比如这个时候有两个业务场景,第一个文字在图片的上方,第二个文字在图片的下方,这个时候就需要一个容器,用来控制template
结构的容器。
对应容器的分发,data数据结构也需要修改一下。
首先我们分析一下容器结构,首先tpl-container
中有一个block
标记用来循环children
结构。并且内部动态渲染template,内部渲染的template
的类型是通过data中的type决定的。并且相应的可以看到image标记与text
标记都已经独立拆分出去。
这里大家消化一下,接下来看一下data中的root
结构是不是有点眼熟。有一点像树的结构,如果容器支持children
自遍历的话那么root的数据结构就是一颗树,那么我们在此数据结构的基础上再套一层,使其变成两层children
结构,外层套一个view
。先看一下数据结构。
外层添加了一层view。相应的template这边也要做一下更改。首先声明template-view。
可以看到template-view
中又再次包含了tpl-container
,children
再次可以进行解析分发。这样就可以进行数据结构的递归遍历。
回到主题,上述演变过程解决了小程序通过数据结构控制XML的问题。还有第二个问题需要解决,就是vue
、react
最终需要操作DOM的问题,小程序中并没有操作DOM的api给逻辑层中的react用,那么就自己写一套即可。
举个例子,比如appendChild
这样的api,进行重写,最终效果为修改上述data数据结构。
最终思路就是复刻一套操作DOM的api,然后操作的并不是DOM,而是我们自己的数据结构,而后,我们自己的数据结构可以通过sandData发送到渲染层进行动态的模版渲染。这样的话就可以完成一套运行时的框架。
在这里看一下remax
完整的VNode
结构:
每个节点都是一个VNode
,声明了一些属性、方法,可以看到appendChild的节点方法。
appendChild源码中本质就是操作data树结构。
remax的源码传送门remax