React Native 實踐有感

React Native (簡稱 RN) 是 Facebook 於 2015 年開源的移動端跨平臺開發框架。RN 從開源以來已經有 6 個年頭了,有着十分豐富的社區資源和生態,時至今日依然有很多移動端項目都使用 RN 來開發。本文主要通過以往的項目實踐來談談在選擇 RN 開發 app 可能需要注意的一些點,也算是自己的一個踩坑經驗總結。

1. 技術選型 - 是否該用 RN?

跨平臺開發框架都是有侷限性的,這一點 RN 也不例外,RN 本身還是要使用原生 API 來實現 UI 的繪製,JS bridge 的創建和與原生平臺的通信都需要消耗資源,基於這樣的前提,RN 開發的應用相對於原生平臺來說往往會佔用更多的內存和 CPU,因此而出現的卡頓、掉幀的概率也會更高,進而對用戶體驗造成較大的影響。

那麼問題來了,RN 真的很差、不適合工程實踐嗎?

這個問題就涉及到技術選型了,是否應該用 RN?什麼樣的情況下適合使用 RN 作爲首選開發技術?

我個人認爲需要從以下幾個方面考慮:

產品類型和市場定位面向 C 端的產品一般最好還是使用原生開發技術,性能穩定性相對會更加可靠一些,尤其是這款產品的市場期望比較高,對用戶和市場規模增長有比較大的期待時。性能更好、更加穩定可靠的技術應當是首選,這樣會帶來更好的用戶體驗。當然如果用戶數量比較少,app 應用場景比較單一的情況不太需要這樣的考慮,比如功能並不複雜的工具類應用。

業務需要很多 app 都使用原生與 H5 的 Hybrid 模式開發,但是 H5 的體驗跟原生相比差距較大,RN 的體驗比 H5 就要好很多,而且 RN 還具有熱更新的能力,這對於需要頻繁更新內容的業務來說是一個不錯的選擇。比如像圖書、漫畫這種內容上新比較頻繁或者 UI 排版更迭頻繁的更適合用 RN,像以音視頻播放爲主這種追求性能穩定的就不太合適了。

團隊規模和開發週期團隊規模比較小,開發週期短的情況下儘量選擇熟悉的技術棧,能夠節省時間。

技術儲備這一點需要考慮到團隊是否有相應的技術,比如如果團隊沒有 Android 或 iOS 原生開發的技術,都只有 web 前端開發,又需要做 app,那麼可以考慮 RN,尤其是有 React 技術儲備的情況。

後期維護成本這一點一般來說考慮的優先級是最低的,開發團隊可能很少會考慮維護的問題,因爲交付之後項目誰維護、要不要維護都是個問題。作爲跨平臺開發框架來說,RN 通常可能需要維護 Android 和 iOS 兩端,尤其是 app 應用場景和功能比較複雜的情況下,與原生交互的部分就少不了,對於純 web 前端開發來說是個不小的挑戰,需要一個人負責兩個平臺的維護工作。總之,RN 一個開發者維護的情況下,那麼對開發者的要求是需要兼顧 Android 和 iOS 兩個平臺,這也是爲什麼說學了 RN 遲早安卓和 iOS 都要學🤣。如果是原生開發,可能需要兩個人維護,一人一個平臺,就會提高維護成本。

綜上,RN 到底適不適合在項目中實踐,最好按實際情況考慮。我個人覺得 RN 還是不錯的,性能表現由於先天性的架構設計問題與原生有差距是正常的,但是也沒有差到無法用的地步,這一點不能人云亦云。

2. 依賴庫的升級維護

RN 項目中經常會用到很多第三方庫,比如路由框架 react-navigation、數據存儲 AsyncStorage、狀態管理 react-redux 等等。在項目維護時我們可能會面臨第三方庫的升級帶來的一系列問題、某些 library 沒人維護了,但是我們出於某些原因還需要繼續使用等等,針對這些情況談談我的理解。

