React Native 升級教戰手冊 — 以 0.60 + AppBundle 為例

Cover

在開發 React Native (以下簡稱 RN) App 期間除了遇到很多雷之外,大致功能做完開始進入長期維護狀態也是偶爾就會有平台的問題冒出來得處理,有時候是 Apple/Google Play 改了些什麼會要求開發者某時候開始的 App 必須符合某些新規範或是 native API 改動,像是前陣子 RN App 用新版的 XCode build 出來的 App 跑起來會直接閃退,一查發現是 RN 自己使用的 iOS API 過舊導致的,但是修正好的 patch 只有在更新的 RN 版本中才有,於是我們只好著手開始進行升級

我們專案的版本是從 0.59.9 升級到 0.61.5,在升級的過程中似乎沒有找到一篇完整的文章比較細部的講要怎麼做,在成功完成升級且順利建置發布版本之後趁記憶新鮮把升級 RN 的一些眉角紀錄一下。

其實正常 RN 升級篇幅應該是無法湊成一篇文章的,而這次升級比較特別,因為 0.60 以後 RN 管理 native 套件的的方式改用 Auto Linking, Cocoapods,既然連套件管理工具都被改掉,可以想見要改的東西肯定是不小

同時 Android 的部份我們也趁這個機會改用 App bundle 來上傳,這似乎是 Google Play Store 新推的打包方法,在文章的最後會補充相關的說明以及心得

RN 官方的升級文件

https://facebook.github.io/react-native/docs/upgrading

RN 有個 command react-native upgrade [version] 來幫你升級,但是這個指令 9 成 9 沒辦法幫你升級好

文件裡頭有提到,實際上這個 command 做的事情是使用 git apply 嘗試把官方的差異套用到專案上,例如從 0.59.90.61.5https://github.com/react-native-community/rn-diff-purge/compare/release/0.59.9..release/0.61.5

基本上只要有裝點 native 套件,或改些專案設定值就會讓這個過程產生 conflict,如果是正常在跑的 RN 專案 conflict 更是會多到連執行 react-native upgrade 都不想,手動來比較快

避免不了的手動升級

至少 RN 團隊做了 rn-diff-purge 這個 Repo,把每個 RN release 版本專案建立時乾淨的樣子快照起來

在升級的時候就看著官方的 diff(而且也建議 clone 回本地慢慢看),把每個變動看懂,然後看自己的專案要怎麼改,因此執行 RN 升級必須對 native platform 有基本的了解

以下就用 0.59.9 升級到 0.61.5 為例,分成幾個部份升級:

升級 RN Nodejs 專案設定

這邊應該是 Javascript 工程師比較能理解的部份,最重要當然就是把 package.json 內指定的 react-native 版本提升到目標版本

看著官方的 package.json 變動 可以發現其他套件的版本也改了,或甚至有加入新套件,建議官方有裝的套件全部都裝一樣的版本

接著就執行 yarn 更新 lock 檔,順便看看有什麼 warning

另外可以看到 App.js 也被改動了,這邊可以理解為用新版 RN 新建的專案會有新的起始樣子,我們的專案會有我們的樣子,因此這邊理論上就不用理會

其他像是 eslint (.eslintrc.js), flow (.flowconfig), prettierrc (.prettierrc.js) 等設定就看要不要跟了

RN 0.60 開始使用的 Auto Linking

如同上面提到,RN 0.60 最大的改動就是使用 Auto Linking 來取代原本的 rnpm,同時也在 iOS 專案使用 CocoaPods 來管理 iOS native 套件

以往 rnpm 在安裝 RN npm 套件之後都會有個像是這樣的指令要 react-native link react-native-some-module,在 android, ios 資料夾動些手腳加入 native binding,而且必須把這些變動加入版本控制中

今後改成自動的,理論上只需要把 RN npm 套件安裝完成,在 RN 建置 native 部份的時候就自動去 node_modules 加入對應的 native binding

聽起確實是進步了對吧,但是需要套件有支援 auto link 才行,沒有支援也沒關係,原本的 native binding 依然可以運作,或許可以趁這個機會把所有 native 套件檢視一遍,如果覺得沒問題順便升級一波也不錯

CocoaPods

