結合趣店 FED 在過去小半年的實踐經驗,我們開發了首個 Taro 三端統一應用:taro-yanxuan(高仿網易嚴選微信小程序),用以探討 Taro 多端開發的正確姿勢。
趣店 FED 早在去年 10 月份就已全面使用 Taro 框架開發小程序(當時版本為 1.1.0-beta.4),至今也上線了 2 個微信小程序、2 個支付寶小程序。
之所以選用 Taro,解決微信小程序原生開發的痛點是一方面,另一方面團隊也有多端統一開發的訴求,Taro 無疑是當時支持最好的。另外 React 也符合團隊的整體技術棧,可顯著降低團隊學習成本。
可以說,Taro 在小程序端、H5 端支持程度已經不錯,也有不少上線實例可以查看,但在 React Native 的支持上,Github 中公開的項目在 RN 這塊均未適配:
這種現況可以理解,畢竟要做到多端統一是有一定難度的,需準確把握各端差異,并做出合理取舍,而 Taro 雖以多端為設計目標,可重心在小程序端,沒有對多端做出一定的開發約束,無從下手也便正常。筆者曾在 2018 iWeb 峰會 - 廈門站做過《多端統一開發實踐》的分享,提到用 Taro 開發 RN 端的坑與大體思路,并加以實踐。
結合趣店 FED 在過去小半年的實踐經驗,我們開發了首個 Taro 三端統一應用:taro-yanxuan(高仿網易嚴選微信小程序),用以探討本文的重點:Taro 開發多端應用的正確姿勢。
在線預覽
可在線預覽 H5、RN 端(直接調用了網易嚴選接口,若要體驗登錄、購物車功能,請使用網易郵箱賬號登錄):
在線預覽:
如下是 React Native 的運行截圖:
樣式管理
樣式管理是多端開發的首要挑戰,因為 React Native 與一般 Web 樣式支持度差異較大,上述幾個未適配 RN 的多端項目多數已栽在樣式上了,用到了大量 RN 不支持的樣式,這種情況再要去兼容 RN 無異于重寫頁面,想必也是有心無力了。這也是本文所強調的,需把握正確的多端開發姿勢。
樣式上 H5 最為靈活,小程序次之,RN 最弱,統一多端樣式即是對齊短板,也就是要以 RN 的約束來管理樣式,同時兼顧小程序的限制,核心可以用三點來概括:
使用 Flex 布局
在進一步闡述之前,需先了解 RN 端幾個影響樣式方案的主要差異:
使用 Flex 布局,不單單是因為 RN 的 View 標簽有默認樣式 display: flex; flex-: column,更重要的是 Flex 可以解決幽靈空白問題:
// View 標簽高度不會是 100px,圖片下方會有幾像素空白,稱為幽靈空白
const imgStyle = { height: '100px' }
<View>
?<Image src={...} style={imgStyle}
View>
常規解決方案是在 View 標簽上設置 font-size / line-height: 0, 或 Image 標簽 display: inline-block 等,但這些在 RN 中都不支持,給 View 標簽設置 display: flex 算是唯一可靠方案了。
何況 Flex 布局能力強大,為啥不用呢?只需要注意一點,RN 中 View 標簽默認主軸方向是 column,如果不將其他端改成與 RN 一致,就需要在所有用到 display: flex 的地方都顯式聲明主軸方向。
基于 BEM 寫樣式
RN 實際上只支持一種樣式聲明方式,即聲明 style 屬性:
const viewStyle = { height: '100%' }
<View style={viewStyle}
這也導致 Taro 在 RN 端基本只支持 class 選擇器這一種寫法(最終編譯成對象字面量),BEM(Block Element )在此處就恰如其分的發揮了作用:
例如每行 2 個元素的列表,每行最后 1 個元素有特定樣式,用偽元素選擇器 :nth-child(even) 很容易實現,在 RN 中就需要自行計算了:
{list.map((item, index) => (
?<View className={classNames('block__element',
? ?index % 2 === 1 && 'block__element--even'
?)} />
)}
基于 BEM 寫 class 樣式,不依賴其他選擇器,雖然會讓代碼稍顯繁瑣,但也能保證多端都是行得通的,不存在支持問題。
采用 style 屬性覆蓋組件樣式
小程序、RN 在頁面、組件間傳遞樣式時均有問題:
// 目前 Taro RN 端還未實現往組件傳遞 className 對應樣式
<CompA compClass='my-style' />
// CompA,樣式不生效
<View className={this.props.compClass} />
上述場景小程序雖可通過組件外部樣式 實現,但官網文檔有強調 “在同一個節點上使用普通樣式類和外部樣式類時,兩個類的優先級是未定義的,因此最好避免這種情況”;用全局樣式倒是可以,但這樣樣式就不好維護了。
那么,通過 style 傳遞、覆蓋組件樣式也就成了唯一可選方案了。需要注意一點,樣式文件是會經過編譯處理兼容多端的,但 style 方式需要運行時兼容:
style={postcss({ background: '#fff' })} />
// 簡單演示,如 RN 不支持 background,需改成 background-color
function postcss(style) {
?const { background, ...restStyle } = style
?const newStyle = {}
?if (background) {
? ?newStyle.backgroundColor = background
?}
?return { ...newStyle, ...restStyle }
}
從這個角度看,styled- 或許是多端開發的最佳樣式方案,然而 Taro 還不支持,且微信小程序官方文檔中也提到 “盡量避免將靜態的樣式寫進 style 中,以免影響渲染速度”,全部樣式都用寫到 style 中恐怕就不靠譜了,但只用來覆蓋少量樣式不見得會有太大影響。
樣式兼容
即便是把握了如上樣式管理思路,多端樣式差異的問題依然存在,例如 white-space: nowrap 這個樣式在 RN 端會報錯,Taro 有提供解決方案:
.text {
?/*postcss-pxtransform rn eject enable*/
?white-space: nowrap;
?/*postcss-pxtransform rn eject disable*/
}
但項目中不止一處會有這個問題,都這樣寫實在不太美觀,可以用 Sass mixins 稍微封裝下:
@mixin eject($attr, $value) {
?/*postcss-pxtransform rn eject enable*/
?#{$attr}: $value;
?/*postcss-pxtransform rn eject disable*/
}
.text {
?@includes eject(white-soace, nowrap);
}
Sass mixins 并不能解決差異,但對于部分各端不兼容的樣式,通過 Sass mixins 統一處理是比較合理的方式,代碼相對美觀也方便維護。
端能力差異
相較于樣式,端能力的差異倒是還好,各端差異是客觀存在的,更不用說 RN 在 iOS 與 Android 上就已存在大量差異。
應對端能力差異,要么改變實現思路,例如 RN 端還不支持 Taro.(get/set),那就改用 async / await + Taro.(get/set)Storage 實現,要么就得使用環境判斷方式了。
Taro 提供 process.env. 用于環境判斷,多數小的差異都可以用這種方式來解決:
function foo() {
?if (process.env.TARO_ENV === 'weapp') {
? ?// 微信小程序邏輯
?}
?if (process.env.TARO_ENV === 'h5') {
? ?// H5 邏輯
?}
?if (process.env.TARO_ENV === 'rn') {
? ?// RN 邏輯
?}
}
這個時候也比較考驗開發者的封裝能力了,一般是建議將這些差異邏輯的判斷統一起來,例如在 src/utils 中進行封裝,對外提供一致的接口,盡量不要在業務頁面中雜糅太多的判斷。
而對于簡單的環境判斷處理不了的問題,就只能動用原生開發了,例如 Taro 還不支持 RN 端的 WebView 組件,就需要自己用原生 RN 實現:
// Taro 頁面,根據環境引入 RN 原生頁面
import { WebView } from '@tarojs/components'
const WebViewRN = process.env.TARO_ENV === 'rn' ? require('./rn').default : null
export default class extends Component {
?render() {
? ?return process.env.TARO_ENV === 'rn' ?
? ? ?<WebViewRN src={this.url} /> :
? ? ?<WebView src={this.url} />
?}
}
// 原生 RN 頁面,從 react-native 引入 WebView
import Taro, { Component } from '@tarojs/taro'
import { WebView } from 'react-native'
export default class WebViewRN extends Component {
?render() {
? ?const source = { uri: this.props.src }
? ?return <WebView source={source} />
?}
}
process.env. 的處理是編譯時而不是運行時,也就是說若不是編譯 RN,上述用原生寫的 RN 頁面不會被打包,保證了編譯成其他端時不會引入不支持的內容。
原生頁面能夠引入,多端問題也就有了基本的實現保障。
Taro RN 端的坑
Taro RN 端目前小問題還是不少的,本項目開發過程中也順帶解了幾個 bug:
除此之外還有好幾個問題,時間關系還未提 pr 解決,暫且先繞過,但其中有兩個坑還是值得一說的。
onClick
RN 的 View 標簽不支持 onClick ,但這又是很通常的需求,原生解決方式是套一層 組件,如:
onPress={this.handlePress}>
? {...}
而 Taro 是引入 響應用戶操作:
? ?{...PanResponder.carete({ ...})}
? ?style={wrapperStyle}
>
? ? style={innerStyle} />
問題在于這樣多嵌套了一層 View,并把樣式拆分成 、 分別應用,但樣式拆分有問題,導致綁定 onClick 之后元素的樣式錯亂了,這點在開發過程中還是相當坑的。
寬高自適應
onClick 的問題也還好,改改樣式能繞過去,寬高自適應的坑就比較尷尬了。
小程序、H5 可用 rpx / em 實現自適應,而 RN 的自適應方案麻煩些,一般需通過 獲取寬高再進行換算。Taro.() 可解決該問題,但編譯 RN 端樣式文件時并沒有考慮這點,即 width: 100px 會被編譯成 width: 50,而不是 width: Taro.(100),無法適配屏幕不同的屏幕尺寸。
因此,目前 Taro RN 端還不好做到自適應,要么非百分比的寬高都用 style + Taro.(),要么就得自己寫個腳本去處理編譯后的樣式文件。
這兩個問題都提了 issue 2204 2205,有需要的可以關注下解決進度
其他
要做到多端統一,能說的細節點實在太多,上述實現思路雖然簡單,但背后也都是隱含著對各端差異的斗爭與取舍,本文也僅是列出最基本的幾點,用于闡述 Taro 多端開發的核心思路。
本項目代碼沒有做過多封裝,方便閱讀,也實現了足夠多的樣式細節進行踩坑,具體涉及的踩坑點、注意事項都在代碼中以注釋 // TODO(Taro 還未支持的)、// NOTE(開發技巧、注意事項)注明了,更多內容就有待各位去實踐、體會了。
總結
如前言所說,Taro 雖然是以多端為設計目標,但重心是小程序端,RN 端目前的支持情況不算特別理想。但充分理解多端差異、掌握正確的多端開發姿勢(特別是樣式管理方面,避免項目成型后再去兼容需要大動刀斧)之后,在簡單的項目上是完全可以一展拳腳的。
若說 2 個禮拜開發一個小程序,是稀疏平常的事,但 2 個禮拜即搞定了小程序端(微信、支付寶、百度等等),還搞定了 H5、React Native 端,后續更新也只要改一處地方,這產出、維護效率就實在太驚人了,這大抵也就是 “Write once, run ” 的魅力所在(雖然在前端領域極容易發展成 “Write once, debug ” )
相信隨著小程序熱度不斷上升,還會有更多優秀的開源框架、解決方案涌現。而我們不傾向于造輪子,更關注基于現有方案如何更好地去開發多端應用。
項目開源地址: