GMTC | 《小程序跨框架开发的探索与实践》 演讲全文

image

前言:随着小程序开发的热度上升,小程序开发框架也层出不穷。但目前每一个框架都会绑定一个专属 DSL,如类 React 或者类 Vue,在一个框架内,开发者没法根据团队技术栈自由选择 DSL,同时也没法共享框架自己的生态与工具。

本次分享将为你们介绍 Taro 如何将各类语法的前端框架(React/Vue 等)运行在小程序上,讨论一个框架支持多 DSL 的实现探索,使得开发者可使用任意热门框架/语法/DSL 来编写小程序应用,同时复用相关生态。css

小程序开发的历程

2017 年 1 月 9 日凌晨,万众期待的微信小程序正式上线。html

在此以前,京东投入一个前端小团队,通过一个月的封闭式开发,以一周一个版本的速度进行迭代,终于在第一时间发布了本身的 「京东购物」 小程序,尽管功能和界面如今看起来有些简陋,但在当时是彻底符合微信小程序「触手可及,用完即走」的理念。前端

image

固然,随着整个项目的不断迭代,如今的 「京东购物」小程序在设计、交互以及功能复杂度已经全面向 APP 端看齐,这里面的工程化实践已经由 刘慧敏 老师在 GMTC 全球大前端技术大会(北京站)2019 进行过度享,有兴趣的能够下载 PPT:京东购物小程序工程化之路vue

当时的微信小程序的开发存在一些缺点,好比依赖管理混乱、工程化流程落后、ES Next 支持不完善、命名规范不统一等。这些问题在如今看来都已经有了各类官方或非官方的解决办法,可是在当时小程序开发的探索阶段,这些问题都是一些痛点问题。node

有句话我我的特别喜欢,那就是「当一门语言的能力不足,而用户的运行环境又不支持其它选择的时候,这门语言就会沦为 “编译目标” 语言」。react

纵观整个前端的历史,不管是 CSS 预处理器的大行其道、各类模版的流行,仍是 CoffeeScript 乃至 TypeScript 的诞生,都印证了这个说法,微信小程序这里也不例外。所以,各类小程序开发框架如百花齐放,层出不穷。webpack

image

这些小程序开发框架最主要的区别是 DSL,这点从 logo 颜色上就能够看出来,除了滴滴的 Chameleon 是自定义 DSL 外,其他的绿色的 logo 是遵循了 Vue 语法(如 mpvue ),蓝色的 logo 是遵循了 React 的语法(如 Taro)。git

在微信小程序以后,各大厂商纷纷发布了本身的小程序平台,好比:支付宝、百度、头条、QQ等,再加上快应用、网易、360、京东等,小程序的赛道愈来愈拥挤,开发人员须要适配的小程序平台愈来愈多,所以,各大小程序开发框架也纷纷进行了多端适配。github

image

所以,站在这个时间节点反过来回顾整个小程序开发框架的进程,你会发现整个 2018 年乃至 2019 年初,小程序的开发框架主要的区别和重心在于:DSL 以及 多端适配web

Taro 的起源与初心

正所谓「业务孵化技术,技术服务业务」,Taro 的诞生源自于业务需求的增长,当时咱们的团队须要同时负责:京东购物,TOPLIFE 等业。团队人力资源捉襟见肘,与此同时,以上的业务都或多或少存在多端的需求,好比 微信小程序、H五、React Native(京东的主流 APP基本都内置了 React Native 渲染引擎),并且能够预见的是,之后颇有可能须要适配更多的小程序平台,而每一个端开发一套代码又不现实,会致使:研发成本上升,代码维护困难

当时咱们团队自研了一款 类React 框架:Nervjs, 整个团队的技术栈所以所有转向了 React ,而当时市面上又没有一款遵循 React 语法的小程序框架,所以,咱们开发了 Taro,但愿可以使用 React 语法写小程序的同时,经过「Write once Run anywhere」来实现跨端的。

image

整个 Taro 框架从 2018 年 6 月 7 日开源至今,一致保持着高速迭代,这些迭代主要集中在三个方面:

image

