一、目標定位
一套遵循 React 語法規範的多端統一開發框架
一種多端代碼轉換方案,這裡的「端」是指微信小程序、Web、ReactNative、百度小程序、支付寶小程序、頭條小程序、快應用等等
具體地,把一份類 React 源碼,通過「編譯」轉換成兼容目標端的形式,即:
轉換
nerv 業務代碼 ------> xx 小程序業務代碼 +
Web 業務代碼 +
ReactNative 業務代碼
目的是降低開發成本,提高效率:
讓原本只能運行在一端的項目獲得多端運行的能力,降低開發者的重構成本。
二、思路探索
初衷
用 React 寫微信小程序。
微信小程序原生方式開發起來 太費勁,遂想用 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 部分組成:
-
配置(JSON)
-
模板(WXML)
-
樣式(WXSS)
-
邏輯(JS)
配置與樣式沒什麼好說的,難點在於模板的轉換和邏輯的轉換
P.S. ReactNative 樣式轉換另說,也是一個難題,因為 RN 在選擇器、屬性名/值及默認值,甚至 CSS 特性支持程度都存在較大差異
編譯轉換
要把一份代碼 A 轉換成另一份代碼 B,需要做 3 件事情:
-
解析代碼 A 生成抽象描述(AST)
-
根據一些映射規則操作 AST,生成新的 AST
-
根據新的 AST 生成代碼 B
P.S. 關於編譯轉換的更多信息,請查看 再看編譯原理 與 [Babel 快速指南](/articles/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 預處理等
-
構建:Web 走 Webpack,ReactNative 走 Expo 的 xdl,其餘的各自走自己的 IDE
-
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 來開發微信小程序,一不小心還能適配多端,也是個不錯的選擇

暫無評論,快來發表你的看法吧