對於 iOS 部份 Auto Link 用到 CocoaPods 幫忙,CocoaPods 是 iOS native 套件管理工具,就像是 npm,只是是用來管理 iOS native 的第三方套件,事實上 RN 0.60 以後連 RN 本身也要透過 CocoaPods 來安裝

  1. CocoaPods 是用 Ruby 寫的 Ruby Gem,先安裝起來: gem install cocoapods
  2. 把 RN 官方的 Podfile 放到自己的專案內: ios/Podfile
  3. ios/PodfileRnDiffApp 都改成 iOS 的專案名稱,例如你的專案有個 ios/MyApp, ios/MyAppTest, 就把 RnDiffApp 改成 MyAppRnDiffAppTest 改成 MyAppTest,以此類推
  4. 進入 ios 資料夾,下 pod install 安裝 iOS native 套件

看一下 ios/Podfile 可以發現 pod 'React', :path => '../node_modules/react-native/' 等一堆指向 node_modules 的 RN 核心套件,同時 use_native_modules! 套用 RN Auto Link Scriptpod install 這個指令會幫你建立 ios/Pods 並且把所有 iOS native 套件都維護在裡面,就像是 npm install 建立並維護 node_modules 一樣,也就同樣地需要讓版本控制忽略這個資料夾

因此,接下來這些狀況需要再執行 cd ios && pod install 來更新/安裝 iOS native modules

  • 安裝新 RN npm 套件有需要 native binding
  • 或是剛剛提到的升級 RN npm 套件到有 Auto Link 版本
  • 新同事加入建立開發環境
  • CI 自動 build app

如果有 iOS native 套件更動時, ios/Podfile.lock 就跟 yarn.lock / package-lock.json 一樣會變動,也需要加入版本控制 sync 給同事們

使用 CocoaPods 後還有一個要注意的事情,就是以後要用 XCode 開啟專案時要開 XCode workspace 檔,如果 App 叫做 MyApp,就是 ios/MyApp.xcworkspace,這樣才能讀到 CocoaPods 套件;開啟 .xcodeproj 會導致有東西讀取不到。用 RnDiffApp 為例,你要開啟的檔案應是下方有反白的檔案:

Arrtjuz

Android 端 Auto Linking

Android 端則是靠 gradle script,接下來下面會需要手動 patch android/app/build.gradle,而 這行 就是來引入 RN 官方的 Auto Linking gradle script,因為 Android 本來就透過 gradle 來建置,所以以後在建置時就會自動進行 Auto Linking 幫我們把 Android native dependencies 安裝準備好

清除原本的 native binding

接下來如果有已經升級到支援 Auto Linking ,但是原本 native link 還在的套件,在 react-native run-android / react-native run-ios 時候會出現:

error React Native CLI uses autolinking for native dependencies, but the following modules are linked manually: 
  - react-native-some-module-1 (to unlink run: "react-native unlink react-native-some-module-1")
  - react-native-some-module-2 (to unlink run: "react-native unlink react-native-some-module-2")
This is likely happening when upgrading React Native from below 0.60 to 0.60 or above. Going forward, you can unlink this dependency via "react-native unlink <dependency>" and it will be included in your app automatically. If a library isn't compatible with autolinking, disregard this message and notify the library maintainers.

這時就執行 react-native unlink 移除即可:

react-native unlink react-native-some-module-1
react-native unlink react-native-some-module-2

而如果當前第三方套件的版本沒有支援 Auto Linking,就像上方講的,可以透過這個機會把這些套件檢視一遍看要不要順便做一次升級

Android part (專案的 android 資料夾)

對 Android native 部份,大多是 .grade, .properties, .java 等文字檔,用文字編輯器修改

理論上沒有特別情況而且官方的更動可以放進自己的專案時,應該盡量與官方同步以避免不必要的錯誤,而且也可以更貼近官方設定進而方便未來的更新

android/app/build.gradle

有幾行新加入跟 Javascript engine/Hermes 相關的設定,預設是關閉的,可以參考一下 RN 官方的說明了解一下 Hermes 是什麼: https://reactnative.dev/docs/hermes

project.ext.react = [ ... enableHermes: false ]
// ...
def jscFlavor = 'org.webkit:android-jsc:+'
// ...
def enableHermes = project.ext.react.get("enableHermes", false);
// ...
dependencies {
  // ...
  if (enableHermes) {
    def hermesPath = "../../node_modules/hermes-engine/android/";
    debugImplementation files(hermesPath + "hermes-debug.aar")
    releaseImplementation files(hermesPath + "hermes-release.aar")
  } else {
    implementation jscFlavor
  }
}

可能未來 android 未來 debug 也跟 iOS 要簽章…這邊看到簽章部份 debug 也要一份了:

android {
  // ...
  signingConfigs {
    debug {
      storeFile file('debug.keystore')
      storePassword 'android'
      keyAlias 'androiddebugkey'
      keyPassword 'android'
    }
    // ... release { ... }
  }
  buildTypes {
    // ...
    debug {
      signingConfig signingConfigs.debug
    }
    release {
      // release block 裡頭的 signingConfig signingConfigs.debug 只是官方的 Sample
      // 如果有發布/上架過,在 signingConfigs 中應有 release block 並且設定好 signingConfig signingConfigs.release
    }
  }
}

同時也必須把 android/app/debug.keystore 準備好,可以直接用 RN 官方的 keystore,畢竟只是開發用

  • def versionCodes = ["armeabi-v7a": 1, "x86": 2, "arm64-v8a": 3, "x86_64": 4] 這行其實沒有變化,不過就像上面講的,直接跟官方同步讓未來升級更順暢
  • implementation "com.android.support:appcompat-v7:${rootProject.ext.supportLibVersion}" 這行被刪除了,我們也把它刪除
  • apply from: file("../../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesAppBuildGradle(project) 加上這行來引入上面提到 RN 官方的 Auto Linking gradle script

MainActivity.java / MainApplication.java

其實大多都是程式碼排版的改變,除此之外 MainApplication.java 第一個比較大的變動是 Native 套件 binding 的地方寫法稍微改了

public class MainApplication extends Application implements ReactApplication {
  // ...
  private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) {
    // ...
    @Override
    protected List<ReactPackage> getPackages() {
      @SuppressWarnings("UnnecessaryLocalVariable")
      List<ReactPackage> packages = new PackageList(this).getPackages();
      // Packages that cannot be autolinked yet can be added manually here, for example:
      // packages.add(new MyReactNativePackage());
      return packages;
    }
    // ...
  }
  // ...
}

同時 List<ReactPackage> packages 套件清單中移除了 new MainReactPackage(),所以 import com.facebook.react.shell.MainReactPackage; 也就不需要了

另外一個變動是多了 private static void initializeFlipper(Context context) 這個 private mehotd,似乎是可以搭配 debug 工具 Flipper,加上之後開發時可以嘗試用用看這個工具

最後幾個 android/ 資料夾內的變動就直接同步 RN 官方即可

iOS part (專案的 ios 資料夾)

首先兩個 Info.plist 檔案只是順序上的調整,XCode 在調整專案設定時本來就會自己去調整,因此這邊不用特別去理會:

接著這兩個檔案就照著 RN 官方的變動修改

接下來 ios/MyApp.xcodeproj/project.pbxproj 的部份比較沒辦法用文字編輯器進行編輯,我們要透過 XCode 來修改專案設定,請直接用 Finder 點兩下打開 ios/MyApp.xcworkspace:

因為不能用公司專案來擷圖,這邊我直接用 RnDiffApp 當作示範,通常這邊會有許多之前改過的設定

Arrtjuz

開始用 XCode 手動修改之前記得先把上面講到的 CocoaPods 設定好,pod install 會幫忙把基本的東西設定到 ios/RnDiffApp.xcodeproj/project.pbxproj

Jpanhzl

請開啟專案設定值:

Jszsx1n

可以看到升級之前的 RN native bindings 前面的圖標都變成空白的:

K3pcphp

要做的事情就是把這些 native binding 移除,但是我們要直接從左方專案內容的 Libraries 移除:

Fuxchuo

RN 0.60 以前的核心模組,也就是確定要移除的有這些:

  • libRCTActionSheet.a / RCTActionSheet.xcodeproj
  • libRCTAnimation.a / RCTAnimation.xcodeproj
  • libRCTBlob.a / RCTBlob.xcodeproj
  • libRCTGeolocation.a / RCTGeolocation.xcodeproj
  • libRCTImage.a / RCTImage.xcodeproj
  • libRCTLinking.a / RCTLinking.xcodeproj
  • libRCTNetwork.a / RCTNetwork.xcodeproj
  • libRCTSettings.a / RCTSettings.xcodeproj
  • libRCTText.a / RCTText.xcodeproj
  • libRCTVibration.a / RCTVibration.xcodeproj
  • libRCTWebSocket.a / RCTWebSocket.xcodeproj
  • libReact.a / React.xcodeproj