通过团队 一年多的努力,Taro 获得了社区的普遍承认,截止 2019年 12 月 18日,Taro 已拥有 22254 Stars 和 250 名 Contributors,社区主动提交的开发案例 150+:taro-user-cases,其中不乏多端案例。

可是尽管如此,Taro 仍是存在一些问题没法解决,或者说:没那么好解决。好比:和 React DSL 强绑定、JSX 适配工做量大、社区贡献复杂等。这些问题归根到底,很大一部分是 Taro 的架构问题。

image

所以咱们团队也一直在等待一次合适的机会,对整个架构进行一次提高,同时修复一些项目快速迭代欠下的技术债。

最主要的是,单纯的项目维护迭代已经知足不了咱们团队躁动的心,咱们渴望借此机会进行一次技术突破。

小程序跨框架开发的探索

在讲 Taro 架构以前,咱们先来回顾一下小程序的架构。

微信小程序主要分为 逻辑层视图层,以及在他们之下的原生部分。逻辑层主要负责 JS 运行,视图层主要负责页面的渲染,它们之间主要经过 EventData 进行通讯,同时经过 JSBridge 调用原生的 API。这也是以微信小程序为首的大多数小程序的架构。

image

因为原生部分对于前端开发者来讲就像是一个黑盒,所以,整个架构图的原生部分能够省略。同时,咱们咱们对 逻辑层 和 视图层 也作一下简化,最后能够获得小程序架构图的极简版:

image

也就是说,只须要在逻辑层调用对应的 App()/Page() 方法,且在方法里面处理 data、提供生命周期/事件函数等,同时在视图层提供对应的模版及样式供渲染就能运行小程序了。这也是大多数小程序开发框架重点考虑和处理的部分。

Taro 当前架构

Taro 当前的架构主要分为:编译时运行时

其中编译时主要是将 Taro 代码经过 Babel 转换成 小程序的代码,如:JSWXMLWXSSJSON

运行时主要是进行一些:生命周期、事件、data 等部分的处理和对接。

image

Taro 编译时

有过 Babel 插件开发经验的应该对一下流程十分熟悉,Taro 的编译时也是遵循了此流程,使用 babel-parser 将 Taro 代码解析成抽象语法树,而后经过 babel-types 对抽象语法树进行一系列修改、转换操做,最后再经过 babel-generate 生成对应的目标代码。

详情能够参考:babel-handbook

image

整个编译时最复杂的部分在于 JSX 编译。

咱们都知道 JSX 是一个 JavaScript 的语法扩展,它的写法变幻无穷,十分灵活。这里咱们是采用 穷举 的方式对 JSX 可能的写法进行了一一适配,这一部分工做量很大,实际上 Taro 有大量的 Commit 都是为了更完善的支持 JSX 的各类写法

但尽管如此,咱们也不可能彻底覆盖全部的状况,所以仍是推荐你们按照官方规范书写 React 代码,同时,咱们也提供了丰富的 ESlint 插件来辅助你们书写规范的代码。

image

这一块咱们团队内部一直有个梗:若是你使用 Taro 开发感受 Bug 少,那说明你的 React 代码写得很规范

Taro 运行时

接下来,咱们能够对比一下编译后的代码,能够发现,编译后的代码中,React 的核心 render 方法 没有了。同时代码里增长了 BaseComponentcreateComponent ,它们是 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}>
        <Text>Hello world!</Text>
      </View>
    )
  }
}

// 编译后
import {BaseComponent, createComponent} from '@tarojs/taro-weapp'

class Index extends BaseComponent {

// ... 

  _createDate(){
    //process state and props
  }
}

export default createComponent(Index)

BaseComponent 大概的 UML 图以下,主要是对 React 的一些核心方法:setStateforceUpdate 等进行了替换和重写,结合前面编译后 render 方法被替换,你们不难猜出:Taro 当前架构只是在开发时遵循了 React 的语法,在代码编译以后实际运行时,和 React 并无关系

image

createComponent 主要做用是调用 Component() 构建页面;对接事件、生命周期等;进行 Diff Data 并调用 setData 方法更新数据。

总结