第三方庫適時升級適時升級的意思就是第三方庫有新版本的時候,在保持 app 穩定性、不引起 regression 問題的情況下儘可能的升級第三方庫。在 app 的迭代中把第三方庫的升級維護考慮進去是很有必要的,以我所在的項目爲例:

我們項目中使用的 react-navigation 版本非常老舊了,還停留在 v2 版本,而最新的 react-navigation 實際已經到了 v5 版本,並且 v5 版本中對核心功能組件進行了拆分,意味着 v5 以後需要安裝 react-navigation 的多個依賴包。react-navigation 一直都是一個 API 變動非常大的 router 庫,每一個大版本的迭代都可能導致原來的路由用法發生改變。對比老舊的 v2 版本來說,升級到新版本是更好的選擇,功能和性能更強、路由靈活性更高,但是在我接手項目之前 react-navigation 一直都沒升級過,直接升級到最新版本變動太大了,風險太高,容易引起功能上的 bug。如果在之前的迭代中能把這塊升級的工作考慮進去,隨着每個迭代一起去做,改動會相對較小,就能平穩過渡到新版本。沒人維護怎麼辦

沒人維護的庫怎麼處理,分幾種情況:

  1. 對功能沒影響的無所謂,比如 react-native-html,我只用它加載一小段 html,它即使不維護了也沒影響,因爲功能已經實現了,後續也無變動;

  2. 跟 app 功能深度捆綁,依賴很強,替換需要很大 effort,這種能繼續維護就儘量自己維護;

  3. 可替代性較強,後續功能迭代還需要依賴它的,這種儘早替換。

RN 版本升級 RN 在 0.59 及之前的版本中只能手動安裝第三方庫,0.60 及以上版本可以 auto link 了,項目的配置簡單了許多,所以最好升級到 0.60 版本以上。

0.63 版本解決了 iOS 13 中本地圖片無法顯示的問題,源於 iOSRCTUIImageViewAnimated中一句代碼[super displayLayer:layer];的缺失導致圖片內容無法正常顯示:

- (void)displayLayer:(CALayer *)layer
{
  if (_currentFrame) {
    layer.contentsScale = self.animatedImageScale;
    layer.contents = (__bridge id)_currentFrame.CGImage ;
  } else { 
    [super displayLayer:layer];
  }
}

從我們的項目來看,升級到 RN 0.63 版本會導致 react-navigation 老版本中的依賴庫 react-native-safe-area-view 報錯。所以連帶的也需要升級 react-navigation,但我上面提到升級 react-navigation 風險比較大,需要比較大的 effort 去做,所以這裏我還是保持 RN 版本小於 0.63,通過react-native-fix-image來修改 iOS 源碼或者用 patch-package 打 patch 實現,做法雖然醜陋了點,但可以最小的 effort 先解決問題,後續再用更穩妥的方式逐步升級 RN 和 react-navigation 版本。

總之,RN 和第三方依賴庫版本太老長時間不升級會帶來很多問題,如老 API 過時、新 API 變動太大,iOS、Android 系統更新帶來的兼容性問題都需要解決,升級應該作爲一個 task 經常關注並適時執行。

慎用 RealmJSRealm 是一個開源的移動端數據庫,性能表現非常不錯,API 也簡單易用。但 RealmJS 真是太難用了,首先安裝就很費勁,經常安裝失敗,即使安裝成功,按照文檔配置好了 iOS 也經常報錯 Missing Realm Constructor,並且這個錯誤問題還偶爾在 production 環境出現,導致 app 直接白屏無法使用。

而且在 iOS 14beta 版中 RealmJS 引發了一個 crash,導致所有 iOS 14beta 版的用戶都受到影響,雖然說這個 crash 在 iOS 14 的 beta2 迭代中就不存在了,但爲了保險起見,我還是決定升級 library。爲此我曾嘗試升級到 v6.6 版本,作爲一個暫時的解決方案,但是安裝依賴失敗這一點簡直不能忍,於是我決定徹底拋棄 RealmJS,改用 Realm 的 native SDK。雖然在 Android 和 iOS 兩端都需要寫 native 代碼來實現存儲功能,但真的比 RealmJS 用起來容易多了,再也不用擔心打包失敗和 missing constructor 了,真的誰用誰知道!