如果之前有用原本的方式 link 其他套件進來 Libraries 或許會有其他的東西,要逐一檢查這些套件是否可以改用 auto-linking,如果可以便可透過 react-native unlink react-native-some-module-1 移除手動的 link

大致完成!上機測試!

就算上面講的修改都改得很順利,也不代表就完成了,因為往往要跑起來才會遇到大條的問題,不過不跑起來也不會知道有什麼問題,所以就從 development 模式把 App 跑起來看看是否跑得起來,是否一切功能都正常,接著再嘗試 build 出 production 版上 Google Play / TestFlight

我們的專案在升級的過程有順便把所有能升級的套件都做一次升級,大部分套件除了 API 改動之外是沒有什麼大問題,搞了比較久的是 react-native-config,因為專案剛建立時安裝套件有在 ios/MyApp.xcodeproj/project.pbxproj 改一些 Build Phase 的參數,更新之後該參數導致 build App 的階段失敗。每個 RN App 的專案設定都不一樣,這邊大家就要自己衡量對於每個 dependencies 的處理方式了

Migrate to AppBundle for Android

AppBundle 是 Google 新推的 Android App 打包/上傳方式:https://developer.android.com/platform/technology/app-bundle

簡單來說這個打包方式讓 Google 可以幫你多做一些事情,像是幫你保管數位簽證以及簽署 App,做 App 分割 (spliting) 使得使用者在更新 App 時只要下載有更新的部份而不用下載整包新的 apk

為了達到這個目的,我們上傳的格式要從 apk 變成 aab,而且這之前要先去 Google Play Console 設定好 App signing:

App signing by Google Play

Eoxzpww

對於已經上架的 App,改用 AppBundle 時 Google Play Console 會提供幾個方式,可以從 Android Studio 或是用 keytool 指令來做,Google Play Console 這邊的 step by step 還算蠻清楚的照著做應該不會有什麼問題;我們的理解是把我們原本簽署 App 的整包 keystore 用 Google Play Console 給的 key 加密匯出並上傳到 Google Play Console,這樣一來 Google Play 就可以在幫我們把 App 多做一些優化後重新簽章

如果 Android App 還沒上架過,也就是一開始就採用 AppBundle 的 Android App,跟著 RN 官方的 Android 上架教學文件 以及 Android 官方 App signing 文件 來看,之後新的 Android App 在開發者端產生的數位簽章只是 upload key,該 aab 用 upload key 簽章後上傳到 Google Play 後,Google Play 會再用他們自己產生的 app signing key 重新簽署給終端使用者下載

Build as aab format to upload

簽章 (signing) 部份搞定好了,再來就是之後上傳到 Google Play 的檔案格式從 apk 改成 aab,其實就是原本 build app 的指令,從:

cd android && ./gradlew assembleRelease

改成:

cd android && ./gradlew bundleRelease

這樣就會 build 出 aab 了,接著就試著把 aab 上傳到 Google Play Console 看看是否一切正常囉;由於我們專案有用 CI 跑 Fastlane 自動 build 出 App,這邊當然也要進行修改,把 gradle(…)task: 'assemble' 改成 task: 'bundle' 使 CI 也用 aab 來打包上傳 App

弄完這些之後立刻就發現改用 aab 上傳的缺點,就是先前可以直接在 Google Play Console 的 Artifact library 下載各個 build 版本的 apk 來安裝,達到像是 TestFlight 那樣快速切換版本;之後變成 AppBundle aab 就無法在 Android 裝置上直接安裝了,於是我們尋找替代方案找到一個方法:internal app sharing,而且 fastlane 也有對應的整合了,稍微修改 CI 流程便可快速地切換安裝的 AppBundle 版本

先寫到這吧,如果是有打算升級 RN 版本越過 0.60 的人,希望這篇有幫助!

跟 RN 打滾的這一年多,除了 App 實做之外也有不少是與工作流程自動化有關,今天分享的 RN 升級可能只是其中一小部份,如果之後還有時間再來寫其他 RN 的經驗分享,希望這篇有幫到正在閱讀的你/妳!