前言
Taro Next 建筑的秘密揭晓,今天的晨读文章由京东@凹凸实验室 授权分享。
正文从这里开始~~
随着小程序开发的火爆,小程序开发框架层出不穷,但目前每个框架都绑定了专用的 DSL,比如一个类或者一个 Vue 类,在框架内部,开发者无法根据团队的技术栈自由选择 DSL,也无法共享框架自身的生态和工具。
本次分享将介绍Taro如何在小程序上运行各种语法的前端框架(如:Vue/Java等),并探讨一个框架支持多种DSL的实现探索,让开发者可以使用任何流行的框架/语法/DSL来编写小程序应用,并复用相关的生态系统。
小程序发展史
2017年1月9日凌晨,万众期待的微信小程序正式上线。
此前,京东投入了一个小型的前端团队,经过一个月的封闭开发,以每周一个版本的速度迭代,终于以第一时间发布了自己的“京东购物”小程序。虽然功能和界面现在看起来有些简单,但在当时,它完全符合微信小程序“触手可及,用着就走”的理念。
当时微信小程序开发存在一些不足,比如依赖管理混乱、工程流程落后、ES Next 支持不完善、命名标准不一致等等。这些问题现在看来都有各种官方或者非官方的解决方案,但在当时小程序开发探索阶段,这些问题都是一些痛点。
有一句话是我个人很喜欢的,就是“当一种语言的能力不足,而用户的操作环境又不支持其他选择时,该语言就会成为一种“编译目标”语言”。
纵观前端的发展史,无论是CSS预处理器的火爆,还是各类模板的风靡,甚至是的诞生,都印证了这一说法,微信小程序也不例外。因此,各类小程序开发框架层出不穷。
这些小程序开发框架最主要的区别就是DSL,从logo的颜色就可以看出来,除了滴滴自定义的DSL,其他的绿色logo都遵循Vue的语法(比如),蓝色logo都遵循语法(比如Taro)。
微信小程序之后,各大厂商纷纷发布自己的小程序平台,如支付宝、百度、今日头条、QQ等,还有快应用、网易、360、京东等。小程序赛道越来越拥挤,开发者需要适配的小程序平台也越来越多,因此各种大小程序开发框架也纷纷适配多终端。
因此从这个时间点回溯整个小程序开发框架的进展,会发现整个2018年乃至2019年初,小程序开发框架的主要差异和侧重点为:DSL和多终端适配。
太郎的由来与初衷
俗话说“业务孵化技术,技术服务业务”,Taro的诞生源于业务需求的增加。当时我们团队需要负责:京东购物等,团队的人力资源捉襟见肘。同时,以上业务都或多或少存在多端需求,如微信小程序、H5、(京东主流APP基本都内置了渲染引擎),而且可以预见未来还会有更多小程序平台需要适配,为每个端开发一套代码不现实,这样会导致:研发成本上升、代码维护困难。
当时我们团队开发了一个类框架: ,整个团队的技术栈全部转向了它,当时市面上还没有遵循该语法的小程序框架,所以我们开发了Taro,希望能够使用该语法编写小程序,同时通过“once Run”实现跨终端。
Taro 整个框架自 2018 年 6 月 7 日开源以来,一直保持着高速的迭代,这些迭代主要集中在三个方面:
经过团队一年多的努力,Taro 得到了社区的广泛认可,截止 2019 年 12 月 18 日,Taro 拥有 250 名用户,社区积极提交的开发案例超过 150 个:taro-user-,其中包括众多多端案例。
不过尽管如此,Taro 还是存在一些无法解决的问题,或者说不是那么容易解决的问题。比如与 DSL 强绑定、JSX 适配工作量大、社区贡献复杂等。归根结底,这些问题很大一部分是 Taro 的架构问题。
因此我们团队一直在等待一个合适的机会来升级整个架构,以及修复一些项目快速迭代所欠下的技术债。
最重要的是,简单的项目维护和迭代已经不能满足我们团队躁动的心,我们渴望借此机会实现技术突破。
小程序跨框架开发探索
在讲Taro的架构之前,我们先来回顾一下小程序的架构。
微信小程序主要分为逻辑层和视图层,以及它们下面的原生部分。逻辑层主要负责 JS 操作,视图层主要负责页面渲染。它们主要进行 Data 通信和调用原生 API。这也是大多数小程序的架构,以微信小程序为首。
由于原生部分对于前端开发者来说就像一个黑盒,所以整个架构图中原生部分可以省略。同时我们也精简了逻辑层和视图层,最终可以得到一个极简版的小程序架构图:
也就是说,只需要在逻辑层调用相应的 App()/Page() 方法,在方法中处理数据、提供生命周期/事件函数等,在视图层提供相应的模板和样式进行渲染,即可运行小程序。这也是大部分小程序开发框架重点关注和处理的部分。
Taro 当前建筑
Taro 目前的架构主要分为:编译时、运行时。
编译进程主要将Taro代码转换成小程序代码,如JS、WXML、WXSS、JSON等;运行进程主要对生命周期、事件、数据进行处理和衔接。
Taro 编译时间
有过插件开发经验的朋友应该对下面的流程非常熟悉,Taro 在编译时也遵循这样的流程,使用 - 将 Taro 代码解析为抽象语法树,再使用 - 对抽象语法树进行一系列的修改和转换操作,最后使用 - 生成对应的目标代码。
详情请参阅:-
整个编译过程中最复杂的部分是 JSX 编译。
我们都知道 JSX 是一种语法扩展,其写法千变万化,非常灵活。这里我们采用穷举法,将所有可能的 JSX 写法一一适配,这部分工作量非常大。其实 Taro 对各种 JSX 写法的支持还是挺多的。
但尽管如此,我们也无法完全覆盖所有的情况,所以我们还是建议大家按照官方的规范来编写代码。同时我们也提供了各种插件来辅助大家编写规范的代码。
我们团队内部一直流传着这样一个笑话:如果你觉得用Taro开发的时候bug很少,那说明你的代码写的很规范。
Taro 运行时
接下来我们对比编译后的代码,发现 的核心方法已经消失了。同时,代码中又添加了 和 ,这两个是 Taro 运行时的核心。
// 编译前
import Taro, { Component } from '@tarojs/taro'
import { View, Text } from '@tarojs/components'
import './index.scss'
export default class Index extends Component {
config = {
navigationBarTitleText: '首页'
}
componentDidMount () { }
render () {
return (
<View className=‘index' onClick={this.onClick}>
Hello world!
)
}
}
// 编译后
import {BaseComponent, createComponent} from '@tarojs/taro-weapp'
class Index extends BaseComponent {
// ...
_createDate(){
//process state and props
}
}
export default createComponent(Index)
大致的UML图如下,主要对一些核心方法进行了替换和重写:,等。结合编译后方法的替换,不难猜测Taro现在的架构只是在开发时遵循了语法,与编译后代码真正运行时没有任何关系。
主要功能是调用()构建页面;连接事件,生命周期等;执行Diff Data并调用方法更新数据。
总结
因此,Taro 目前的架构特点是:
其他解决方案的架构
小程序开发框架有很多,我们也从社区中获得了很多启发。

