hybrid 应用
Hybrid App(混合模式移动应用)是指介于web-app、native-app这两者之间的app,兼具“Native App良好用户交互体验的优势”和“Web App跨平台开发的优势”。
客户端提供webview
控件,能在App内使用H5技术开发页面,如果需要调用到系统能力的功能需要客户端实现,两者建立桥梁通信实现交互
优点
- 具备完整webview功能
- 可间接使用系统原生功能
- 随时更新无需发版
缺点
- 性能差距明显
- 白屏时间长,用户体验差
- 系统操作需要客户端原生支持提供方法
- 有时候受限客户端原因,需要特殊兼容
微信小程序
简介
「触手可及,用完即走」
小程序提供了自己的视图层描述语言 WXML
和 WXSS
,以及基于 JavaScript
的逻辑层框架,并在视图层与逻辑层间提供了数据传输和事件系统,让开发者能够专注于数据与逻辑。
框架的核心是一个响应的数据绑定系统,可以让数据与视图非常简单地保持同步。当做数据修改的时候,只需要在逻辑层修改数据,视图层就会做相应的更新。
技术发展史
小程序并非凭空冒出来的一个概念。当微信中的 WebView 逐渐成为移动 Web 的一个重要入口时,微信就有相关的 JS API 了。
2015年初,微信发布了一整套网页开发工具包,称之为 JS-SDK
,开放了拍摄、录音、语音识别、二维码、地图、支付、分享、卡券等几十个API。
JS-SDK是对之前的 WeixinJSBridge
的一个包装,以及新能力的释放,并且由对内开放转为了对所有开发者开放,在很短的时间内获得了极大的关注。从数据监控来看,绝大部分在微信内传播的移动网页都使用到了相关的接口。
JS-SDK 解决了移动网页能力不足的问题,通过暴露微信的接口使得 Web 开发者能够拥有更多的能力,然而在更多的能力之外,JS-SDK 的模式并没有解决使用移动网页遇到的体验不良的问题。
用户在访问网页的时候,在浏览器开始显示之前都会有一个白屏的过程,在移动端,受限于设备性能和网络速度,白屏会更加明显。因此设计了一个 JS-SDK 的增强版本,其中有一个重要的功能,称之为“微信 Web 资源离线存储”。
微信 Web 资源离线存储是面向 Web 开发者提供的基于微信内的 Web 加速方案。
通过使用微信离线存储,Web 开发者可借助微信提供的资源存储能力,直接从微信本地加载 Web 资源而不需要再从服务端拉取,从而减少网页加载时间,为微信用户提供更优质的网页浏览体验。每个公众号下所有 Web App 累计最多可缓存 5M 的资源。
在内部测试中,我们发现离线存储能够解决一些问题,但对于一些复杂的页面依然会有白屏问题,例如页面加载了大量的 CSS 或者是 JavaScript 文件。除了白屏,影响 Web 体验的问题还有缺少操作的反馈,主要表现在两个方面:页面切换的生硬和点击的迟滞感。
微信面临的问题是如何设计一个比较好的系统,使得所有开发者在微信中都能获得比较好的体验。这个问题是之前的 JS-SDK 所处理不了的,需要一个全新的系统来完成,它需要使得所有的开发者都能做到:
- 快速的加载
- 更强大的能力
- 原生的体验
- 易用且安全的微信数据开放
- 高效和简单的开发
- 无需安装
而缺点也很多:
- 组件较少
- webview限制很多,普通页面开发区别很大
- 行为限制很多,包体积大小,资源使用,页面打开层级等等
- 不同机型的兼容问题也是比较多
这就是小程序的由来。
小程序与普通网页开发的区别
网页开发渲染线程和脚本线程是互斥的,这也是为什么长时间的脚本运行可能会导致页面失去响应,而在小程序中,二者是分开的,分别运行在不同的线程中。
网页开发者可以使用到各种浏览器暴露出来的 DOM API,进行 DOM 选中和操作。而如上文所述,小程序的逻辑层和渲染层是分开的,逻辑层运行在 JSCore 中,并没有一个完整浏览器对象,因而缺少相关的DOM API和BOM API。这一区别导致了前端开发非常熟悉的一些库,例如 jQuery
、 Zepto
等,在小程序中是无法运行的。
同时 JSCore 的环境同 NodeJS 环境也是不尽相同,所以一些 NPM 的包在小程序中也是无法运行的。
开发端 | 开发场景 |
---|---|
网页开发者 | 各式各样的浏览器 |
PC 端 | IE、Chrome、QQ浏览器等 |
移动端 | Safari、Chrome以及 iOS、Android 系统中的各式 WebView |
小程序 | 两大操作系统 iOS 和 Android 的微信客户端,以及用于辅助开发的小程序开发者工具 |
小程序中三大运行环境也是有所区别的
运行环境 | 逻辑层 | 渲染层 |
---|---|---|
iOS | JavaScriptCore | WKWebView |
安卓 | V8 | chromium定制内核 |
小程序开发者工具 | NWJS | Chrome WebView |
网页开发者在开发网页的时候,只需要使用到浏览器,并且搭配上一些辅助工具或者编辑器即可。小程序的开发则有所不同,需要经过申请小程序帐号、安装小程序开发者工具、配置项目等等过程方可完成。
渲染层和逻辑层
小程序的运行环境分成渲染层和逻辑层,其中 WXML 模板和 WXSS 样式工作在渲染层,JS 脚本工作在逻辑层。
小程序的渲染层和逻辑层分别由2个线程管理:渲染层的界面使用了WebView
进行渲染;逻辑层采用JsCore线程
运行JS脚本。一个小程序存在多个界面,所以渲染层存在多个WebView线程,这两个线程的通信会经由微信客户端(下文中也会采用Native来代指微信客户端)做中转,逻辑层发送网络请求也经由Native转发,小程序的通信模型下图所示。
特别需要注意的是setData
调用方式和时机
- setData接口的调用涉及逻辑层与渲染层间的线程通信,通信过于频繁可能导致处理队列阻塞,界面渲染不及时而导致卡顿,应避免无用的频繁调用。
- 由于小程序运行逻辑线程与渲染线程之上,setData的调用会把数据从逻辑层传到渲染层,数据太大会增加通信时间。
- setData操作会引起框架处理一些渲染界面相关的工作,一个未绑定的变量意味着与界面渲染无关,传入setData会造成不必要的性能消耗。
开发环境
小程序提供了一个基于nwjs
实现的IDE开发工具,可以模拟代码预览效果,但是实际上并不是真机的webview环境,所以很多时候会发现开发环境跟真机预览的差别存在很多细节问题.并且因为小程序本身的包体大小限制,很多时候无法直接唤起真机调试,需要特殊处理将体积压到2M以下才行
加载过程
微信客户端在打开小程序之前,会把整个小程序的代码包下载到本地。
紧接着通过 app.json
的 pages
字段就可以知道你当前小程序的所有页面路径:
{
"pages":[
"pages/logs/logs"
]
}
于是微信客户端就把首页的代码装载进来,通过小程序底层的一些机制,就可以渲染出这个首页。
小程序启动之后,在 app.js
定义的 App
实例的 onLaunch
回调会被执行:
App({
onLaunch: function () {
// 小程序启动之后 触发
}
})
整个小程序只有一个 App 实例,是全部页面共享的
你可以观察到 pages/logs/logs
下其实是包括了4种文件的,微信客户端会先根据 logs.json
配置生成一个界面,顶部的颜色和文字你都可以在这个 json
文件里边定义好。紧接着客户端就会装载这个页面的 WXML
结构和 WXSS
样式。最后客户端会装载 logs.js
,你可以看到 logs.js
的大体内容就是:
Page({
data: { // 参与页面渲染的数据
logs: []
},
onLoad: function () {
// 页面渲染后 执行
}
})
Page
是一个页面构造器,这个构造器就生成了一个页面。在生成页面的时候,小程序框架会把 data
数据和 index.wxml
一起渲染出最终的结构,于是就得到了你看到的小程序的样子。
在渲染完界面之后,页面实例就会收到一个 onLoad
的回调,你可以在这个回调处理你的逻辑。
开发痛点
依赖管理混乱、工程化流程落后、ES Next 支持不完善、命名规范不统一等。这些问题在现在看来都已经有了各种官方或非官方的解决办法.
最常见的开发模式就是使用某一套完善的开发框架,他们最主要的区别就是DSL,类Vue或者类React语法为主.
在微信小程序之后,各大厂商纷纷发布了自己的小程序平台,多端适配型框架的需求也就应运而生了,(Taro, uni-app等)
所以开发技术选型的主要考虑因素就是: DSL 以及 多端适配
Taro1.x
市面上第一款遵循React语法的多端小程序框架,自研出Nervjs
.同时也支持使用 React/Vue/Nerv 等框架来开发
「 Write once Run anywhere」
目前(Taro3.x)官方支持转换的平台如下:
- H5
- ReactNative
- 微信小程序
- 京东小程序
- 百度小程序
- 支付宝小程序
- 字节跳动小程序
- QQ 小程序
- 钉钉小程序
- 企业微信小程序
- 支付宝 IOT 小程序
- 飞书小程序
设计思路
必须满足下述要求:
- 代码多端复用,不仅能运行在时下最热门的 H5、微信小程序、React Native,对其他可能会流行的端也留有余地和可能性。
- 完善和强大的组件化机制,这是开发复杂应用的基石。
- 与目前团队技术栈有机结合,有效提高效率。
- 学习成本足够低
- 背后的生态强大
在一个优秀且严格的规范限制下,从更高抽象的视角(语法树)来看,每个人写的代码都差不多。
也就是说,对于微信小程序这样不开放不开源的端,我们可以先把 React 代码分析成一颗抽象语法树,根据这颗树生成小程序支持的模板代码,再做一个小程序运行时框架处理事件和生命周期与小程序框架兼容,然后把业务代码跑在运行时框架就完成了小程序端的适配。
对于 React 已经支持的端,例如 Web、React Native 甚至未来的 React VR,我们只要包一层组件库再做些许样式支持即可。鉴于时下小程序的热度和我们团队本身的业务侧重程度,组件库的 API 是以小程序为标准,其他端的组件库的 API 都会和小程序端的组件保持一致。
架构
Taro 当前的架构主要分为:编译时 和 运行时。
Taro 编译时
使用 babel-parser
将 Taro 代码解析成抽象语法树,然后通过 babel-types
对抽象语法树进行一系列修改、转换操作,最后再通过 babel-generate
生成对应的目标代码。
Babel 的编译过程亦是如此,主要包含三个阶段
- 解析过程,在这个过程中进行词法、语法分析,以及语义分析,生成符合 ESTree 标准 虚拟语法树(AST)
- 转换过程,针对 AST 做出已定义好的操作,babel 的配置文件 .babelrc 中定义的 preset 、 plugin 就是在这一步中执行并改变 AST 的
- 生成过程,将前一步转换好的 AST 生成目标代码的字符串
如果对AST是什么不了解的话可以使用这个网站尝试一下https://astexplorer.net/
省去整个编译过程得到的结果代码如下
{this.props.counter.num}
{{counter.num}}
因为JSX的丰富自由度不是字符串模板可以比拟的,所以当时只能支持大概80%的写法转换,剩余不支持的写法转由eslint
插件提醒用户修改。
在开源的过程中,Taro 支持的 JSX 写法一直在不断完善,力求让开发体验更加接近于 React,主要包括以下语法支持:
- 支持 Ref,提供了更加方便的组件和元素定位方式
- 支持 this.props.children 写法,方便进行自定义组件传入子元素
- 在循环体内执行函数和表达式
- 定义 JSX 作为变量使用
- 支持复杂的 if-else 语句
- 在 JSX 属性中使用复杂表达式
- 在 style 属性中使用对象
- 只有使用到的变量才会作为 state 加入到小程序 data,从而精简小程序数据
Taro 运行时
我们可以对比一下编译后的代码,可以发现,编译后的代码中,React 的核心 render
方法没有了。同时代码里增加了 BaseComponent
和 createComponent
,它们是 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 (
Hello world!
)
}
}
// 编译后
import {BaseComponent, createComponent} from '@tarojs/taro-weapp'
class Index extends BaseComponent {
// ...
_createDate(){
//process state and props
}
}
export default createComponent(Index)
BaseComponent
大概的 UML
(统一建模语言)图如下,主要是对 React 的一些核心方法:setState
、forceUpdate
等进行了替换和重写,结合前面编译后 render
方法被替换,大家不难猜出:Taro 当前架构只是在开发时遵循了 React 的语法,在代码编译之后实际运行时,和 React 并没有关系。
而 createComponent
主要作用是调用 Component()
构建页面;对接事件、生命周期等;进行 Diff Data
并调用 setData
方法更新数据。
跨端兼容
仅仅将代码按照语法规范转换之后还远远不够,因为不同端的特有原生能力或组件等限制,所以决定了跨端开发必然有部分代码需要开发中实现兼容,而为了最大程度弥补不同端的差异,Taro制定了一个统一标准,在不同端依靠它们的语法与能力去实现组件库与API,同时还要为不同端编写相应的运行时框架,初始化等等。因为最初的设计来源就是小程序,所以决定直接采用小程序的标准,让其他端向小程序靠齐。
因为我们有编译的操作,在书写代码的时候,只需要引入标准组件库 @tarojs/components
与运行时框架 @tarojs/taro
,代码经过编译之后,会变成对应端所需要的库。
例如,为了提升开发便利性,我们为 Taro 加入了 Redux
支持,我们的做法就是,在小程序端,我们实现了 @tarojs/redux
这个库来作为小程序的 Redux
辅助库,并且以他作为基准库,它具有和 react-redux
一致的 API,在书写代码的时候,引用的都是 @tarojs/redux
,经过编译后,在 H5 端会替换成 nerv-redux
(Nerv
的 Redux
辅助库),在 RN 端会替换成 react-redux
。这样就实现了 Redux
在 Taro 中的多端支持。
小程序组件化
开源之初,由于种种原因,Taro 的微信小程序端组件化采用的是小程序 标签来实现的,利用小程序
标签的特性,将组件 JS 文件编译成 JS + WXML 模板,在父组件(页面)的模板中通过
标签引用子组件的 WXML 模板来进行拼接,从而达到组件化的目的。
实践证明,Template
模板方案是一个失败的组件化方案,Taro 开源初期的 Bug 主要来源于此。因为这一方案将 JS 逻辑与模板拆分开了,需要手工来保证 JS 与模板中数据一致,这样在循环组件渲染、组件多重嵌套的情况下,要保证组件正确渲染与 props
正确传递的难度非常大,实现的成本也非常高。而且,囿于小程序 标签的缺陷,一些功能(例如自定义组件包含子元素等)无法实现。
所以,在经过艰辛的探索与实践之后,我们采用了小程序原生组件化来作为 Taro 的小程序端组件化方案,并且通过一些处理,绕开了小程序组件化的诸多限制,为 Taro 的稳定性打下了坚实基础,并带来了以下好处:
- 小程序端组件化更加健壮
- 尽可能减少由于框架带来的性能问题
- 依托官方组件化,方便以后解锁更多可能
功能更新
1.1
- 加入了对 百度智能小程序 和 支付宝小程序 的支持
- 为每个平台提供了平台标识
1.2
- 字节跳动(头条)小程序支持
- 微信小程序转 Taro
- CSS Modules 支持
- MobX 支持
1.3
- 支持快应用和 QQ 小程序的开发
- 全面支持 JSX 语法和 React Hooks
- 大幅提高 H5 性能和可用性
- Taro Doctor
技术选型与权衡
总结
当前Taro的特点是:
- 重编译,轻运行
- 编译代码与React无关
- 直接使用Babel编译,导致在工程化和插件方面羸弱
缺点也比较多:
- 维护困难,每次需要新增一个功能,例如支持解析 Markdown 文件,就需要直接改动 CLI,不够灵活
- 难以共建,CLI 的代码非常复杂,而且逻辑分支众多,让很多想要一起共建的人难以入手
- 可扩展性偏低,自研的构建系统,设计之初没有考虑到后续的扩展性,导致开发者想要添加自定义的功能无从下手
mpvue2.0(截至2018.8.10已停止维护)
mpvue 是一个使用 Vue.js 开发小程序的前端框架,目前支持 微信小程序、百度智能小程序,头条小程序 和 支付宝小程序。 框架基于 Vue.js,修改了的运行时框架 runtime 和代码编译器 compiler 实现,使其可运行在小程序环境中,从而为小程序开发引入了 Vue.js 开发体验。
Vue.js 小程序版, fork 自 vuejs/vue@2.4.1,保留了 vue runtime 能力,添加了小程序平台的支持。
所以本章节Vue相关知识都是指Vue2
- 彻底的组件化开发能力:提高代码复用性
- 完整的 Vue.js 开发体验
- 方便的 Vuex 数据管理方案:方便构建复杂应用
- 快捷的 webpack 构建机制:自定义构建策略、开发阶段 hotReload
- 支持使用 npm 外部依赖
- 使用 Vue.js 命令行工具 vue-cli 快速初始化项目
- H5 代码转换编译成小程序目标代码的能力
框架原理
mpvue
保留了 vue.runtime 核心方法,无缝继承了Vue.js
的基础能力mpvue-template-compiler
提供了将 vue 的模板语法转换到小程序的 wxml 语法的能力- 修改了 vue 的建构配置,使之构建出符合小程序项目结构的代码格式: json/wxml/wxss/js 文件
Vue代码
- 将小程序页面编写为 Vue.js 实现
- 以 Vue.js 开发规范实现父子组件关联
小程序代码
- 以小程序开发规范编写视图层模板
- 配置生命周期函数,关联数据更新调用
- 将 Vue.js 数据映射为小程序数据模型
并在此基础上,附加如下机制
- Vue.js 实例与小程序 Page 实例建立关联
- 小程序和 Vue.js 生命周期建立映射关系,能在小程序生命周期中触发 Vue.js 生命周期
- 小程序事件建立代理机制,在事件代理函数中触发与之对应的 Vue.js 组件事件响应
这套机制总结起来非常简单,但实现却相当复杂。在揭秘具体实现之前,读者可能会有这样一些疑问:
要同时维护 Vue.js 和小程序,是否需要写两个版本的代码实现?
首先,mpvue 为提高效率而生,本身提供了自动生成小程序代码的能力,小程序代码根据 Vue.js 代码构建得到,并不需要同时开发两套代码。
小程序负责视图层展现,Vue.js的视图层是否还需要,如果不需要应该如何处理?
Vue.js 视图层渲染由 render 方法完成,同时在内存中维护着一份虚拟 DOM,mpvue 无需使用 Vue.js 完成视图层渲染,因此我们改造了 render 方法,禁止视图层渲染。
生命周期如何打通,数据同步更新如何实现?
生命周期关联:生命周期和数据同步是 mpvue 框架的灵魂,Vue.js 和小程序的数据彼此隔离,各自有不同的更新机制。mpvue 从生命周期和事件回调函数切入,在 Vue.js 触发数据更新时实现数据同步。小程序通过视图层呈现给用户、通过事件响应用户交互,Vue.js 在后台维护着数据变更和逻辑。可以看到,数据更新发端于小程序,处理自 Vue.js,Vue.js 数据变更后再同步到小程序。为实现数据同步,mpvue 修改了 Vue.js runtime 实现,在 Vue.js 的生命周期中增加了更新小程序数据的逻辑。
事件代理机制:用户交互触发的数据更新通过事件代理机制完成。在 Vue.js 代码中,事件响应函数对应到组件的 method, Vue.js 自动维护了上下文环境。然而在小程序中并没有类似的机制,又因为 Vue.js 执行环境中维护着一份实时的虚拟 DOM,这与小程序的视图层完全对应,我们思考,在小程序组件节点上触发事件后,只要找到虚拟 DOM 上对应的节点,触发对应的事件不就完成了么;另一方面,Vue.js 事件响应如果触发了数据更新,其生命周期函数更新将自动触发,在此函数上同步更新小程序数据,数据同步也就实现了。
建构流程
建构流程是整个项目最核心的地方之一,通过我们所熟知的 webpack,完成了 template 转换为 wxml 和 样式转换优化以及其他的若干代码的拼接压缩混淆等操作,最终使之可以运行在微信小程序的环境中。
生命周期
除了 Vue2 本身的生命周期外,mpvue 还兼容了小程序生命周期
app 部分:
- onLaunch,初始化
- onShow,当小程序启动,或从后台进入前台显示
- onHide,当小程序从前台进入后台
page 部分:
- onLoad,监听页面加载
- onShow,监听页面显示
- onReady,监听页面初次渲染完成
- onHide,监听页面隐藏
- onUnload,监听页面卸载
- onPullDownRefresh,监听用户下拉动作
- onReachBottom,页面上拉触底事件的处理函数
- onShareAppMessage,用户点击右上角分享
- onPageScroll,页面滚动
- onTabItemTap, 当前是 tab 页时,点击 tab 时触发 (mpvue 0.0.16 支持)
架构
mpvue 的实现同样分为:编译时和运行时。
在 Vue 源码的 platforms 文件夹下面增加了 mp 目录,在里面实现了 complier(编译时)
和 runtime (运行时)
支持。
编译时
相比于Taro将JSX转成小程序模板,mpvue是将vue模板进行转换,两者的相似性可以简化很多工作程序
{{ message }}
-------------------------------------------编译--------------------------------------------------------
{{ message }}
运行时
Vue2原理图
vue文件主要分三部分: template
, script
, style
模板部分会经由vue-loader
进行ast分析,生成render渲染函数
脚本部分会进行Vue实例化,对data数据进行响应式处理,每次修改都会触发render函数
render函数会生成虚拟Dom,每次新旧虚拟Dom之间会进行patch对比,得到最终修改结果才去操作真实Dom更新
mpvue 整体原理图
其中mpvue-template-compiler
作用是将Vue2预编译成render函数以避免运行时编译开销和CSP限制, 只有在编写具有特定需求的构建工具时才需要它。
因为小程序跟Vue原理不同,所以直接移除Vue的dom操作阶段,取而代之的是直接交给小程序原生实现
在Vue实例化的时候也会调用小程序实例化,每次Vue进行patch之后,会直接调用$updateDataToMP()
获取挂载在page实例上数据,然后通过setData
方法更新视图
总结
mpvue属于半编译半运行,主要体现在
- JS运行本质上还是用的是Vue那一套代码,除了部分特性因为小程序的限制无法兼容(如 :
filter
、slot
、v-html
) - 模板代码需要预先进行编译成WXML模板
所以两边属于一个割裂状态,vue负责数据处理,小程序负责渲染视图,实际上是方便开发但是没有优化小程序的体验
Wepy2(已废弃)
设计思想
- 非侵入式设计 WePY 2 运行于小程序之上,是对小程序原有能力的封装和优化,并不会对原有小程序框架有任何改动或者影响。
- 兼容原生代码 能够兼容原生代码,即部分页面为原生,部分页面为 WePY。同时做到无需任何改动即可引用现有原生开发的小程序组件。
- 基于小程序原生组件实现组件化开发 小程序原生组件在性能上相较之前有了很大的提升。因此 WePY 2 完全基于小程序原生组件实现,不支持小程序基础库 < 1.6.3 的版本。
- 基于 Vue Observer 实现数据绑定 数据绑定基于 Vue Observer 实现,同时为其定制小程序特有的优化。
- 更优的可扩展性 对于 core 库提供 mixin、hooks、use 等方式进行开发扩展,对于 cli 编译工具提供更强大的插件机制方便开发者可以侵入到编译的任何一个阶段进行个性化定制的编译。
转换流程
WePY 继承了 WXML 的基本模板语法,并支持大部分 Vue 模板语法。
HTML 模板标签映射表
标签 | 转换后 |
---|---|
select | picker |
datalist | picker |
img | image |
source | audio |
video | video |
track | video |
a | navigator |
span | label |
其它 | view |
事件处理
小程序原生的事件系统 使用bind,catch 等关键字进行事件监听。 而在 WePY 中,事件沿用了 Vue 的风格,使用 v-on 或者是 @ 操作符进行事件监听。同时 WePY 中会有一个统一的事件分发器接管原生事件。大致如下图:
WePY 在编译过程中,会找到所有监听事件,并为其分配事件 ID。同时将事件代码(可以是一个方法,也可以是一个简单的代码片段)注入至 JS 代码中。 然后当事件分发器接收到原生事件时,会通过事件 ID,分发到相应的事件逻辑当中。
这样做的好处主要是:
- 可控性更强。用户可利用相关钩子函数从而处理全局的用户事件。(典型场景:为页面全部按钮统一增加一个点击上报功能)
- 灵活度更高。相对于原生只能使用函数名的方式来说,还可使用简单代码片段。(典型场景:@tap="num++")
Taro2.x
更多是对底层架构的革新,提高拓展性,稳定性,可维护性,降低开发和学习成本
CLI
Taro 2.0 的 CLI 将会变得非常轻量,只会做区分编译平台、处理不同平台编译入参等操作,随后再调用对应平台的 runner 编译器 做代码编译操作,而原来大量的 AST 语法操作将会改造成 Webpack Plugin 以及 Loader,交给 Webpack 来处理。
- 利于维护,大量的逻辑交由 Webpack 来处理,我们只需要维护一些插件
- 更加稳定,相较于自研的构建系统,新的构建会更加稳定,降低一些奇怪错误的出现概率
- 可扩展性强,可以通过自行加入 Webpack Loader 与 Plugin 的方式做自己想要的扩展
- 各端编译统一,接入 Webpack 后,Taro 各端的编译配置可以实现非常大程度的统一
其他
编译配置调整
异步编程调整
主编译流程钩子
编译添加 Loader
编译添加 Plugin
Taro RN 依赖升级
Taro3.x
跨框架:React、Nerv、Vue 2、Vue 3、jQuery
更快的运行速度
运行时性能主要分为两个部分,一是更新性能,二是初始化性能。
对于更新性能而言,旧版本的 Taro 会把开发者 setState
的数据进行一次全量的 diff,最终返回给小程序是按路径更新的 data
。而在 Taro Next 中 diff 的工作交给了开发者使用的框架(React/Nerv/Vue),而框架 diff 之后的数据也会通过 Taro 按路径去最小化更新。因此开发者可以根据使用框架的特性进行更多更细微的性能优化。
初始化性能则是 Taro Next 的痛点。原生小程序或编译型框架的初始数据可以直接用于渲染,但 Taro Next 在初始化时会把框架的渲染数据转化为小程序的渲染数据,多了一次 setData
开销。
为了解决这个问题,Taro 从服务端渲染受到启发,在 Taro CLI 将页面初始化的状态直接渲染为无状态的 wxml,在框架和业务逻辑运行之前执行渲染流程。我们将这一技术称之为预渲染(Prerender),经过 Prerender 的页面初始渲染速度通常会和原生小程序一致甚至更快。
更快的构建速度和 source-map 支持
作为一个编译型框架,旧版本的 Taro 会进行大量的 AST 操作,这类操作显著地拖慢了 Taro CLI 的编译速度。而在 Taro Next 中不会操作任何开发者代码的 AST,因此编译速度得到了大幅的提高。
正因为 AST 操作的取消,Taro Next 也轻松地实现了 source-map
的支持。这对于开发体验是一个巨大的提升:
其他
跨端:H5、微信、支付宝、百度、字节跳动...小程序
微信小程序转 React/Vue
渲染 HTML 字符串
CSS-in-JS
虚拟列表(VirtualList)
开放式插件系统
预渲染
开放式架构
不同于 Taro 1、2 时代的架构,新的架构主要基于运行时,我们都知道使用 React 开发 web,渲染页面主要依靠的是 react-dom 去操作 DOM 树,而 React Native 依靠的是 Yoga 布局引擎,但是我们却能通过 React 将他们联系在一起,这主要是通过抽象的 Virtual DOM 技术来实现的,通过 Virtual DOM 达到跨平台统一的目的。而小程序中虽然没有直接暴露 DOM 和 BOM API,但是我们却可以类比 React 和 React Native 的思想,在小程序中模拟实现 DOM 以及 BOM 的 API,从而实现直接将 React 运行到小程序环境中的目的,这就是 Taro 新架构的由来。
目标是可以通过插件的形式扩展 Taro 的端平台支持能力:
- 插件开发者无需修改 Taro 核心库代码,即可编写出一个端平台插件。
- 插件使用者只需安装、配置端平台插件,即可把代码编译到指定平台。
- 开发者可以继承现有的端平台插件,然后对平台的适配逻辑进行自定义。
初 Taro 选择重编译时的主要原因是处于性能考虑,毕竟同等条件下,编译时做的工作越多,也就意味着运行时做的工作越少,性能会更好;另外,重编译时也保证了 Taro 的代码在编译之后的可读性。但是从长远来看,计算机硬件的性能越来越冗余,如果在牺牲一点可以容忍的性能的情况下换来整个框架更大的灵活性和更好的适配性,我们认为是值得的。
Taro实现了一套高效、精简版的 DOM/BOM API运行时渲染代码 @tarojs/runtime
Taro 运行时。在小程序端连接框架(DSL)渲染机制到小程序渲染机制,连接小程序路由和生命周期到框架对应的生命周期。在 H5/RN 端连接小程序生命周期 规范到框架生命周期。
不管什么框架,最终都是转成浏览器可执行的代码,用的都是通用的DOM/BOM API,例如createElement
、appendChild
、removeChild
等
在 DOM/BOM
注入之后,理论上来说,Nerv/Preact 就可以直接运行了。但是 React 有点特殊,因为 React-DOM
包含大量浏览器兼容类的代码,导致包太大,而这部分代码我们是不需要的,因此我们需要做一些定制和优化。
React16+实现
可以看到React大体分三层
- React核心实现源码
- Diff/Fiber算法实现源码, 负责维护虚拟树
- 具体平台渲染实现源码,负责组件,事件,节点渲染等
所以Taro实现了@tarojs/react
基于react-reconciler
的小程序专用 React 渲染器,连接@tarojs/runtime
的 DOM 实例,相当于小程序版的react-dom
,暴露的 API 也和react-dom
保持一致。
其中最重要的的两个实现功能
hostConfig: 关联对应的BOM/DOM实现API,完成Dom操作功能
render: 实际的渲染方法
function render(element, domContainer, cb) { const oldRoot = ContainerMap.get(domContainer); if (oldRoot != null) { return oldRoot.render(element, cb); } const root = new Root(TaroReconciler, domContainer); ContainerMap.set(domContainer, root); return root.render(element, cb); }
目前为止已经完成了代码运行逻辑,剩下的就是基于组件的template,动态渲染页面
Vue实现
抛开已实现的BOM/DOM API之后,React和Vue的区别很小,只需要在运行时的CreateVuePage
进行一些处理,例如生命周期对齐等,其他部分基本都一致
事件
首先的 Taro Next 事件,具体的实现方式如下:
- 在 小程序组件的模版化过程中,将所有事件方法全部指定为 调用 ev 函数,如:
bindtap
、bindchange
、bindsubmit
等。 - 在 运行时实现
eventHandler
函数,和 eh 方法绑定,收集所有的小程序事件 - 通过
document.getElementById()
方法获取触发事件对应的TaroNode
- 通过
createEvent()
创建符合规范的TaroEvent
- 调用
TaroNode.dispatchEvent
重新触发事件
Taro Next 事件本质上是基于 Taro DOM 实现了一套自己的事件机制,这样做的好处之一是,无论小程序是否支持事件的冒泡与捕获,Taro 都能支持。
更新
无论是 React 还是 Vue ,最终都会调用 Taro DOM 方法,如:appendChild
、insertChild
等。
这些方法在修改 Taro DOM Tree 的同时,还会调用 enqueueUpdate
方法,这个方法能获取到每一个 DOM 方法最终修改的节点路径和值,如:{root.cn.[0].cn.[4].value: "1"}
,并通过 setData
方法更新到视图层。
这里更新的粒度是 DOM 级别,只有最终发生改变的 DOM 才会被更新过去,相对于之前 data 级别的更新会更加精准,性能更好。
新架构特点
新的架构基本解决了之前的遗留问题:
- 无 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 多了红色部分的带来的性能隐患,如:引入 React/Vue 带来的 包的 Size 增加,运行时的损耗、Taro DOM Tree 的构建和更新、DOM data 初始化和更新。
而我们真正能做的,只有绿色部分,也就是:Taro DOM Tree 的构建和更新、DOM data 初始化和更新。
Update Date
Taro Next 的更新是 DOM 级别的,比 Data 级别的更新更加高效,因为 Data 粒度更新实际上是有冗余的,并不是所有的 Data 的改变最后都会引起 DOM 的更新。
其次,Taro 在更新的时候将 Taro DOM Tree 的 path
进行压缩,这点也极大的提升了性能。
最终的结果是:在某些业务场景写,add
、select
数据,Taro Next 的性能比原生的还要好。
功能更新
3.1
开放式架构
新增小程序性能优化组件 CustomWrapper
原生小程序渐进式混合使用 Taro 开发
拥抱 React 17、TypeScript 4
3.2
更快编译速度
source-map 支持
多 React Native 版本支持,拥抱最新版 0.64
更丰富API与组件
API与组件按需引入
3.3
支持使用 H5 标签
小程序支持使用框架的 DevTools
修复百度小程序 flex 布局的问题
3.4 beta
支持使用 Preact 进行开发(一款体积超小的类 React 框架,提供和 React 几乎一致的 API,而体积只有 5k 左右)
Vue 3 支持 Composition API 版本的小程序生命周期钩子
运行时体积优化
3.5 canary
支持适配鸿蒙