所以,整个 Taro 当前架构的特色是:

  • 重编译时,轻运行时:这从两边代码行数的对比就可见一斑。
  • 编译后代码与 React 无关:Taro 只是在开发时遵循了 React 的语法。
  • 直接使用 Babel 进行编译:这也致使当前 Taro 在工程化和插件方面的羸弱。

image

其它解决方案的架构

小程序开发框架百花齐放,咱们也从社区里获得了很多启发。

接下来咱们来看看 遵循 vue 语法的小程序开发框架的表明:mpvue 是怎样实现的。

image

看过 Vue 源码的同窗对上面的文件夹和架构确定熟悉,本质上,mpvue 就是 fork 了一份 vuejs/vue@2.4.1 的代码,保留了 Vue runtime 能力,同时添加了小程序平台的支持。

具体在源码中的表现就是:在 Vue 源码的 platforms 文件夹下面增长了 mp 目录,在里面实现了 complier(编译时)runtime (运行时)支持。

mpvue 的实现一样分为:编译时运行时

mpvue 编译时

其中编译时作的事情和 Taro 很相似:将 Vue SFC 写法的代码编译成 小程序代码文件(JS、WXML、WXSS、JSON)。

最大的区别是 Taro 将 JSX 编译成 小程序模版,而 mpvue 是将 Vue 模版编译成 小程序模版。可是因为 Vue 模版和 小程序模版的类似性,mpvue 在这一块的工做量比 Taro 少得多。

image

mpvue 运行时

而 mpvue 的运行时和 Vue 的运行时是强关联的,首先咱们来看看 Vue 的运行时。

一个 .vue 的单文件由三部分构成: template, script, style

橙色路径部分, template 会在编译的过程当中,在 vue-loader 中经过 ast 进行分析,最终生成一段 render 函数,执行 render 函数会生成虚拟dom树,虚拟 DOM 树是对真实 DOM 树的抽象,树中的节点被称做 vnode 。

Vue 拿到 虚拟 DOM 树以后,就能够去和上次老的 虚拟 DOM 树 作 patch diff 对比。patch 阶段以后,vue 就会使用真实的操做DOM 的方法(好比说 insertBefore , appendChild 之类的),去操做DOM结点,更新视图。

同时,绿色路径的部分,在实例化 Vue 的时候,会对数据 data 作响应式的处理,在监测到 data 发生改变时,会调用 render 函数,生成最新的虚拟 DOM 树, 接着对比老的虚拟 DOM 树进行 patch, 找出最小修改代价的 vnode 节点进行修改。

image

而 mpvue 的运行时,会首先将 patch 阶段的 DOM 操做相关方法置空,也就是什么都不作。其次,在建立 Vue 实例的同时,还会偷偷的调用 Page() 用于生成了小程序的 page 实例。而后 运行时的 patch 阶段会直接调用 $updateDataToMp() 方法,这个方法会获取挂在在 page 实例上维护的数据 ,而后经过 setData 方法更新到视图层。

mpvue 总体原理图也就以下:

image

一些总结与思考

所以,和 Taro 重编译时轻运行时不一样,mpvue 算是:半编译时,半运行时。这点从代码量的对比也能大体反映出来。

mpvue 的 WXML 模版和 Taro 同样,也是经过代码编译获得的;不一样于 Taro 运行时和 React 无关,mpvue 本质上仍是将 Vue 运行在了小程序,且实现了 Vue@2.4.1 绝大部分特性(只有极少数特性因为小程序模版的限制未能实现,如 :filterslotv-html);且整个框架基于 Webpack 实现了较为完善的工程化。

mage

其余小程序框架的实现原理和效果上的差别性,也带来了咱们的一些思考:

  • 编译时 OR 运行时:当初 Taro 选择重编译时的主要缘由是处于性能考虑,毕竟同等条件下,编译时作的工做越多,也就意味着运行时作的工做越少,性能会更好;另外,重编译时也保证了 Taro 的代码在编译以后的可读性。可是从长远来看,计算机硬件的性能愈来愈冗余,若是在牺牲一点能够容忍的性能的状况下换来整个框架更大的灵活性和更好的适配性,咱们认为是值得的
  • 模版静态编译 OR 动态构建:尽管 Taro 和 mpvue 的模版都是经过静态编译生成的,可是社区也不乏动态构建的例子,好比:Remax
  • DSL 限制:咱们可否实现一个小程序开发框架,摆脱 DSL 的限制?

