Taro 多端開發的正確姿式:打造三端統一的網易嚴選(小程序、H五、React Native)

前言

筆者所在的趣店 FED 早在去年 10 月份就已全面使用 Taro 框架開發小程序(當時版本爲 1.1.0-beta.4),至今也上線了 2 個微信小程序、2 個支付寶小程序。css

之因此選用 Taro,解決微信小程序原生開發的痛點是一方面,另外一方面團隊也有多端統一開發的訴求,Taro 無疑是當時支持最好的。另外 React 也符合我的及團隊的總體技術棧,可顯著下降團隊學習成本。html

能夠說,Taro 在小程序端、H5 端支持程度已經不錯,也有很多上線實例能夠查看,但在 React Native 的支持上,Github 中公開的項目在 RN 這塊均未適配:前端

這種現況能夠理解,畢竟要作到多端統一是有必定難度的,需準確把握各端差別,並作出合理取捨,而 Taro 雖以多端爲設計目標,可重心在小程序端,沒有對多端作出必定的開發約束,無從下手也便正常。筆者曾在 2018 iWeb 峯會 - 廈門站作過《多端統一開發實踐》的分享,提到用 Taro 開發 RN 端的坑與大致思路,並加以實踐。react

結合趣店 FED 在過去小半年的實踐經驗,咱們開發了首個 Taro 三端統一應用:taro-yanxuan(高仿網易嚴選微信小程序),用以探討本文的重點:Taro 開發多端應用的正確姿式。git

相關代碼已開源:github.com/js-newbee/t…github

在線預覽

小程序端已支持微信小程序、支付寶小程序,但沒法提供在線版,請 clone 代碼本地運行。小程序

H5 端、RN 端可在線預覽(直接調用了網易嚴選接口,若要體驗登陸、購物車功能,請使用網易郵箱帳號登陸):微信小程序

小程序 H5 - 訪問連接 React Native
請 clone 代碼本地運行
H5
Expo Snacks

以下是 React Native 的運行截圖:react-native

首頁、分類 詳情、加購物車 購物車、我的
首頁、分類
二級分類、詳情
購物車、我的

樣式管理

樣式管理是多端開發的首要挑戰,由於 React Native 與通常 Web 樣式支持度差別較大,上述幾個未適配 RN 的多端項目多數已栽在樣式上了,用到了大量 RN 不支持的樣式,這種狀況再要去兼容 RN 無異於重寫頁面,想必也是有心無力了。這也是本文所強調的,需把握正確的多端開發姿式。跨域

樣式上 H5 最爲靈活,小程序次之,RN 最弱,統一多端樣式便是對齊短板,也就是要以 RN 的約束來管理樣式,同時兼顧小程序的限制,核心能夠用三點來歸納:

  • 使用 Flex 佈局
  • 基於 BEM 寫樣式
  • 採用 style 屬性覆蓋組件樣式

使用 Flex 佈局

在進一步闡述以前,需先了解 RN 端幾個影響樣式方案的主要差別:

  • display 只有 flex / noneposition 只有 relative / absolute
  • 不支持標籤選擇器、子代選擇器、僞元素,不支持 background: url() 等;
  • 文本要用 Text 標籤包裹,文本的樣式不能加在 View 標籤上,只能加在 Text 標籤上。

使用 Flex 佈局,不僅僅是由於 RN 的 View 標籤有默認樣式 display: flex; flex-direction: column,更重要的是 Flex 能夠解決幽靈空白問題:

// View 標籤高度不會是 100px,圖片下方會有幾像素空白,稱爲幽靈空白
<View>
  <Image src={...} style={{ height: '100px' }} </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 屬性:

<View style={{ height: '100%' }}
複製代碼

這也致使 Taro 在 RN 端基本只支持 class 選擇器這一種寫法(最終編譯成對象字面量),BEM(Block Element Modifier)在此處就恰如其分的發揮了做用:

  1. 避免樣式衝突(RN、小程序樣式獨立,但 H5 不是)
  2. 自解釋、語義化

例如每行 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} />
複製代碼

上述場景小程序雖可經過組件外部樣式 externalClasses 實現,但官網文檔有強調 "在同一個節點上使用普通樣式類和外部樣式類時,兩個類的優先級是未定義的,所以最好避免這種狀況";用全局樣式卻是能夠,但這樣樣式就很差維護了。

那麼,經過 style 傳遞、覆蓋組件樣式也就成了惟一可選方案了。須要注意一點,樣式文件是會通過編譯處理兼容多端的,但 style 方式須要運行時兼容:

<Comp 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-components 或許是多端開發的最佳樣式方案,然而 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-space, nowrap);
}
複製代碼

Sass mixins 並不能解決差別,但對於部分各端不兼容的樣式,經過 Sass mixins 統一處理是比較合理的方式,代碼相對美觀也方便維護。

端能力差別

相較於樣式,端能力的差別卻是還好,各端差別是客觀存在的,更不用說 RN 在 iOS 與 Android 上就已存在大量差別。

應對端能力差別,要麼改變實現思路,例如 RN 端還不支持 Taro.(get/set)StorageSync,那就改用 async / await + Taro.(get/set)Storage 實現,要麼就得使用環境判斷方式了。

Taro 提供 process.env.TARO_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 v1.2.16 已支持):

import { WebView } from '@tarojs/components'
// Taro 已開啓 Tree shaking,能夠放心引入各端組件
// 未使用的內容在編譯時會被自動去掉
import WebViewRN from './rn'

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() {
    return <WebView source={{ uri: this.props.src }} />
  }
}
複製代碼

