Taro
一.目标定位
一套遵循 React 语法规范的多端统一开发框架
一种多端代码转换方案,这里的“端”是指微信小程序、Web、ReactNative、百度小程序、支付宝小程序、头条小程序、快应用等等
具体地, 把一份类React源码,通过“编译”转换成兼容目标端的形式 ,即:
转换 nerv业务代码 ------> xx小程序业务代码 + Web业务代码 + ReactNative业务代码
目的是降低开发成本,提高效率:
让原本只能运行在一端的项目获得多端运行的能力,降低开发者的重构成本。
二.思路探索
初衷
用React写微信小程序。
微信小程序原生方式开发起来 ofollow,noindex" target="_blank">太费劲 ,遂想用React开发微信小程序
延伸
在React业务代码转微信小程序代码这个最初的需求实现之后,发现依靠同样的转换思路可以适配多端,即 从1对1延伸到1对n :
P.S.其中 Nerv 是一种类React框架,API与React类似
P.S.Taro组件库之所以以微信小程序为标准,也是初衷使然(都做完了不能浪费啊)
思路
想要一份代码通吃 n
端,无非2种思路:
-
直接从
1
端向n - 1
端转换 -
加一层抽象,从这层抽象转换到
n
端
以Bash与Batch(Windows批处理脚本)为例,如果只写一份脚本,想既能在*nix跑,又能在Windows跑,第一种思路只需要实现1个东西(从 bash
向 n - 1
端转换):
function bash2batch(bash) { // ... return equivalentBatch; }
或者(从 batch
向 n - 1
端转换):
function batch2bash(batch) { // ... return equivalentBash; }
如果能实现 AtoB
,一份 A
就可以适配 A
和 B
了,但 “硬”转通常比较困难 ,因此在Bash与Batch的场景,诞生了第二种思路的实现:
Batsh: A language that compiles to Bash and Windows Batch.
也就是加一层抽象 C
,再分别实现 CtoA
和 CtoB
,从 Batsh 这层抽象转换到 n
端:
// 1.定义抽象层Batsh const batsh = 'Neither bash nor batch'; // 2.实现抽象层向2端转换 function batsh2batch(batsh) { // ... return equivalentBatch; } function batsh2bash(batsh) { // ... return equivalentBash; }
类似地,Taro也采用了第二种思路,这层抽象就是Taro业务代码:
P.S.Taro业务代码即图中的Nerv代码,叫Taro代码更准确一些,因为增加了一些Taro特有的API支持(如 Taro.getEnv()
),是Nerv的超集
三.核心实现
以微信小程序为例,它由4部分组成:
配置与样式没什么好说的, 难点 在于模板的转换和逻辑的转换
P.S.ReactNative样式转换另说,也是一个难题,因为RN在选择器、属性名/值及默认值,甚至CSS特性支持程度都存在较大差异
编译转换
要把一份代码A转换成另一份代码B,需要做3件事情:
-
解析代码A生成抽象描述(AST)
-
根据一些映射规则操作AST,生成新的AST
-
根据新的AST生成代码B
P.S.关于编译转换的更多信息,请查看再看编译原理与 Babel快速指南
模板的转换
把 JSX 语法转换成可以在小程序运行的字符串模板。
输入JSX:
render() { const { percent } = this.state; return ( <View className='index'> <Button className='add_btn' onClick={this.props.add}>+</Button> { percent && <MyProgress percent={percent} strokeWidth={6} color='#FF4949' /> } </View> ); }
经 @tarojs/transformer-wx 转换,输出微信小程序模板:
<block> <view class="index"> <button class="add_btn" bindtap="funPrivatesBrJC">+</button> <block wx:if="{{percent}}"> <my-progress percent="{{percent}}" strokeWidth="{{6}}" color="#FF4949"></my-progress> </block> </view> </block>
View
、 Button
等都是 Taro内置组件 :
Taro 以 微信小程序组件库 为标准,结合 jsx 语法规范,定制了一套自己的组件库规范
相关package如下:
-
@tarojs/components :支持Web环境Nerv组件库,通过编译替换为目标平台的原生标签/组件
-
@tarojs/taro-components-rn :支持ReactNative环境的React组件库(之所以ReactNative组件库独立出来,可能是因为差异较大,难以通过编译手段实现转换)
都会被转换成目标端的原生组件:
在小程序端,我们可以使用所有的小程序原生组件,而在其他端,我们提供了对应的组件库实现
但自定义组件 my-progress
在微信小程序中是不存在的,所以 并不能如预期地跑起来
势必需要一种 跨端组件定义 ,为此Taro提供了2个东西:
-
跨端组件库 Taro UI
-
支持把自定义组件打包成各目标端支持的形式(具体见 基于 Taro 开发第三方多端 UI 库 )
前者解决有没有的问题,应对一般应用场景。后者开放一种自定义的能力,满足需要定制的场景
逻辑的转换
类似于组件库需要做多端适配,各端能力差异也同样需要适配:
组件库以及端能力都是依靠不同的端做不同实现来抹平差异
运行时框架负责适配各端能力 ,以支持跑在上面的Taro业务代码,主要有3个作用:
-
适配组件化方案、配置选项等基础API
-
适配平台能力相关的API(如网络请求、支付、拍照等)
-
提供一些应用级的特性,如事件总线(
Taro.Events
、Taro.eventCenter
)、运行环境相关的API(Taro.getEnv()
、Taro.ENV_TYPE
)、UI适配方案(Taro.initPxTransform()
)等
实现上, @tarojs/taro
是API适配的统一入口, 编译时分平台替换 :
- @tarojs/taro : 只是一层空壳,提供API签名
平台适配相关的package有6个:
-
@tarojs/taro-alipay :适配支付宝小程序
-
@tarojs/taro-h5 :适配Web
-
@tarojs/taro-rn :适配ReactNative
-
@tarojs/taro-swan :适配百度小程序
-
@tarojs/taro-tt :适配头条小程序
-
@tarojs/taro-qapp :适配快应用
P.S.与组件库适配方案不同的是,API干脆放弃编译转换这条路,直接整个替掉
实际上,要想只维护一份业务代码,那么Taro提供的API必定是 n端API的并集 ,例如:
// 各小程序都支持的API Taro.setStorage() // 百度小程序专有API Taro.textToAudio() // 支付宝小程序与微信小程序参数处理上存在差异的API Taro.getStorageSync() // ...
这些API都可以直接使用,不用关心当前平台是否支持,因为运行时框架的适配工作的一部分就是抹平平台能力API差异,例如:
H5 端就无法调用扫码、蓝牙等端能力
采用微信小程序标准,所以这些 API 在 H5 端运行的时候将什么也不做。
同时 在业务层区分目标环境 ,保证这些平台相关的代码仅在预期的目标环境下执行:
-
编译时:
process.env.TARO_ENV
-
运行时:
Taro.getEnv()
例如:
// 分平台调用API if (process.env.TARO_ENV === 'swan') { Taro.textToAudio() } // 分平台使用不同组件 <View> {process.env.TARO_ENV === 'weapp' && <ScrollViewWeapp />} {process.env.TARO_ENV === 'h5' && <ScrollViewH5 />} </View>
P.S.编译时静态的环境区分足够应对大多数场景了,运行时的环境区分仅 备不时之需
四.结构
从设计上看,Taro方案分为3层:
业务层(类React代码) --------------------- 转换层(JSX转微信小程序) --------------------- 适配层 组件库(适配n端原生组件) 运行时框架(适配n端API能力) ---------------------
此外,还有
-
生态:UI库、路由、数据流管理、CSS预处理等
-
Lint:对于转换层不支持的写法,通过静态检查给出一部分警告
五.源码简析
对应到具体实现,各部分对应的package如下( taro/packages/ ):
// 转换 babel-plugin-transform-jsx-to-stylesheet taro-plugin-babel taro-plugin-csso taro-plugin-uglifyjs taro-transformer-wx // 适配-组件库 taro-components-rn taro-components // 适配-运行时框架 taro-alipay taro-h5 taro-qapp taro-rn taro-swan taro-tt taro-weapp taro // 生态 postcss-plugin-constparse postcss-pxtransform postcss-unit-transform taro-async-await taro-mobx-common taro-mobx-h5 taro-mobx-prop-types taro-mobx-rn taro-mobx taro-plugin-less taro-plugin-sass taro-plugin-stylus taro-plugin-typescript taro-redux-h5 taro-redux-rn taro-redux taro-router-rn taro-router // 构建 taro-cli taro-rn-runner taro-webpack-runner // Lint eslint-config-taro eslint-plugin-taro // 其它(公共方法) taro-utils
另外,还有个 有意思的东西 :
// 微信小程序转Taro taroize // taroize 之后的运行时 taro-with-weapp
反向转换 是另一扇门,就转换而言,从1对1延伸到1对n之后,下一个阶段就是n到1了,即:
// 目标端 A = weapp B = ReactNative C = ReactNative // 抽象层 T = Taro // 第一阶段:1对1 T2A() // 第二阶段:1对n T2A(), T2B(), T2C()... // 第三阶段:n到1 A2T(), B2T(), C2T()...
等到第三阶段完成,就 天下大同 了(随便拿个什么东西都能转换到n端)
P.S.目前(2018/12/9), A2T()
(小程序代码转 Taro)已经待发布了,具体见 版本计划
六.限制
限制方面感受最深的应该是JSX,毕竟JSX的灵活性令人发指(动态组件、高阶组件),同时微信小程序的模板语法又限制极多(即便通过 WXS 这个补丁增强了一部分能力),这就出现了一个不可调和的 矛盾 ,因此:
JSX 的写法极其灵活多变,我们只能通过穷举的方式,将常用的、React 官方推荐的写法作为转换规则加以支持,而一些比较生僻的,或者是不那么推荐的写的写法则不做支持,转而以 eslint 插件的方式,提示用户进行修改
具体地,JSX限制如下:
- 不支持 动态组件
- 不能在包含 JSX 元素的
map
循环中使用if
表达式 - 不能使用
Array#map
之外的方法操作 JSX 数组 - 不能在 JSX 参数中使用匿名函数
- 不允许在 JSX 参数(props)中传入 JSX 元素
- 只支持class组件
- 暂不支持在
render()
之外的方法定义 JSX - 不能在 JSX 参数中使用对象展开符
- 不支持无状态组件(函数式组件)
-
props.children
只能传递不能操作
对于这些转换限制,弥补性方案是Lint检查报错,并提供替代方案
除JSX外,还有2点比较大的限制:
-
CSS:受限于ReactNative的CSS支持程度(只能使用flex布局)
-
标签:约定 不要使用 HTML 标签(都用多端适配过的内置组件,如
View
、Button
)
P.S.囿于静态转换自身的限制,很多转换是没办法实现的
七.应用场景
当业务要求同时在不同的端都要求有所表现的时候,针对不同的端去编写多套代码的成本显然非常高
也就是说,当同一业务在多端有重叠需求时,Taro之类的多端代码转换方案才有意义
另一类场景是Taro最初想要解决的微信小程序开发体验问题,如果用Taro来开发微信小程序,一不小心还能适配多端,也是个不错的选择