背景
Taro 是一套遵循 React 語法規(guī)范的 多端開發(fā) 解決方案∽現(xiàn)如今市面上端的形態(tài)多種多樣,Web、React-Native囊骤、微信小程序等各種端大行其道晃择,當(dāng)業(yè)務(wù)同時在不同的端都要求有所表現(xiàn)的時候,針對不同的端編寫多套代碼的成本顯然非常高也物,這時只編寫一套代碼就能適配到多端的能力就顯得極為重要宫屠。
使用 Taro,我們只需書寫一套代碼滑蚯,再通過 Taro 的編譯工具浪蹂,即可將源代碼分別編譯出在不同端(微信/百度/支付寶/字節(jié)跳動小程序、快應(yīng)用告材、H5坤次、React-Native 等)運行的代碼。
58租房-創(chuàng)新找房業(yè)務(wù)有上線三端并高度一致的需求斥赋,為了節(jié)約開發(fā)人力和創(chuàng)新缰猴,我們在該項目中探索性的嘗試使用Taro進(jìn)行開發(fā),下面將從 Taro框架編譯原理灿渴、源碼改造洛波、依賴改造、業(yè)務(wù)開發(fā)實踐過程幾個角度來分別闡述一下骚露。
Taro框架編譯原理
因為H5和React-Native都可以遵循React語法規(guī)范開發(fā)蹬挤,在 React 中,是使用 JSX 來作為組件的模板的棘幸,而小程序則與 Vue 一樣焰扳,是使用字符串模板的。這樣兩者之間就有著巨大的差異了误续。
JSX
render () {
return (
<View className='index'>
{this.state.list.map((item, idx) => (
<View key={idx}>{item}</View>
))}
<Button onClick={this.jump}>跳轉(zhuǎn)</Button>
</View>
)
}
小程序模板
<view class="index">
<view wx:key={idx} wx:for="{{list}}" wx:for-item="item" wx:for-index="idx">{{item}}</view>
<view bindtap="jump">跳轉(zhuǎn)</view>
</view>
那么這個時候我們就想吨悍,要是能夠?qū)?JSX 編譯成小程序模板就好了。
事實上在我們平時的開發(fā)中蹋嵌,這種編譯的操作到處可見育瓜,babel 就是我們最常用的 JS 代碼編譯器,一般瀏覽器是不能支持一些非常新的語法特性的栽烂,但我們又想使用它們躏仇,這個時候就可以借助 babel 來將我們的高版本的 ES 代碼,編譯成瀏覽器可以運行的 ES 代碼腺办。而我們像要將 JSX編譯成小程序模板焰手,也是同樣的道理。我們首先來了解一下 Babel 的運行機(jī)制怀喉。
Babel 作為一個 代碼編譯器 书妻,能夠?qū)?ES6/7/8 的代碼編譯成 ES5 的代碼,其核心利用的就是計算中非彻#基礎(chǔ)的編譯原理知識躲履,將輸入語言代碼见间,通過編譯器執(zhí)行,輸出目標(biāo)語言的代碼崇呵。編譯原理的一般過程就是缤剧,輸入源程序,經(jīng)過詞法分析域慷、語法分析荒辕,構(gòu)造出語法樹,再經(jīng)過語義分析犹褒,理解程序正確與否抵窒,再對語法樹做出需要的操作與優(yōu)化,最終生成目標(biāo)代碼叠骑。
將 JSX 編譯成小程序模板李皇,非常幸運的是 babel 的核心編譯器 babylon 是支持對 JSX 語法的解析的,我們可以直接利用它來幫我們構(gòu)造 AST宙枷,而我們需要專注的核心就是如何對 AST 進(jìn)行轉(zhuǎn)換操作掉房,得出我們需要的新 AST,再將新 AST 進(jìn)行遞歸遍歷慰丛,生成小程序的模板卓囚。
源碼改造
我們是基于Taro1.3.10版本開發(fā)的,為了使Taro兼容58RN工程我們對源碼做了以下改造:
taro腳手架taro-cli
- 升級內(nèi)部react-native版本"react-native": "0.57.8"
- 固定注冊包名
AppRegistry.registerComponent
為wuba
taro依賴庫taro-rn
1.刪除Expo相關(guān)依賴的api诅病,保證依賴純凈
taro依賴庫taro-router-rn
修改initRouter
中基于react-navigation
的初始化參數(shù)哪亿,使其保證和58RN跳轉(zhuǎn)方式統(tǒng)一
為了便于版本管理,我們將taro關(guān)鍵字轉(zhuǎn)換為fangchan-taro贤笆,并修改其依賴樹蝇棉,fangchan-taro托管于npm,后續(xù)陸續(xù)改造了taro-cli、taro-rn、taro-rn-component等依賴卓箫,我們把Taro下的依賴統(tǒng)一進(jìn)行管理,置于同一個組織贴唇,標(biāo)記為公開,形如:
@fangchan/taro-rn飞袋、@fangchan/taro-cli
圖上是Taro開發(fā)RN端的工作流程,通過執(zhí)行taro build --type rn --watch
指令將Taro編譯成RN代碼链患,并開啟metro server將rn_temp
下的js文件打包成js bundle
巧鸭,通過npm start
開啟服務(wù)后就可以在58rn測試載體頁上愉快的訪問了。
依賴改造
Taro提供的組件和Api相對比較基礎(chǔ)麻捻,通用性更強(qiáng)纲仍,但是58移動端并沒有對應(yīng)的sdk支持呀袱,所以我們暫時去掉了不支持的api,并在有必要的情況下進(jìn)行重寫郑叠。
例如taro-rn中的請求Request
是必要的API夜赵,我們對其改造,讓它內(nèi)部調(diào)用房產(chǎn)SDK的請求乡革,保證請求內(nèi)部流程一致寇僧。
import HMS from 'house-middleware-sdk'
function request (options) {
options = options || {}
let url = options.url
let data = options.data || {}
if (typeof options === 'string') {
options = {
url: options
}
}
let method = options.method || 'GET'
method = method.toUpperCase()
if (method === 'GET') {
return HMS.get(url, data)
}
if (method === 'POST') {
const formData = new FormData()
Object.keys(data).forEach(key => {
formData.append(key, data[key])
})
return HMS.post(url, formData)
}
}
export default {
request
}
房產(chǎn)業(yè)務(wù)中使用到的業(yè)務(wù)組件如篩選、底部欄等沸版,不同端提供的Api如登錄嘁傀、認(rèn)證等需要我們自行開發(fā),我們把開發(fā)分為兩個階段:
- 第一階段為了業(yè)務(wù)快速遷移上線视粮,盡量復(fù)用各端已有組件细办,使用條件編譯完成各端打包
- 第二階段對于通用性較強(qiáng)的組件、基于Taro封裝各端API和組件庫提高開發(fā)效率蕾殴、降低維護(hù)成本
目前我們處于第一階段笑撞,使用Taro提供的條件編譯來處理不同端的差異
if (process.env.TARO_ENV === 'weapp') {
// 微信小程序端執(zhí)行邏輯
} else if (process.env.TARO_ENV === 'h5') {
// h5 端執(zhí)行邏輯
} else if (process.env.TARO_ENV === 'rn') {
// react-native 端執(zhí)行邏輯
}
業(yè)務(wù)開發(fā)實現(xiàn)
1.項目初始化
我們通過改造的cli來快速構(gòu)建項目fangchan-taro init TaroDemo
,構(gòu)建好后項目目錄結(jié)構(gòu)如下
├── dist 編譯結(jié)果目錄
├── config 配置目錄
| ├── dev.js 開發(fā)時配置
| ├── index.js 默認(rèn)配置
| └── prod.js 打包時配置
├── src 源碼目錄
| ├── pages 頁面文件目錄
| | ├── index index 頁面目錄
| | | ├── index.js index 頁面邏輯
| | | └── index.css index 頁面樣式
| ├── app.css 項目總通用樣式
| └── app.js 項目入口文件
└── package.json
我們首先需要修改入口文件钓觉,在構(gòu)造中初始化全局參數(shù)茴肥,在全局配置config
中定義頁面以及設(shè)置小程序?qū)Ш綑诤驮O(shè)置rn特殊返回鍵處理等操作。
class App extends Component {
constructor(props) {
super(props);
initGlobalConst();
}
config = {
pages: [
'pages/index/index',
'pages/listpage/index',
'pages/filterpage/index',
'pages/guidepage/index',
'pages/demandpage/index',
],
window: {
backgroundTextStyle: 'light',
navigationBarBackgroundColor: '#fff',
navigationBarTitleText: 'WeChat',
navigationBarTextStyle: 'black',
},
backStackConfig: {
ctrlFun: process.env.TARO_ENV === 'rn' ?
require('./utils/initRN').ctrlBackStackFun : false
}
};
render() {
return (
<Index/>
)
}
}
Taro.render(<App/>, document.getElementById('app'));
然后在'src/index/index.js'中的componentWillMount
生命周期處理多端的初始化操作
componentWillMount() {
process.env.TARO_ENV === 'rn' && initRN((goal) => {
this.renderPage();
});
process.env.TARO_ENV === 'h5' && initH5((goal) => {
this.renderPage();
});
process.env.TARO_ENV === 'weapp' && initH5((goal) => {
this.renderPage();
});
}
以RN端為例议谷,需要在initRN
方法中從native端獲取header信息炉爆、跳轉(zhuǎn)協(xié)議等,所以這一步是必不可少的卧晓。
function initRN(callback) {
HMS.initPackage(
() => HMS.initNativeParams(
() => {
const cacheFlag = global.jumpParams.content.params.useCache || true;
HMS.CacheUtil.setUseCache(cacheFlag);
upDateGlobalConst('PACKAGE', global.CURRENT_PACKAGE);
upDateGlobalConst('FULL_PATH', global.jumpParams.content.params.full_path);
handleTargetPageType(callback);
}
)
);
}
2. 頁面間跳轉(zhuǎn)
我們只需要在入口文件的 config 配置中指定好 pages芬首,然后就可以在代碼中通過 Taro 提供的 API 來跳轉(zhuǎn)到目的頁面,例如:
// 跳轉(zhuǎn)到目的頁面逼裆,打開新頁面
Taro.navigateTo({
url: '/pages/page/path/name'
})
// 傳入?yún)?shù) id=2&type=test
Taro.navigateTo({
url: '/pages/page/path/name?id=2&type=test'
})
3. 狀態(tài)管理
為了減少學(xué)習(xí)成本郁稍,我們沿用RN端使用的狀態(tài)管理機(jī)制mobx進(jìn)行狀態(tài)管理,在Taro端使用方式和RN端完全一致胜宇。這樣也方便已有的RN項目后面能快速遷移到Taro耀怜。
4.樣式管理
樣式管理是多端開發(fā)的一大挑戰(zhàn),因為 React Native 與一般 Web 樣式支持度差異較大桐愉。樣式上 H5 最為靈活财破,小程序次之,RN 最弱从诲,統(tǒng)一多端樣式即是對齊短板左痢,也就是要以 RN 的約束來管理樣式,同時兼顧小程序的限制。
不過這也正巧適用于我們團(tuán)隊俊性,因為我們對RN樣式控制比較熟略步,所以我們并沒有采用scss的方式,而是沿用編寫RN樣式的方式定页,完全使用style來編寫Taro樣式趟薄。
<View
style={{
display: 'flex',
position:'relative',
width: Taro.pxTransform((global.WINDOW_WIDTH_VALUE - 30) * 2),
height: Taro.pxTransform(130)
flexDirection: 'column',
backgroundColor: '#EAEAEA'
}}
/>
其中有幾點特殊需要注意:
1.因為RN一般需通過Dimensions 獲取寬高再進(jìn)行換算,Taro提供的 pxTransform() 可解決該問題典徊,但編譯 RN 端樣式文件時并沒有考慮這點杭煎,
所有數(shù)字必須在style中用Taro.pxTransform()包裝,且單位是px。
- 在開發(fā)中必須聲明flex布局和排版宫峦,因為RN和小程序H5默認(rèn)橫縱不一樣岔帽。
- position: 'absolute'需要在外層父view設(shè)置position: 'relative'。
- 覆蓋組件樣式可以通過style傳遞导绷,但style不支持?jǐn)?shù)組犀勒。
最終看一下實現(xiàn)的效果還是能保證高度統(tǒng)一的。
開發(fā)中遇到的問題
1.語法問題
由于微信小程序端的限制妥曲,有一些jsx用法不能得到很好地支持贾费,比如不能使用 Array#map 之外的方法操作 JSX 數(shù)組、暫不支持在 render() 之外的方法定義 JSX檐盟、不能在 JSX 參數(shù)中使用對象展開符褂萧、不支持無狀態(tài)組件等。
2.房產(chǎn)組件庫依賴問題
因為組件庫中使用了es6語法葵萎,且部分組件有mobx的引用导犹,直接依賴會有不兼容的問題,所以將其統(tǒng)一用babel轉(zhuǎn)成es5格式打包到lib中引用羡忘。
3.跨域問題
RN不存在跨域問題谎痢,小程序網(wǎng)絡(luò)請求需要在小程序設(shè)置中配置,H5存在跨域的問題卷雕,這個可通過 devServer.proxy 解決节猿,以及編譯打包的靜態(tài)資源是固定文件名,建議改成帶 hash 值方便緩存管理漫雕,這些配置在項目里的 src/config 中都能找到滨嘱。
總結(jié)
本文從一個RN開發(fā)者角度來進(jìn)行Taro跨平臺項目實踐,其中在狀態(tài)管理及樣式管理上都采用了和RN一樣的機(jī)制浸间,一是為了組內(nèi)同學(xué)可以快速上手開發(fā)太雨,二是為了將已有rn項目能快速平移到Taro。但是對H5和小程序兩端掌握的能力有限魁蒜,需要實踐積累囊扳。
目前由于組件庫煤墙、sdk積累較少,前期開發(fā)業(yè)務(wù)的同時要同時進(jìn)行業(yè)務(wù)開發(fā)宪拥、底層封裝、腳手架調(diào)整铣减,業(yè)務(wù)開發(fā)速度因此受到影響她君,粗略的估算是RN同等業(yè)務(wù)開發(fā)時間的2倍左右。預(yù)計未來理想情況下葫哗,基于完整的組件庫缔刹、sdk、以及標(biāo)準(zhǔn)化的協(xié)議可以抹平系統(tǒng)劣针、平臺校镐、端之間的大多數(shù)差異,開發(fā)效率趨近于目前RN情況捺典。
小程序各端統(tǒng)一技術(shù)棧到Taro后鸟廓,兩方理論上可以做到無縫銜接,底層資源共享襟己、復(fù)用引谜,這將顯著提升開發(fā)效率,后續(xù)我們在推動四端邏輯擎浴、協(xié)議統(tǒng)一的同時员咽,會加強(qiáng)組間溝通,與FE同學(xué)共同促進(jìn)通用層優(yōu)化贮预,使業(yè)務(wù)開發(fā)者可以專注于業(yè)務(wù)本身贝室。