新架构 Taro Next 的适配与实现

这一次,咱们站在浏览器的角度来思考前端的本质:不管开发这是用的是什么框架,React 也好,Vue 也罢,最终代码通过运行以后都是调用了浏览器的那几个 BOM/DOM 的 API ,如:createElementappendChildremoveChild 等。

image

所以,咱们建立了 taro-runtime 的包,而后在这个包中实现了 一套 高效、精简版的 DOM/BOM API(下面的 UML 图只是反映了几个主要的类的结构和关系):

image

而后,咱们经过 Webpack 的 ProvidePlugin 插件,注入到小程序的逻辑层。

image

这样,在小程序的运行时,就有了 一套高效、精简版的 DOM/BOM API

React 实现

DOM/BOM 注入以后,理论上来讲,Nerv/Preact 就能够直接运行了。可是 React 有点特殊,由于 React-DOM 包含大量浏览器兼容类的代码,致使包太大,而这部分代码咱们是不须要的,所以咱们须要作一些定制和优化。

在 React 16+ ,React 的架构以下:

image

最上层是 React 的核心部分 react-core ,中间是 react-reconciler,其的职责是维护 VirtualDOM 树,内部实现了 Diff/Fiber 算法,决定何时更新、以及要更新什么。

Renderer 负责具体平台的渲染工做,它会提供宿主组件、处理事件等等。例如 React-DOM 就是一个渲染器,负责 DOM 节点的渲染和 DOM 事件处理。

所以,咱们实现了 taro-react 包,用来链接 react-reconcilertaro-runtime 的 BOM/DOM API:

image

具体的实现主要分为两步:

  1. 实现 react-reconcilerhostConfig 配置,即在 hostConfig 的方法中调用对应的 Taro BOM/DOM 的 API。
  2. 实现 render 函数(相似于 ReactDOM.render)方法,能够当作是建立 Taro DOM Tree 的容器。

image

通过上面的步骤,React 代码实际上就能够在小程序的运行时正常运行了,而且会生成 Taro DOM Tree,那么偌大的 Taro DOM Tree 怎样更新到页面呢?

首先,咱们将小程序的全部组件挨个进行模版化处理,从而获得小程序组件对应的模版,以下图就是小程序的 view 组件通过模版化处理后的样子:

image

而后,咱们会:基于组件的 template,动态 “递归” 渲染整棵树

具体流程为先去遍历 Taro DOM Tree 根节点的子元素,再根据每一个子元素的类型选择对应的模板来渲染子元素,而后在每一个模板中咱们又会去遍历当前元素的子元素,以此把整个节点树递归遍历出来。

image

整个 Taro Next 的 React 实现流程图以下:

image

Vue 实现

别看 React 和 Vue 在开发时区别那么大,其实在实现了 BOM/DOM API 以后,它们之间的区别就很小了。

Vue 和 React 最大的区别就在于运行时的 CreateVuePage 方法,这个方法里进行了一些运行时的处理,好比:生命周期的对齐。

image

其余的部分,如经过 BOM/DOM 方法构建、修改 DOM Tree 及渲染原理,都是和 React 一致的。

Flutter 实现

提到 Flutter ,就不得不提 Flutter WebFlutter Web 是在标准浏览器 API 之上实现 Flutter 的核心绘图层,本质上也是最终调用了 BOM/DOM API。所以,理论来讲,也是能够进行适配的,但这一块咱们并不会投入太多的精力,最终会像快应用同样交给社区来实现和维护。

image

更多细节

接下来和你们展开聊一下 Taro Next 更多的细节实现,好比:事件、更新、生命周期。

事件

首先的 Taro Next 事件,具体的实现方式以下:

  1. 在 小程序组件的模版化过程当中,将全部事件方法所有指定为 调用 ev 函数,如:bindtapbindchangebindsubmit 等。
  2. 在 运行时实现 eventHandler 函数,和 eh 方法绑定,收集全部的小程序事件
  3. 经过 document.getElementById() 方法获取触发事件对应的 TaroNode
  4. 经过 createEvent() 建立符合规范的 TaroEvent
  5. 调用 TaroNode.dispatchEvent 从新触发事件

