杏仁移動開發(fā)工程師,前嵌入式工程師,關(guān)注大前端技術(shù)新潮流。
本文使用的環(huán)境:
React@16.3.1
React Native@0.55.4
react-native-code-push@5.3.4
Android SDK@23
Android Build Tool@23.0.3
Gradle@2.14.1
Android Gradle Plugin@2.2.3
Why CodePush?
CodePush 是微軟提供的一個熱更新前后臺方案,它對 React Native 項目有很好的支持。
目前針對 React Native 的 hot update 方案有許多,但是 CodePush 是最成熟穩(wěn)定的方案,它最大的特點是提供了完整的后臺工具。它主要的優(yōu)點是:
微軟出品,大廠保證
良好的多環(huán)境支持(Testing,Staging, Production)
灰度發(fā)布、自動回滾等等特性
良好的數(shù)據(jù)統(tǒng)計支持:下載、安裝、出錯一目了然
強大的 CLI 工具,一個終端搞定全部流程
由于 React Native 執(zhí)行的是腳本 js 文件,對熱更新有天然的親和,有余力的團隊可以嘗試實現(xiàn)自己的框架,一個簡單的實現(xiàn)思路是:
修改加載
jsBundle的代碼,轉(zhuǎn)而從指定的本地存儲位置去加載。如果沒有,下載 bundle, 并且本次打開使用 app 包中的 bundle。如果找到
jsBundle文件,調(diào)用 api 比較版本號,如果不一致,則從指定服務(wù)器下載最新的 bundle 進行替換。通過反射調(diào)用私有方法,在下載完成的回調(diào)中更新運行時資源,從而能立即看到更新的效果。
使用類似
google-diff-match-patch的 diff 工具,生成差異化補丁,不必下載完整 bundle,從而大大減小補丁包體積。
網(wǎng)上有很多資料和源碼,這里就不細述了。
后臺配置
為了使用 Code Push 發(fā)布熱更新,我們需要向微軟服務(wù)注冊我們的應(yīng)用。這部分工作微軟提供了強大的命令行工具:CodePush CLI。
安裝 cli 工具
npm 全局安裝:
npm install -g code-push-cli關(guān)聯(lián)賬號
使用命令
code-push register注冊一個賬號,可以直接使用 GitHub 賬號授權(quán),完成后將 token 復(fù)制回命令行中。
使用 whoami 查看登錄狀態(tài):
code-push whoami注冊應(yīng)用
登錄成功后,我們注冊一個app:
code-push app add 你的App名稱 android react-native注意一定要為 Android 和 iOS 分別注冊,兩者的更新包內(nèi)容會有差異。
查詢狀態(tài)
每個 App 有不同的運行時環(huán)境,比如 Production,Staging等,我們也可以配置自己的環(huán)境。查看 App 的不同環(huán)境和部署狀況:
code-push deployment ls 注冊的app名稱目前我們還沒有發(fā)布任何更新,所以表中的狀態(tài)是空的。
到這里就完成了后端的基本配置。
App端配置
版本兼容
安裝 Code Push 環(huán)境前首先要 check 版本的兼容性問題,不同的RN版本需要使用不同的 Code Push,原則上我們建議將 RN 和 CodePush 都升級到最新版本。
下表是官方文檔中的兼容性說明:
| React Native version(s) | Supporting CodePush version(s) |
|---|---|
| <0.14 | Unsupported |
| v0.14 | v1.3 (introduced Android support) |
| v0.15-v0.18 | v1.4-v1.6 (introduced iOS asset support) |
| v0.19-v0.28 | v1.7-v1.17 (introduced Android asset support) |
| v0.29-v0.30 | v1.13-v1.17 (RN refactored native hosting code) |
| v0.31-v0.33 | v1.14.6-v1.17 (RN refactored native hosting code) |
| v0.34-v0.35 | v1.15-v1.17 (RN refactored native hosting code) |
| v0.36-v0.39 | v1.16-v1.17 (RN refactored resume handler) |
| v0.40-v0.42 | v1.17 (RN refactored iOS header files) |
| v0.43-v0.44 | v2.0+ (RN refactored uimanager dependencies) |
| v0.45 | v3.0+ (RN refactored instance manager code) |
| v0.46 | v4.0+ (RN refactored js bundle loader code) |
| v0.46-v0.53 | v5.1+ (RN removed unused registration of JS modules) |
| v0.54-v0.55 | v5.3+ (Android Gradle Plugin 3.x integration) |
安裝包
使用命令:
npm info react-native-code-push來查看包相關(guān)信息。
我們建議始終將RN、React以及一些相關(guān)庫升級到最新版本。在根目錄下使用命令:
npm install --save react-native-code-push來安裝最新版本的 CodePush。
也可以參照上面的兼容性表格,安裝指定版本:
npm install --save react-native-code-push@5.1.4工程配置(Android)
如果工程創(chuàng)建的時候比較早,可能是使用命令create-react-native-app來創(chuàng)建的,則需要在根目錄執(zhí)行:
npm run eject來改變工程結(jié)構(gòu),防止后面的兼容性問題。
配置安卓工程,官方提供了兩種途徑:
使用命令行工具
rnpm(現(xiàn)在已經(jīng)被整合到React Native CLI工具中了)。執(zhí)行
react-native link react-native-code-push手動配置
如果你是新手,或者對 gradle、安卓工程結(jié)構(gòu)不了解,我們強烈建議執(zhí)行一次手動配置,幫助理解到底發(fā)生了什么。
手動配置
step 1
在android/settings.gradle文件中添加:
- include ':app', ':react-native-code-push'
- project(':react-native-code-push').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-code-push/android/app')
這個文件定義了哪些 module 應(yīng)該被加入到編譯過程,對于單個 module 的項目可以不用需要這個文件,但是對于 multiModule 的項目我們就需要這個文件,否則 gradle 不知道要加載哪些項目。這個文件的代碼在初始化階段就會被執(zhí)行。
我們添加的內(nèi)容告訴 gradle:去 node_modules 目錄下的 react-native-code-push 加載 CodePush 子項目。
step 2
在 android/app/build.gradle 中的 dependencies 方法中添加依賴:
- ...
- dependencies {
- ...
- compile project(':react-native-code-push')
- }
這樣就能在主工程中引用到 CodePush 模塊了。
step 3
繼續(xù)在 android/app/build.gradle 中,添加在編譯打包階段 CodePush 需要執(zhí)行的 task 引用:
- ...
- apply from: "../../node_modules/react-native-code-push/android/codepush.gradle"
- ...
這段代碼其實就是調(diào)用了 project 對象的 apply 方法,傳入了一個以 from 為 key 的 map。完整寫出來就是這樣的:
project.apply([from: '../../node_modules/react-native-code-push/android/codepush.gradle'])apply from 和 apply plugin的區(qū)別在于,前者是從指定 url 去加載腳本文件,后者則用是從倉庫拉取 plugin id 對應(yīng)的二進制執(zhí)行包。
step 4
CodePush 發(fā)布有各種環(huán)境(deployment),默認有 Staging 和 Production,我們需要在 buildType 中配置對應(yīng)的環(huán)境,并且設(shè)置 PushKey,從而讓 App 端的 CodePush RunTime 根據(jù)不同的健值來下載正確的更新包。
查詢各個環(huán)境 Key 的方法是使用上文安裝的 CLI 工具:
code-push deployment ls App名稱 -k上表中的 Deployment Key 就是對應(yīng)環(huán)境的 Key 值了。
在 android/app/build.gradle 中,配置 buildTypes:
- buildTypes {
- // 對應(yīng)Production環(huán)境
- release {
- ...
- buildConfigField "String", "CODEPUSH_KEY", '"從上述結(jié)果中復(fù)制的production值"'
- ...
- }
- // 對應(yīng)Staging環(huán)境
- releaseStaging {
- // 從 release 拷貝配置,只修改了 pushKey
- initWith release
- buildConfigField "String", "CODEPUSH_KEY", '"從上述結(jié)果中復(fù)制的stagingkey值"'
- }
- debug {
- buildConfigField "String", "CODEPUSH_KEY", '""'
- }
- }
注意這里不同 buildType 的命名,Staging 環(huán)境對應(yīng)的 buildType 就叫releaseStaging,要符合這樣的命名規(guī)范。
Debug 環(huán)境雖然用不到 CodePush, 但是也要配置空的 Key 值,否則會報錯。
step 5
處理完引用關(guān)系后,我們修改 MainApplication.java,在 App 執(zhí)行時啟動 CodePush 服務(wù):
- // 聲明包
- import com.microsoft.codepush.react.CodePush;
- public class MainApplication extends Application implements ReactApplication {
- private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) {
- ...
- // 重寫 getJSBundleFile() 方法,讓 CodePush 去獲取正確的 jsBundle
- @Override
- protected String getJSBundleFile() {
- return CodePush.getJSBundleFile();
- }
- @Override
- protected List<ReactPackage> getPackages() {
- return Arrays.<ReactPackage>asList(
- new MainReactPackage(),
- // 創(chuàng)建一個CodePush運行時實例
- new CodePush(BuildConfig.CODEPUSH_KEY, MainApplication.this, BuildConfig.DEBUG)
- ...
- );
- }
- };
- }
js端引入 Code Push
配置完項目工程后,我們將 CodePush 引入到 js 端。
首先將 App 的根組件包裹在 CodePush 中:
- import codePush from "react-native-code-push";
- AppRegistry.registerComponent('BDCRM', () => codePush(App));
CodePush 會在 App 啟動后自動去 check 和更新最新的版本,我們可以添加一些配置,讓它在進入后臺的時候也執(zhí)行檢查:
- let codePushOptions = { checkFrequency: codePush.CheckFrequency.MANUAL };
- AppRegistry.registerComponent('BDCRM', () => codePush(codePushOptions)(App));
CodePush js端的 api 不多,我們可以用這些 api 控制更新的一系列流程,常用的有:
- // 檢測是否有更新包可用
- codePush.checkForUpdate(deploymentKey: String = null, handleBinaryVersionMismatchCallback: (update: RemotePackage) => void): Promise<RemotePackage>;
- // 獲取本地最新更新包的屬性
- codePush.getCurrentPackage(): Promise<LocalPackage>;
- // 重啟app(即使不用在 Hot Updating,也挺有用的)
- codePush.restartApp(onlyIfUpdateIsPending: Boolean = false): void;
- // 手動進一次更新
- codePush.sync(options: Object, syncStatusChangeCallback: function(syncStatus: Number), downloadProgressCallback: function(progress: DownloadProgress), handleBinaryVersionMismatchCallback: function(update: RemotePackage)): Promise<Number>;
更多詳細信息見文檔。
使用 CodePush CLI 發(fā)布更新
完成前后端的配置,打包發(fā)布應(yīng)用后,后續(xù)的改動我們就能通過 CLI 工具來發(fā)布啦!
升級前首先要 check:
應(yīng)用的版本號要有更新(app/build.gradle: defaultConfig/versionName)
js bundle 要有改動,Code Push 會 diff 前后版本,如果代碼一致會認為是無效的更新包
打開終端,進入到工程目錄,完整發(fā)布命令是:
- code-push release-react <appName> <platform>
- [--bundleName <bundleName>]
- [--deploymentName <deploymentName>]
- [--description <description>]
- [--development <development>]
- [--disabled <disabled>]
- [--entryFile <entryFile>]
- [--gradleFile <gradleFile>]
- [--mandatory]
- [--noDuplicateReleaseError]
- [--outputDir <outputDir>]
- [--plistFile <plistFile>]
- [--plistFilePrefix <plistFilePrefix>]
- [--sourcemapOutput <sourcemapOutput>]
- [--targetBinaryVersion <targetBinaryVersion>]
- [--rollout <rolloutPercentage>]
- [--privateKeyPath <pathToPrivateKey>]
- [--config <config>]
命令參數(shù)很多,但用途都一目了然,嫌每次打麻煩的話,做成腳本也可以。
一般來說,我們發(fā)布應(yīng)用首先會在測試環(huán)境進行穩(wěn)定性測試,通過后再發(fā)布到生產(chǎn)環(huán)境中:
打包發(fā)布 Staging 環(huán)境
code-push release-react 應(yīng)用名 --platform android --deploymentName Staging --description "修復(fù)一些bug"這樣,我們 Staging 環(huán)境就可以收到更新推送啦,具體加載新 bundle 的實際,和我們在應(yīng)用中配置的策略有關(guān),上文已經(jīng)介紹過了。
測試 ok 后,提升(Promoting)到 Production 環(huán)境,并且進行灰度20%發(fā)布
code-push promote 應(yīng)用名 Staging Production --rollout 20%在生產(chǎn)環(huán)境驗證 ok,使用 patch 將灰度修改為100%,進行全網(wǎng)發(fā)布:
code-push patch 應(yīng)用名 Production -rollout 100%以上就是按照 測試 - 灰度 - 全部發(fā)布 步驟的一個典型 CodePush 發(fā)布工作流。
總體來說,CodePush 能滿足我們灰度發(fā)布 React Native 應(yīng)用的大部分需求了,由微軟提供的服務(wù)器端支持可以節(jié)省很多工作,是一個成熟可靠的方案。如果要說缺點,可能有幾個需要考慮一下:
服務(wù)器速度,國內(nèi)網(wǎng)絡(luò)狀況可能會影響下發(fā)的成功率和效率。
污染代碼,在 js 端必須將根節(jié)點包裹到 CodePush 模塊中去,污染了代碼。
冗余,如果只是想要簡單的下發(fā)小體積的 js bundle,CodePush 顯得太“重”,過于冗余了,這時候用輕量化的方案更好。
總之,我們根據(jù)自己項目的需要去進行選型就好了!
更多細節(jié),可以參考文檔