接下来我们看一下遵循Vue语法的小程序开发框架的代表:它是如何实现的。
看过 Vue 源码的朋友一定对上面的文件夹和架构很熟悉,本质上它是 /vue@2.4.1 代码的一个 fork,保留了 Vue 的能力,并且增加了对小程序平台的支持。
在源码中具体的表现为:在Vue源码文件夹下增加了一个mp目录,在这个目录中实现了(编译时)和(运行时)支持。
实现也分为:编译时和运行时。
编译时
编译时做的事情和Taro很相似,将Vue SFC代码编译成小的程序代码文件(JS、WXML、WXSS、JSON)。
最大的区别在于,Taro 将 JSX 编译进小程序模板,而 Vue 模板则编译进小程序模板。不过由于 Vue 模板与小程序模板比较类似,所以这部分工作量比 Taro 要小很多。
运行
的运行时和Vue的运行时强相关,首先我们来了解一下Vue的运行时。
单个 .vue 文件由三部分组成: 、 、 。
橙色路径部分在编译过程中会被vue-中的ast解析,最终生成一个函数,函数的执行会生成一个虚拟DOM树,虚拟DOM树是对真实DOM树的抽象,树中的节点称为。
Vue 获取到虚拟 DOM 树之后,可以和旧的虚拟 DOM 树做 diff 对比,对比完成后 Vue 会使用真实的 DOM 操作方法(如 等)来操作 DOM 节点,更新视图。
同时绿色路径部分在 Vue 实例化时会响应式地处理数据,当检测到数据发生变化时会调用函数生成最新的虚拟 DOM 树,然后与旧的虚拟 DOM 树进行比较,找到修改成本最小的节点进行修改。
在运行时,首先会清除阶段的DOM操作相关方法,即什么都不做。其次,在创建Vue实例时,会秘密调用Page()生成小程序的页面实例。然后运行过程中会直接调用$()方法,该方法会获取页面实例上维护的数据,然后通过该方法更新到视图层。
整体示意图如下:
一些总结和思考
因此,与 Taro 更注重编译时而较少关注运行时不同,可以认为是:一半编译时,一半运行时。从代码大小的对比中也可以大致反映出来。
的 WXML 模板与 Taro 相同,也是通过代码编译获得;与 Taro 独立于运行时不同,其本质是将 Vue 运行在小程序中,并实现了 Vue@2.4.1 的大部分特性(仅有少数特性由于小程序模板限制未实现,如:slot、v-html);整个框架基于相对完整的工程化实现。
其他小程序框架的实现原理和效果的差异也给了我们一些思考:
编译时还是运行时:Taro 选择重新编译的主要原因是出于性能方面的考虑,毕竟在同等条件下,编译时做的工作越多,意味着运行时做的工作越少,性能就越好。另外重新编译也保证了 Taro 代码编译后的可读性。但从长远来看,计算机硬件的性能越来越冗余,如果牺牲一点可以忍受的性能,换取更大的灵活性和整个框架更好的适配性,我们认为还是值得的。
模板静态编译OR动态构建: 虽然Taro和的模板都是通过静态编译生成的,但是社区中也有很多动态构建的例子,比如:。
DSL的限制:我们能否实现一个小程序开发框架,摆脱DSL的限制?
新架构 Taro Next 的适配和实现
这次,我们站在浏览器的角度去思考前端的本质:无论使用什么框架开发,不管是不是Vue,最终的代码运行之后都会调用浏览器的BOM/DOM API,比如:,,等等。
因此我们创建了 taro- 包,并在该包中实现了高效、精简的 DOM/BOM API(下面的 UML 图仅体现了几个主要类的结构和关系):
然后我们通过插件的方式注入到小程序的逻辑层中。
这样,小程序在运行时就有一套高效、精简的DOM/BOM API。
完成
注入 DOM/BOM 之后,理论上 Nerv/ 就能直接运行了。但是稍微特殊一点的是,DOM 包含了很多浏览器兼容代码,导致包太大,而且我们不需要这部分代码,所以需要做一些定制和优化。
在 16+ 版本中,架构如下:
最上面一层是核心部分-core,中间一层是-,职责是维护树,内部实现了Diff/算法,决定什么时候更新,更新什么。
它负责具体平台的渲染,提供宿主组件,处理事件等。例如DOM是一个渲染器,负责渲染DOM节点,处理DOM事件。
因此我们实现了 taro- 来连接 taro- 和 taro- 的 BOM/DOM API:
具体实现主要分为两步:
经过上述步骤,代码在小程序运行时其实已经可以正常运行了,并且会生成一棵 Taro DOM Tree。那么如何将这么大的 Taro DOM Tree 更新到页面中呢?
首先,我们将小程序所有组件逐一模板化,得到小程序组件对应的模板,下图是模板处理后的小程序视图组件:
然后,我们将:根据组件动态地“递归”渲染整个树。
具体过程是先遍历 Taro DOM Tree 根节点的子元素,然后根据每个子元素的类型选择对应的模板来渲染该子元素。接着,在每个模板中,我们都会遍历当前元素的子元素,以此递归的方式遍历整个节点树。
整个Taro Next实现流程图如下:
Vue 实现
虽然在开发的时候它们和Vue有很大的区别,但是实际上在实现了BOM/DOM API之后,它们之间的区别就很小了。
Vue 和 最大的区别在于运行时方法,它会进行一些运行时的处理,比如生命周期对齐。
其他部分如构建和修改DOM Tree、通过BOM/DOM方法的渲染原理等与一致。
完成
说到它就不得不提 Web。Web 是在标准浏览器 API 之上实现的核心绘制层,本质上最终调用的是 BOM/DOM API。因此理论上也可以适配,但我们不会在这个方面投入太多精力,最终还是会像快应用一样,交给社区去实现和维护。