3. 跨平臺的侷限性

RN 對原生平臺依賴太強,取代不了原生。雖然它已經能做很多事了,但是:

很多功能還是需要原生端實現既然根植於原生,必然是脫離不了原生平臺的。很多功能使用原生方案實現是更好的選擇,比如拍照、圖片編輯、動畫使用原生 API 實現更直接、性能表現更好。

Android/iOS 系統升級適配 Android 和 iOS 系統更新或者條款更新總會需要開發者做一些適配工作,比如 Android 10 存儲權限的變更,導致共享目錄在 Android 10 以後不能再直接訪問,WRITE_EXTERNAL_STORAGE權限也不起作用。我們項目中用到第三方庫rn-fetch-blob來做下載功能,但是由於此庫無人維護,只能自己適配。由於下載和存儲是在 Native 端實現的,只能在 Native 端去做改動。

此外,對於 iOS 來說,要適配更新的 iOS 系統,我們經常需要升級 Xcode,可能在新版本的 Xcode 上就會遇到原來能編譯通過的項目現在卻編譯失敗了。

調試不方便 RN 需要 JS 的運行環境,在開發模式下本地需要啓動一個 package server 來監控文件的變更,配合 chrome 或者react dev tools來調試 JS 代碼。Native 代碼仍然需要使用 Android studio 或者 Xcode 來調試,這無疑增加了調試工作量。讓人難受的是有時候會因爲環境問題或者第三方庫的原因導致頻繁出現紅屏報錯,爲了解決這些 error 需要各種 search,時間就耗在這些問題上了。

安全性存在問題 RN 打包時會把 JS 代碼和資源文件打包成一個 js bundle 文件,這個 bundle 文件中就包含了所有編譯之後的 JS 代碼,因此一些重要的配置信息如 API key、secret 等最好不要寫在 JS 代碼中,以免造成安全問題。官方文檔也針對 security 做了比較清楚的說明。

穩定性問題 RN 的穩定性與原生平臺是有差距的,這一點必須承認,尤其是在 Android 端。RN 需要 JS 的運行環境來解釋執行 JS 編譯之後的 bundle 文件,在 Android 端使用了 webkit 官方開源的 jsc.so,此外還有很多其它的 so 調用,比如 Android 系統的 libc.so。一些 crash 問題就是由動態鏈接庫造成的,可能跟用戶本身設備系統版本和 webview 版本有關,系統庫導致的 crash 也沒有堆棧信息,因此這些問題很難定位原因,比如 libc.so 導致的 crash。還有 RN 組件本身導致的 crash,這些問題都是 RN 穩定性不如原生的因素之一。

4. 關於性能優化

性能優化是應用開發中常見的話題,RN 應用的優化需要從 JS 和原生端同時入手。

Crash 問題的追蹤我們的項目中使用了 Firebase crashlytics 來統計分析 crash log,從 Firebase console 可以看到,JS 端的 exception 都會通過 RN 原生代碼拋出,Android 中通過ExceptionsManagerModule中的reportException拋出異常信息,iOS 則通過 RCTAsset 中的RCTFormatError拋出異常。JS 端的 exception 一般也會有堆棧信息,可以在 js bundle 中去查找相關代碼定位 exception。 

Native 的 crash 則分別按照 Android 和 iOS 平臺的方式去定位,比如 Android 上傳native debug symbol到 Google play console,iOS 上傳 dSYM 文件到 Firebase 或相應的統計分析平臺,將符號化的日誌文件轉化成更加清晰的堆棧信息,便於我們分析定位問題。

在實踐中我發現很多 JS 端 exception 都是代碼不規範導致的,輕則導致 app 白屏重則 crash,比如從 Object 取值的時候 Object 可能是空的,不存在 key value。類似這樣的情況一定要謹慎處理,這裏建議使用 loadash 的 get 函數取值,在取值爲 undefined 的情況,還可以設置默認值。