image

能够看到,Taro Next 事件本质上是基于 Taro DOM 实现了一套本身的事件机制,这样作的好处之一是,不管小程序是否支持事件的冒泡与捕获,Taro 都能支持。

更新

不管是 React 仍是 Vue ,最终都会调用 Taro DOM 方法,如:appendChildinsertChild 等。

这些方法在修改 Taro DOM Tree 的同时,还会调用 enqueueUpdate 方法,这个方法能获取到每个 DOM 方法最终修改的节点路径和值,如:{root.cn.[0].cn.[4].value: "1"},并经过 setData 方法更新到视图层。

image

能够看到,这里更新的粒度是 DOM 级别,只有最终发生改变的 DOM 才会被更新过去,相对于以前 data 级别的更新会更加精准,性能更好。

生命周期

相对与其余部分大刀阔斧的升级改造,生命周期多是变更最小的部分之一。和以前相似,生命周期的实现是在运行时维护的 App 实例 / Page 实例进行了生命周期方法的一一对应。

const config: PageInstance = {
  onLoad (this: MpInstance, options) {
    //...
  },
  onUnload () {
    //...
  },
  onShow () {
    safeExecute('onShow')
  },
  onHide () {
    safeExecute('onHide')
  },
  onPullDownRefresh () {
    safeExecute('onPullDownRefresh')
  }
  //...
}

新架构特色

和以前的架构不一样,Taro Next 是 近乎全运行

新的架构基本解决了以前的遗留问题:

  • 无 DSL 限制:不管是大家团队是 React 仍是 Vue 技术栈,都可以使用 Taro 开发
  • 模版动态构建:和以前模版经过编译生成的不一样,Taro Next 的模版是固定的,而后基于组件的 template,动态 “递归” 渲染整棵 Taro DOM 树。
  • 新特性无缝支持:因为 Taro Next 本质上是将 React/Vue 运行在小程序上,所以,各类新特性也就无缝支持了。
  • 社区贡献更简单:错误栈将和 React/Vue 一致,团队只须要维护核心的 taro-runtime。
  • 基于 Webpack:Taro Next 基于 Webpack 实现了多端的工程化,提供了插件功能。

性能优化

前面提到,同等条件下,编译时作的工做越多,也就意味着运行时作的工做越少,性能会更好。Taro Next 的新架构变成 近乎全运行 以后,花了不少精力在性能优化上面。

再这以前。能够先看一下 Taro Next 的流程和原生小程序的流程对比。

image

能够发现,相比原生小程序,Taro Next 多了红色部分的带来的性能隐患,如:引入React/Vue 带来的 包的 Size 增长,运行时的损耗、Taro DOM Tree 的构建和更新、DOM data 初始化和更新。

而咱们真正能作的,只有绿色部分,也就是:Taro DOM Tree 的构建和更新DOM data 初始化和更新

Size

首先咱们来看包 Size,下面的表格是 TodoMVC 的例子,在原生、Taro Old、Taro Next 等状况下的包大小对比,能够看到,引入 React/Vue 后,包大小在 Gzip 状况下大概增长了 30k 左右。

image

不过咱们在前面一再强调:和以前模版经过编译生成的不一样,Taro Next 的模版是固定的,而后基于组件的 template,动态 “递归” 渲染整棵 Taro DOM 树。也就是说,Taro Next 的 WXML 大小是有上限的

随着项目的增长,页面愈来愈多,原生的项目 WXML 体积会不断增长,而 Taro Next 不会。也就是说,当页面的数量超过一个临界点时,Taro Next 的包体积可能会更小。所以,包 Size 的问题不足为虑。

image

DOM Tree

在 Taro DOM Tree 的构建和更新阶段,咱们实现了一套仅实现了高效的、精简版 DOM/BOM API,并且仅仅实现了必要的。

Github上有一个仓库 jsdom,基本上是在 Node.js 上实现了一套 Web 标准的 DOM/BOM ,这个仓库的代码在压缩前大概有 2.1M,而 Taro Next 的核心的 DOM/BOM API 代码才 1000 行不到。

所以,咱们最大限度的保证了 Taro DOM Tree 构建和更新阶段的性能。

image

Update Date

在数据更新阶段,首先前面有提到过,Taro Next 的更新是 DOM 级别的,比 Data 级别的更新更加高效,由于 Data 粒度更新其实是有冗余的,并非全部的 Data 的改变最后都会引发 DOM 的更新

其次,Taro 在更新的时候将 Taro DOM Tree 的 path 进行压缩,这点也极大的提高了性能。

image

最终的结果是:在某些业务场景写,addselect 数据,Taro Next 的性能比原生的还要好。

taro-benchmark

固然,实验的数据总归会有缺陷,最终具体的性能表现,还要靠各类复杂业务场景的检验。你们若是对 Taro Next 的性能感兴趣的,能够自行跑一下 taro-benchmark 包,对比一下结果。

咱们也在一直持续的全方位优化 Taro Next 的性能,具体能够关注 Taro Next 的最新的 Commit 。

总结及展望

Taro 将来规划

Taro Next 将会在不久以后的 3.0 版本正式发布,支持使用 React/Vue 开发跨端小程序,而后在会在后续的迭代中拓展至其余端,并完善对应的生态。

image

Taro 团队仍是会将支持的重点放在 React/Vue,Flutter 和 Angular 会像快应用同样,交给社区来适配和维护,快应用就是华为的 Qiyu8Issacpeng 在帮咱们进行适配,很是感谢他们。

同时,咱们还打造了 「Taro 移动端一站式研发平台」,将先前积累的多端开发工做流和工程化的方案进行了统一,并内置了数据监控、组件市场以及可视化搭建,当前正处于内测阶段。

image

一点思考

  • 业务孵化技术,技术服务业务:这也是整个 Taro 项目从建立到迭代至今最重要的、感觉最深的一点。
  • 自上而下 OR 自下而上:从开发者的角度自上而下看,React/Vue 的代码书写方式差别挺大的;然而站在浏览器的角度自下而上的看,它们的差异其实没那么大,都是调用了 BOM/DOM 那几个经常使用的 API。若是咱们再往底层一点,站在渲染层的角度,不一样平台之间的差别会不会也没那么大?好比:Flutter。
  • Learn Once Write AnyWhere & Write Once Run AnyWhere:不少开发者更喜欢 React 提出的Learn Once Write AnyWhere,而咱们 Taro 的口号是 Write Once Run AnyWhere,这一点也常常致使咱们常常被人喷,这里说一点我本身的想法:Learn Once Write AnyWhere其实本质上对开发者更友好,好比开发者只须要学习 React 技术栈,就能够开发 Web/移动端 应用,可是对项目就没那么友好了,每一个项目都得维护一份代码;而 Write Once Run AnyWhere 是对开发者没那么友好(适配的端越多,适配的成本必然也会水涨船高,对开发者要求也很变高),可是根据咱们的实践,对项目会更友好,「一套代码,多端适配」。固然,这里适配的粒度,并不必定是项目级别的,其实在咱们的具体实践中,有至关一部分是:业务级甚至是页面级的

image

写在最后

正所谓「单丝不成线,独木不成林」,Taro 发展至今早已不在属于单一团队的项目了,而是整个 Taro 开发社区共同的项目。

最后,仍是借此机会感谢一些社区全部帮助过 Taro 的成长的人,特别是 Taro 的贡献者们,很是感谢!

image

同时也感谢受邀成为 TaroUI 核心维护人员的 Garfield550 (小姐姐)、梁音ShaoQian Liu,他们将支撑起 TaroUI 的后续迭代与维护。

固然还有在社区中乐于助人、积极贡献的 zacksleoJay Fongloveonelonglolipop99波仔糕原罪lentoo白领夏公子YuanQuantourzelingxiaoZhu 等等。

此外,还要感谢一直默默为 Taro 发展提供宝贵建议的研发团队:腾讯云、数字广东、腾讯CDC、网易严选、华为开源团队、招联消费金融等等。

长风破浪会有时,直挂云帆济沧海。

欢迎关注凹凸实验室博客:aotu.io

或者关注凹凸实验室公众号(AOTULabs),不定时推送文章:

image