process.env.TARO_ENV 的處理是編譯時而不是運行時,且 Taro 引入了 Tree shaking,也就是說若不是編譯 RN,上述用原生寫的 WebViewRN 不會被打包,保證了編譯成其餘端時不會引入不支持的內容(不然在非 RN 端引用 react-native 會報錯)。

原生頁面可以引入,多端問題也就有了基本的實現保障。

不過上述方式會令代碼中充斥着大量 process.env.TARO_ENV,仍是不夠理想的,3 月中旬發佈的 Taro v1.2.17 提供了一種更方便的跨平臺開發方式,更適合用於多端兼容:

// 例如原先有一個組件 test.js
// 若須要分別實現 h5 端、RN 端的組件,則將相應組件命名爲:
// test.h5.js,test.rn.js
// 這樣只須要引入 test,Taro 會根據環境自動引入相應的組件
// 就不須要寫 process.env.TARO_ENV 的判斷了
import Test from '../../components/test'

render() {
    // 例如編譯 h5 時,實際引入的組件是 test.h5.js
    // 組件只須要遵循對外接口統一便可
    return <Test data={data} onClick={onClick} /> } 複製代碼

Taro RN 端的坑

Taro RN 端目前小問題仍是很多的,本項目開發過程當中也順帶解了幾個 bug:

給 Taro 提的 pr

除此以外還有好幾個問題,時間關係還未提 pr 解決,暫且先繞過,但高度自適應這個坑仍是值得一說的。

小程序、H5 可用 rpx / em 實現自適應,而 RN 的自適應方案麻煩些,通常需經過 Dimensions 獲取寬高再進行換算。Taro 提供的 pxTransform() 可解決該問題,但編譯 RN 端樣式文件時並無考慮這點,即 width: 100px 會被編譯成 width: 50,而不是 width: Taro.pxTransform(100),也就沒法適配不一樣的屏幕尺寸。

所以,目前 Taro RN 端還很差作到自適應,要麼非百分比的寬高都用 style + Taro.pxTransform(),要麼就得本身寫個腳本去處理編譯後的樣式文件。

這個問題已提了 issue 2204,有須要的能夠關注下解決進度

Taro H5 端的坑

Taro 對 H5 端的支持度尚可,若僅僅想要實現兼容小程序與 H5,也仍建議採用 BEM 寫樣式 + style 屬性覆蓋組件樣式的方案,能夠有效規避小程序自定義組件的諸多侷限,只是在 CSS 特性上就不用像 RN 那樣拘束,transition、僞元素等使用起來無壓力。

另外就是小程序、RN 都沒有跨域問題,但 H5 會有,這個可經過 devServer.proxy 解決,以及編譯打包的靜態資源是固定文件名,建議改爲帶 hash 值方便緩存管理,這些配置在項目裏的 src/config 中都能找到,就再也不復述了。

H5 端的坑更多的是集中在內置組件不夠完善、端能力缺失較多,畢竟 Taro 的設計是以微信小程序爲基準,去補充其餘端的差別,編譯成小程序就是直接用的小程序內置組件,但在 H5 端就須要一整套功能對等的內置組件,Taro 要作到一致所需的繁雜細節也可想而知。

舉一個比較明顯的坑來講,就是還不支持 Taro.switchTab(),暫時只能用以下方式先繞過:

if (process.env.TARO_ENV === 'h5') {
  Taro.navigateBack({ delta: Taro.getCurrentPages().length - 1 })
  setTimeout(() => { Taro.redirectTo({ url }) }, 100)
}
複製代碼

好在官方已計劃在接下來的 1.3 版本重構 H5 TabBar,到時這個問題也就解決了。

其餘

要作到多端統一,能說的細節點實在太多,上述實現思路雖然簡單,但背後也都是隱含着對各端差別的鬥爭與取捨,本文也僅是列出最基本的幾點,用於闡述 Taro 多端開發的核心思路。

本項目代碼沒有作過多封裝,方便閱讀,也實現了足夠多的樣式細節進行踩坑,具體涉及的踩坑點、注意事項都在代碼中以註釋 // TODO(Taro 還未支持的)、// NOTE(開發技巧、注意事項)註明了,更多內容就有待各位去實踐、體會了。

註釋

ps: Taro 的版本更新速度仍是比較給力的,本項目最先基於 v1.2.11 開發,2 月 19 號發佈時用的是 v1.2.13,到 3 月 11 號已更新到 v1.2.17 版本,筆者會盡可能跟隨版本變更對本文內容、github 代碼作出相應完善。

總結

如前言所說,Taro 雖然是以多端爲設計目標,但重心是小程序端,RN 端目前的支持狀況不算特別理想。但充分理解多端差別、掌握正確的多端開發姿式(特別是樣式管理方面,避免項目成型後再去兼容須要大動刀斧)以後,在簡單的項目上是徹底能夠一展拳腳的。

若說 2 個禮拜開發一個小程序,是稀疏日常的事,但 2 個禮拜即搞定了小程序端(微信、支付寶、百度等等),還搞定了 H五、React Native 端,後續更新也只要改一處地方,這產出、維護效率就實在太驚人了,這大抵也就是 "Write once, run anywhere" 的魅力所在(雖然在前端領域極容易發展成 "Write once, debug everywhere" 😂)

相信隨着小程序熱度不斷上升,還會有更多優秀的開源框架、解決方案涌現。而咱們不傾向於造輪子,更關注基於現有方案如何更好地去開發多端應用。如有興趣的前端小夥伴,不妨加入咱們,一塊兒搞事 caiminxing#qudian.com 😁(base 廈門)

本文由趣店 FED 出品,首發於趣店技術學院;項目開源地址 github.com/js-newbee/t…

相關文章
相關標籤/搜索