import _ from "loadash";
const obj = {"key1": "1", "key2": "2"};
const a = _.get(obj, "key1.key2.key3", "");
if (a.length > 0) {
    // do something
}

本例中在路徑 “key1.key2.key3” 下都取不到值,a 就會是 undefined,這時候如果不賦予一個空字符串作爲默認值,那麼在 if 判斷時就會拋出異常,因爲 undefined 沒有 length 這個屬性。在我們平常寫代碼過程中有很多類似這樣的細節需要注意。

shouldComponentUpdate 官方文檔說完善地使用這個函數可以避免重新渲染那些實際沒有變化的子組件所帶來的額外開銷。但是在實際開發中,我們所面臨的情況可能比官方給出的例子要複雜得多,實際的業務邏輯、狀態變化遠遠不是一兩個變量能 cover 的。對於這個函數的使用,在不影響系統功能的前提下,可以儘量去用它控制組件的重複渲染,但不要指望它能幫我們 handle 複雜的業務場景下的頁面 render 規則。

其它優化這裏貼上很久之前寫的一點優化方案,可能部分已經不太適用了。其中防止 navigator 重複跳轉的問題,處理方式並不是好的選擇。這裏以我目前項目爲例,由於使用的是 react-navigation,爲了防止用戶操作過快多次點擊導致多次重複跳轉同一頁面,我們在頁面跳轉之前會判斷下一個頁面的 routeName,傳遞的參數等是否與當前 stack navigator 中存在的頁面相同,如果全部相同第二次之後就不再跳轉頁面。示例代碼如下 (由於 react-navigation 版本不同使用 API 可能略有差異):

export const navigateOnce = (getStateForAction: any) => (action: any, lastState: any) => {
  const { type, routeName, params } = action;
  return lastState &&
    type === NavigationActions.NAVIGATE &&
    routeName === lastState.routes[lastState.routes.length - 1].routeName &&
    JSON.stringify(params) === JSON.stringify(lastState.routes[lastState.routes.length - 1].params)
    ? null
    : getStateForAction(action, lastState);
};
CustomStackNavigator.router.getStateForAction =
navigateOnce(CustomStackNavigator.router.getStateForAction);

5. 一些開發中的建議 & tips

不要過於依賴第三方庫對於一些簡單的功能,能自己動手實現的儘量自己寫。這裏不是提倡重複造輪子,而是引入過多第三方庫可能會增加維護的工作量,畢竟不是你自己寫的代碼,一旦出了 bug 要麼寄希望於他人修復、要麼自己來改,而且隨着版本迭代,可能這個庫已經無法滿足當前的功能需求了。一般來說大廠的 SDK 質量還是有保證的,小廠的或者個人開發者的就不好說了,引入太多第三方 SDK 也可能對 app 穩定性造成影響。

offline 的調試開發過程中我們經常需要 debug,RN 會在本地啓動一個 package server 運行在 8081 端口,對於 iOS 來說 package server 通過 websoket 與 RN 建立連接,Android 由於通過 adb reverse 將 package server 端口映射到 Android 系統,所以即使斷網也能保持 package server 和 app 的連接。因此通常需要斷網調試時我都是把電腦網絡斷開,在模擬器上來 debug。使用真機 debug offline 模式會比較麻煩,Android 還好,iOS 真機一旦斷網就無法連接到 package server 了。如果 app 某些功能需要斷網也能使用的場景,在 offline 調試時使用模擬器或者 Android 真機會比較方便一點。

webp 支持 webp 其實不屬於 RN 的範疇,它是 Google 的一種圖片格式,使用 webp 格式圖片替代 png 或 jpg 格式文件,能夠減少圖片文件大小,減小應用包的體積。如何轉換 webp 圖片可以看 google 官方文檔。像 Android 項目中的大尺寸圖片如 splash 啓動頁就可以轉換成 webp 格式,可以大幅減小圖片所佔空間。

圖片快速加載 fastimageRN 中的 Image 組件加載網絡圖片比較緩慢,緩存機制不完善,對於大圖的顯示比較耗時,性能也比較差。這裏推薦使用 react-native-fast-image,其 iOS 端基於 SDWebImage,Android 使用 Glide 來加載圖片,有比較完善的緩存機制,能夠快速加載並顯示圖片。對於圖片較多的頁面,使用 fast image 組件能夠提高圖片渲染速度。

如何打 debug 包這裏我們打 debug 包的目的只是爲了測試,僅供參考。

在 debug 模式下想要不依賴 package server 讓打出的 debug 包獨立運行,需要先將 js bundle 打出來。可以使用如下命令,以 Android 爲例:

npx react-native bundle --platform android --dev false --entry-file index.js --bundle-output 
android/app/src/main/assets/index.android.bundle --assets-dest android/app/src/main/res

指定 output 路徑和 assets 圖片資源路徑,可以將 android bundle 文件和圖片資源輸出到工程目錄下,再通過./gradlew assembleDebug打包成 debug 版本的 apk。iOS 與此類似,只需要生成 js bundle 文件和導出 assets 圖片資源,在 Xcode——>Build Phases——>Compile Sources 添加 js bundle 和 assets 的引用,就可以直接通過 Xcode 進行 build。

npx react-native bundle --entry-file index.js --platform ios --dev false --bundle-output 
ios/main.jsbundle --assets-dest ios

爲了 build 方便,可以將腳本寫到 package.json 的 scripts 中,取個別名如ios-bundle,之後可以直接使用npm run ios-bundle進行打包。

禁用字體縮放效果手機系統調節字體大小後,app 中的文本字體大小也會隨之變化,尤其在 Android 上影響非常明顯。本來顯示效果滿分,調整字體大小後 UI 瞬間錯亂。在 RN 中我們可以通過在 app 啓動時禁用 Text 和 TextInput 組件的 font scaling 來實現,例如:

(Text as any).defaultProps = { ...((Text as any).defaultProps || {}), allowFontScaling: false };

    (TextInput as any).defaultProps = { ...((TextInput as any).defaultProps || {}), allowFontScaling: false };

強制使用 LTR 有些語言如阿拉伯語、希伯來語是從右往左排列的,當 Android 手機語言切換到阿拉伯語時,app 如果不做任何限制,UI 會默認從右向左顯示。可以通過如下方案強制 LTR(left to right) 顯示。

在 AndroidManifest 文件中給 application 設置

android:supportsRtl="false"

對於一些組件仍然支持 RTL 樣式的,需要在 styles.xml 中添加 layoutDirection,使 UI 樣式爲 LTR

<style >
    <!-- Customize your theme here. -->
    <item >ltr</item>

總結

RN 作爲移動端跨平臺開發框架來說,優缺點十分明顯。優點是上手比較簡單,開發者生態比較活躍,社區資源也比較豐富,缺點是性能穩定性與原生平臺還是存在一定差距的,尤其是對功能複雜、與原生交互較多的應用可能並不適用 RN 開發。雖然近年來使用 RN 開發的熱度貌似有所降低,尤其是以 Airbnb 爲首的一些公司放棄了 RN,並且 Flutter 這樣跨平臺框架的崛起,導致網上出現很多 “RN 已經涼了” 的聲音。但是時至今日,RN 仍然還在很多項目中得到廣泛應用,Facebook 仍然還在持續維護,開發者生態依然生機勃勃,可以說 RN 的生態是移動端跨平臺開發框架中最好的也不爲過,說涼涼還爲時過早。 

我個人認爲 RN 依然是有競爭力的,至於要不要用 RN 在技術選型階段還是要多考慮考慮,怎麼用、用不用得好在開發階段就需要多研究,在實踐過程中不斷優化改進。最後,歡迎大家一起探討,有好的實踐可以互相交流。

參考文章

  1. Security - React Native

  2. React Native 性能優化總結

  3. Loadash documentation

  4. Performance Overview

  5. 24 tips for React Native you probably want to know

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/RBTovCm9u-xx9qJUDTGg2A