React Native應用部署/熱更新-CodePush最新集成總結(新)
本文出自《React Native學習筆記》系列文章。
了解更多,可以關注我的GitHub和加入:
React Native學習交流群

更新說明:
此次博文更新適配了最新版的CodePush v1.17.0;添加了iOS的集成方式與調試技巧;添加了更為簡潔的CodePush發布更新的方式以及進行了一些其他的優化。
React Native的出現為移動開發領域帶來了兩大革命性的創新:
- 整合了移動端APP的開發,不僅縮短了APP的開發時間,也提高了APP的開發效率。
- 為移動APP動態更新提供了基礎。
本文將向大家分享React Natvie應用部署/動態更新方面的內容。
React Native支持大家用React Native技術開發APP,并打包生成一個APP。在動態更新方面React Native只是提供了動態更新的基礎,對將應用部署到哪里,如何進行動態更新并沒有支持的那么完善。好在微軟開發了CodePush,填補React Native 應用在動態更新方面的空白。CodePush 是微軟提供的一套用于熱更新 React Native 和 Cordova 應用的服務。下面將向大家分享如何使用CodePush實時更新你的應用,后期會分享不采用CodePush,如何自己去實現React Native應用熱更新。
CodePush簡介
CodePush 是微軟提供的一套用于熱更新 React Native 和 Cordova 應用的服務。
CodePush 是提供給 React Native 和 Cordova 開發者直接部署移動應用更新給用戶設備的云服務。CodePush 作為一個中央倉庫,開發者可以推送更新 (JS, HTML, CSS and images),應用可以從客戶端 SDK 里面查詢更新。CodePush 可以讓應用有更多的可確定性,也可以讓你直接接觸用戶群。在修復一些小問題和添加新特性的時候,不需要經過二進制打包,可以直接推送代碼進行實時更新。
CodePush 可以進行實時的推送代碼更新:
- 直接對用戶部署代碼更新
- 管理 Alpha,Beta 和生產環境應用
- 支持 React Native 和 Cordova
- 支持JavaScript 文件與圖片資源的更新
CodePush開源了react-native版本,react-native-code-push托管在GitHub上。
安裝與注冊CodePush
使用CodePush之前首先要安裝CodePush客戶端。本文以OSX 10.11.5作為平臺進行演示。
安裝 CodePush CLI
管理 CodePush 賬號需要通過 NodeJS-based CLI。
只需要在終端輸入 npm install -g code-push-cli
,就可以安裝了。
安裝完畢后,輸入 code-push -v
查看版本,如看到版本代表成功。

目前我的版本是 1.12.1-beta
PS.
npm
為NodeJS的包管理器,如果你沒安裝NodeJS請先安裝。
創建一個CodePush 賬號
在終端輸入code-push register
,會打開如下注冊頁面讓你選擇授權賬號。

授權通過之后,CodePush會告訴你“access key”,復制此key到終端即可完成注冊。

然后終端輸入
code-push login
進行登陸,登陸成功后,你的session文件將會寫在 /Users/你的用戶名/.code-push.config。
PS.相關命令
-
code-push login
登陸 -
code-push loout
注銷 -
code-push access-key ls
列出登陸的token -
code-push access-key rm <accessKye>
刪除某個 access-key
在CodePush服務器注冊app
為了讓CodePush服務器知道你的app,我們需要向它注冊app: 在終端輸入code-push app add <appName>
即可完成注冊。

注冊完成之后會返回一套deployment key,該key在后面步驟中會用到。
心得:如果你的應用分為Android和iOS版,那么在向CodePush注冊應用的時候需要注冊兩個App獲取兩套deployment key,如:
code-push app add MyApp-Android
code-push app add MyApp-iOS
PS.相關命令
-
code-push app add
在賬號里面添加一個新的app -
code-push app remove
或者 rm 在賬號里移除一個app -
code-push app rename
重命名一個存在app -
code-push app list
或則 ls 列出賬號下面的所有app -
code-push app transfer
把app的所有權轉移到另外一個賬號
集成CodePush SDK
Android
下面我們通過如下步驟在Android項目中集成CodePush。
第一步:在項目中安裝 react-native-code-push插件,終端進入你的項目根目錄然后運行
npm install --save react-native-code-push
第二步:在Android project中安裝插件。
CodePush提供了兩種方式:RNPM 和 Manual,本次演示所使用的是RNPM。
運行npm i -g rnpm
,來安裝RNPM。
在React Native v0.27及以后版本RNPM已經被集成到了 React Native CL中,就不需要再進行安裝了。
第三步: 運行 rnpm link react-native-code-push
。這條命令將會自動幫我們在anroid文件中添加好設置。

在終端運行此命令之后,終端會提示讓你輸入deployment key,這是你只需將你的deployment Staging key輸入進去即可,如果不輸入則直接單擊enter跳過即可。
第四步: 在 android/app/build.gradle文件里面添如下代碼:
apply from: "../../node_modules/react-native-code-push/android/codepush.gradle"
然后在/android/settings.gradle中添加如下代碼:
include ':react-native-code-push'
project(':react-native-code-push').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-code-push/android/app')
第五步: 運行 code-push deployment -k ls <appName>
獲取 部署秘鑰。默認的部署名是 staging,所以 部署秘鑰(deployment key ) 就是 staging。
第六步: 添加配置。當APP啟動時我們需要讓app向CodePush咨詢JS bundle的所在位置,這樣CodePush就可以控制版本。更新 MainApplication.java文件:
public class MainApplication extends Application implements ReactApplication {
private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) {
@Override
protected boolean getUseDeveloperSupport() {
return BuildConfig.DEBUG;
}
@Override
protected String getJSBundleFile() {
return CodePush.getJSBundleFile();
}
@Override
protected List<ReactPackage> getPackages() {
// 3. Instantiate an instance of the CodePush runtime and add it to the list of
// existing packages, specifying the right deployment key. If you don't already
// have it, you can run "code-push deployment ls <appName> -k" to retrieve your key.
return Arrays.<ReactPackage>asList(
new MainReactPackage(),
new CodePush("deployment-key-here", MainApplication.this, BuildConfig.DEBUG)
);
}
};
@Override
public ReactNativeHost getReactNativeHost() {
return mReactNativeHost;
}
}
關于deployment-key的設置
在上述代碼中我們在創建CodePush實例的時候需要設置一個deployment-key,因為deployment-key分生產環境與測試環境兩種,所以建議大家在build.gradle中進行設置。在build.gradle中的設置方法如下:
打開android/app/build.gradle文件,找到android { buildTypes {} }
然后添加如下代碼即可:
android {
...
buildTypes {
debug {
...
// CodePush updates should not be tested in Debug mode
...
}
releaseStaging {
...
buildConfigField "String", "CODEPUSH_KEY", '"<INSERT_STAGING_KEY>"'
...
}
release {
...
buildConfigField "String", "CODEPUSH_KEY", '"<INSERT_PRODUCTION_KEY>"'
...
}
}
...
}
心得:另外,我們也可以將deployment-key存放在local.properties中:
code_push_key_production=erASzHa1-wTdODdPJDh6DBF2Jwo94JFH08Kvb
code_push_key_staging=mQY75RkFbX6SiZU1kVT1II7OqWst4JFH08Kvb
如圖:

然后在就可以在android/app/build.gradle可以通過下面方式來引用它了:
Properties properties = new Properties()
properties.load(project.rootProject.file('local.properties').newDataInputStream())
android {
...
buildTypes {
debug {
...
// CodePush updates should not be tested in Debug mode
...
}
releaseStaging {
...
buildConfigField "String", "CODEPUSH_KEY", '"'+properties.getProperty("code_push_key_production")+'"'
...
}
release {
...
buildConfigField "String", "CODEPUSH_KEY", '"'+properties.getProperty("code_push_key_staging")+'"'
...
}
}
...
}
在android/app/build.gradle設置好deployment-key之后呢,我們就可以這樣使用了:
@Override
protected List<ReactPackage> getPackages() {
return Arrays.<ReactPackage>asList(
...
new CodePush(BuildConfig.CODEPUSH_KEY, MainApplication.this, BuildConfig.DEBUG), // Add/change this line.
...
);
}
第七步:修改versionName。
在 android/app/build.gradle中有個 android.defaultConfig.versionName屬性,我們需要把 應用版本改成 1.0.0(默認是1.0,但是codepush需要三位數)。
android{
defaultConfig{
versionName "1.0.0"
}
}
至此Code Push for Android的SDK已經集成完成。
iOS
CodePush官方提供RNPM、CocoaPods與手動三種在iOS項目中集成CodePush的方式,接下來我就以RNPM的方式來講解一下如何在iOS項目中集成CodePush。
第一步:在項目中安裝react-native-code-push插件,終端進入你的項目根目錄然后運行
npm install --save react-native-code-push
第二步: 運行 rnpm link react-native-code-push
。這條命令將會自動幫我們在ios中添加好設置。
在終端運行此命令之后,終端會提示讓你輸入deployment key,這是你只需將你的deployment Staging key輸入進去即可,如果不輸入則直接單擊enter跳過即可。
關于deployment-key的設置
在我們想CodePush注冊App的時候,CodePush會給我們兩個deployment-key分別是在生產環境與測試環境時使用的,我們可以通過如下步驟來設置deployment-key。
1.用Xcode 打開項目 ? Xcode的項目導航視圖中的PROJECT
下選擇你的項目 ? 選擇Info頁簽 ? 在Configurations
節點下單擊 + 按鈕 ? 選擇Duplicate "Release
? 輸入Staging(名稱可以自定義);

2.然后選擇Build Settings
頁簽 ? 單擊 + 按鈕然后選擇添加User-Defined Setting

3.然后輸入CODEPUSH_KEY(名稱可以自定義)

提示:你可以通過
code-push deployment ls <APP_NAME> -k
命令來查看deployment key。
4.打開 Info.plist文件,在CodePushDeploymentKey列的Value中輸入$(CODEPUSH_KEY)

到目前為止,iOS的設置已經完成了,和在Android上的集成相比是不是簡單了很多呢。
使用CodePush進行熱更新
設置更新策略
在使用CodePush更新你的應用之前需要,先配置一下更新控制策略,即:
- 什么時候檢查更新?(在APP啟動的時候?在設置頁面添加一個檢查更新按鈕?)
- 什么時候可以更新,如何將更新呈現給終端用戶?
最簡單的方式是在根component中進行上述策略控制。
- 在 js中加載 CodePush模塊:
import codePush from 'react-native-code-push'
2.在componentDidMount
中調用sync
方法,后臺請求更新
codePush.sync()
如果可以進行更新,CodePush會在后臺靜默地將更新下載到本地,等待APP下一次啟動的時候應用更新,以確保用戶看到的是最新版本。
如果更新是強制性的,更新文件下載好之后會立即進行更新。
如果你期望更及時的獲得更新,可以在每次APP從后臺進入前臺的時候去主動的檢查更新:
在應用的根component的componentDidMount
中添加如下代碼:
AppState.addEventListener("change", (newState) => {
newState === "active" && codePush.sync();
});
發布更新
CodePush支持兩種發布更新的方式,一種是通過code-push release-react
簡化方式,另外一種是通過code-push release
的復雜方式。
第一種方式:通過
code-push release-react
發布更新
這種方式將打包與發布兩個命令合二為一,可以說大大簡化了我們的操作流程,建議大家多使用這種方式來發布更新。
命令格式:
code-push release-react <appName> <platform>
eg:
code-push release-react MyApp-iOS ios
code-push release-react MyApp-Android android
再來個更高級的:
code-push release-react MyApp-iOS ios --t 1.0.0 --dev false --d Production --des "1.優化操作流程" --m true
其中參數--t為二進制(.ipa與apk)安裝包的的版本;--dev為是否啟用開發者模式(默認為false);--d是要發布更新的環境分Production與Staging(默認為Staging);--des為更新說明;--m 是強制更新。
關于code-push release-react
更多可選的參數,可以在終端輸入code-push release-react
進行查看。
另外,我們可以通過code-push deployment ls <appName>
來查看發布詳情與此次更新的安裝情況。
第二中方式:通過
code-push release
發布更新
code-push release
發布更新呢我們首先需要將js與圖片資源進行打包成 bundle。
生成bundle
發布更新之前,需要先把 js打包成 bundle,如:
第一步: 在 工程目錄里面新增 bundles文件:mkdir bundles
第二步: 運行命令打包 react-native bundle --platform 平臺 --entry-file 啟動文件 --bundle-output 打包js輸出文件 --assets-dest 資源輸出目錄 --dev 是否調試
。
eg:
react-native bundle --platform android --entry-file index.android.js --bundle-output ./bundles/index.android.bundle --dev false

需要注意的是:
- 忽略了資源輸出是因為 輸出資源文件后,會把bundle文件覆蓋了。
- 輸出的bundle文件名不叫其他,而是 index.android.bundle,是因為 在debug模式下,工程讀取的bundle就是叫做 index.android.bundle。
- 平臺可以選擇 android 或者 ios。
發布更新
打包bundle結束后,就可以通過CodePush發布更新了。在終端輸入
code-push release <應用名稱> <Bundles所在目錄> <對應的應用版本> --deploymentName: 更新環境 --description: 更新描述 --mandatory: 是否強制更新
eg:
code-push release GitHubPopular ./bundles/index.android.bundle 1.0.6 --deploymentName Production --description "1.支持文章緩存。" --mandatory true

注意:
- CodePush默認是更新 staging 環境的,如果是staging,則不需要填寫 deploymentName。
- 如果有 mandatory 則Code Push會根據mandatory 是true或false來控制應用是否強制更新。默認情況下mandatory為false即不強制更新。
- 對應的應用版本(targetBinaryVersion)是指當前app的版本(對應build.gradle中設置的versionName "1.0.6"),也就是說此次更新的js/images對應的是app的那個版本。不要將其理解為這次js更新的版本。
如客戶端版本是 1.0.6,那么我們對1.0.6的客戶端更新js/images,targetBinaryVersion填的就是1.0.6。 - 對于對某個應用版本進行多次更新的情況,CodePush會檢查每次上傳的 bundle,如果在該版本下如1.0.6已經存在與這次上傳完全一樣的bundle(對應一個版本有兩個bundle的md5完全一樣),那么CodePush會拒絕此次更新。
如圖:
對應一個版本有兩個bundle的md5完全一樣
所以如果我們要對某一個應用版本進行多次更新,只需要上傳與上次不同的bundle/images即可。如:
eg:
對1.0.6的版本進行第一次更新:
code-push release GitHubPopular ./bundles/index.android.bundle 1.0.6 --deploymentName Production --description "1.支持文章緩存。" --mandatory true
對1.0.6的版本進行第二次更新:
code-push release GitHubPopular ./bundles/index.android.bundle 1.0.6 --deploymentName Production --description "1.新添加收藏功能。" --mandatory true
- 在終端輸入
code-push deployment history <appName> Staging
可以看到Staging版本更新的時間、描述等等屬性。
eg:
code-push release Equipment ./bundles 1.0.1
下面我們啟動事先安裝好的應用,看有什么反應:

應用啟動之后,從CodePush服務器查詢更新,并下載到本地,下載好之后,提示用戶進行更新。這就是CodePush用于熱更新的整個過程。
更多部署APP相關命令
- code-push deployment add <appName> 部署
- code-push deployment rename <appName> 重命名
- code-push deployment rm <appName> 刪除部署
- code-push deployment ls <appName> 列出應用的部署情況
- code-push deployment ls <appName> -k 查看部署的key
- code-push deployment history <appName> <deploymentNmae> 查看歷史版本(Production 或者 Staging)
調試技巧
如果你用模擬器進行調試CodePush,在默認情況下是無法達到調試效果的,因為在開發環境下裝在模擬器上的React Native應用每次啟動時都會從NodeJS服務器上獲取最新的bundle,所以還沒等CodePush從服務器將更新包下載下來時,APP就已經從NodeJS服務器完成了更新。
Android
為規避這個問題在Android可以將開發環境的調試地址改為一個不可用的地址,如下圖:

這樣APP就無法連接到NodeJS服務器了,自然也就不能從NodeJS服務器下載bundle進行更新了,它也只能乖乖的等待從CodePush服務器下載更新包進行更新了。
iOS
在iOS中我們需要上文中講到的生成bundle,將bundle包與相應的圖片資源拖到iOS項目中如圖:

然后呢,我們需要在AppDelegate.m中進行如下修改:
//#ifdef DEBUG
// jsCodeLocation = [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index.ios" fallbackResource:nil];
//#else
jsCodeLocation = [CodePush bundleURL];
//#endif
讓React Native通過CodePush去獲取bundle包即可。
JavaScript API Reference
- allowRestart
- checkForUpdate
- disallowRestart
- getUpdateMetadata
- notifyAppReady
- restartApp
- sync
其實我們可以將這些API分為兩類,一類是自動模式,一類是手動模式。
自動模式
sync
為自動模式,調用此方法CodePush會幫你完成一系列的操作。其它方法都是在手動模式下使用的。
codePush.sync
codePush.sync(options: Object, syncStatusChangeCallback: function(syncStatus: Number), downloadProgressCallback: function(progress: DownloadProgress)): Promise<Number>;
通過調用該方法CodePush會幫我們自動完成檢查更新,下載,安裝等一系列操作。除非我們需要自定義UI表現,不然直接用這個方法就可以了。
sync方法,提供了如下屬性以允許你定制sync方法的默認行為
- deploymentKey (String): 部署key,指定你要查詢更新的部署秘鑰,默認情況下該值來自于Info.plist(Ios)和MianActivity.java(Android)文件,你可以通過設置該屬性來動態查詢不同部署key下的更新。
- installMode (codePush.InstallMode): 安裝模式,用在向CodePush推送更新時沒有設置強制更新(mandatory為true)的情況下,默認codePush.InstallMode.ON_NEXT_RESTART即下一次啟動的時候安裝。
- mandatoryInstallMode (codePush.InstallMode):強制更新,默認codePush.InstallMode.IMMEDIATE。
- minimumBackgroundDuration (Number):該屬性用于指定app處于后臺多少秒才進行重啟已完成更新。默認為0。該屬性只在
installMode
為InstallMode.ON_NEXT_RESUME
情況下有效。 - updateDialog (UpdateDialogOptions) :可選的,更新的對話框,默認是null,包含以下屬性
- appendReleaseDescription (Boolean) - 是否顯示更新description,默認false
- descriptionPrefix (String) - 更新說明的前綴。 默認是” Description: “
- mandatoryContinueButtonLabel (String) - 強制更新的按鈕文字. 默認 to “Continue”.
- mandatoryUpdateMessage (String) - 強制更新時,更新通知. Defaults to “An update is available that must be installed.”.
- optionalIgnoreButtonLabel (String) - 非強制更新時,取消按鈕文字. Defaults to “Ignore”.
- optionalInstallButtonLabel (String) - 非強制更新時,確認文字. Defaults to “Install”.
- optionalUpdateMessage (String) - 非強制更新時,更新通知. Defaults to “An update is available. Would you like to install it?”.
- title (String) - 要顯示的更新通知的標題. Defaults to “Update available”.
eg:
codePush.sync({
updateDialog: {
appendReleaseDescription: true,
descriptionPrefix:'\n\n更新內容:\n',
title:'更新',
mandatoryUpdateMessage:'',
mandatoryContinueButtonLabel:'更新',
},
mandatoryInstallMode:codePush.InstallMode.IMMEDIATE,
deploymentKey: CODE_PUSH_PRODUCTION_KEY,
});
手動模式
codePush.allowRestart
codePush.allowRestart(): void;
允許重新啟動應用以完成更新。
如果一個CodePush更新將要發生并且需要重啟應用(e.g.設置了InstallMode.IMMEDIATE模式),但由于調用了disallowRestart
方法而導致APP無法通過重啟來完成更新,
可以調用此方法來解除disallowRestart
限制。
但在如下四種情況下,CodePush將不會立即重啟應用:
- 自上一次
disallowRestart
被調用,沒有新的更新。 - 有更新,但
installMode
為InstallMode.ON_NEXT_RESTART
的情況下。 - 有更新,但
installMode
為InstallMode.ON_NEXT_RESUME
,并且程序一直處于前臺,并沒有從后臺切換到前臺的情況下。 - 自從上次
disallowRestart
被調用,沒有再調用restartApp
。
codePush.checkForUpdate
codePush.checkForUpdate(deploymentKey: String = null): Promise<RemotePackage>;
向CodePush服務器查詢是否有更新。
該方法返回Promise,有如下兩種值:
-
null 沒有更新
通常有如下情況導致RemotePackage為null:- 當前APP版本下沒有部署新的更新版本。也就是說沒有想CodePush服務器推送基于當前版本的有關更新。
- CodePush上的更新和用戶當前所安裝的APP版本不匹配。也就是說CodePush服務器上有更新,但該更新對應的APP版本和用戶安裝的當前版本不對應。
- 當前APP已將安裝了最新的更新。
- 部署在CodePush上可用于當前APP版本的更新被標記成了不可用。
- 部署在CodePush上可用于當前APP版本的更新是"active rollout"狀態,并且當前的設備不在有資格更新的百分比的設備之內。
A RemotePackage instance
有更新可供下載。
eg:
codePush.checkForUpdate()
.then((update) => {
if (!update) {
console.log("The app is up to date!");
} else {
console.log("An update is available! Should we download it?");
}
});
codePush.disallowRestart
codePush.disallowRestart(): void;
不允許立即重啟用于以完成更新。
eg:
class OnboardingProcess extends Component {
...
componentWillMount() {
// Ensure that any CodePush updates which are
// synchronized in the background can't trigger
// a restart while this component is mounted.
codePush.disallowRestart();
}
componentWillUnmount() {
// Reallow restarts, and optionally trigger
// a restart if one was currently pending.
codePush.allowRestart();
}
...
}
codePush.getUpdateMetadata
codePush.getUpdateMetadata(updateState: UpdateState = UpdateState.RUNNING): Promise<LocalPackage>;
獲取當前已安裝更新的元數據(描述、安裝時間、大小等)。
eg:
// Check if there is currently a CodePush update running, and if
// so, register it with the HockeyApp SDK (https://github.com/slowpath/react-native-hockeyapp)
// so that crash reports will correctly display the JS bundle version the user was running.
codePush.getUpdateMetadata().then((update) => {
if (update) {
hockeyApp.addMetadata({ CodePushRelease: update.label });
}
});
// Check to see if there is still an update pending.
codePush.getUpdateMetadata(UpdateState.PENDING).then((update) => {
if (update) {
// There's a pending update, do we want to force a restart?
}
});
codePush.notifyAppReady
codePush.notifyAppReady(): Promise<void>;
通知CodePush,一個更新安裝好了。當你檢查并安裝更新,(比如沒有使用sync方法去handle的時候),這個方法必須被調用。否則CodePush會認為update失敗,并rollback當前版本,在app重啟時。
當使用sync
方法時,不需要調用本方法,因為sync
會自動調用。
codePush.restartApp
codePush.restartApp(onlyIfUpdateIsPending: Boolean = false): void;
立即重啟app。
當以下情況時,這個方式是很有用的:
- app 當 調用
sync
或LocalPackage.install
方法時,指定的install mode
是ON_NEXT_RESTART
或ON_NEXT_RESUME時
。 這兩種情況都是當app重啟或resume
時,更新內容才能被看到。 - 在特定情況下,如用戶從其它頁面返回到APP的首頁時,這個時候調用此方法完成過更新對用戶來說不是特別的明顯。因為強制重啟,能馬上顯示更新內容。
總結
上文已經介紹了CodePush在動態更新方面的一些特性,但CodePush也存在著一些缺點:
- 服務器在國外,在國內訪問,網速不是很理想。
- 其升級服務器端程序并不開源的,后期微軟會不會對其收費還是個未知數。
如果在沒有更好的動態更新React Native應用的方案的情況下,并且這些問題還在你的接受范圍之內的話,那么CodePush可以作為動態更新React Native應用的一種選擇。
后期會向大家分享不采用CodePush,自己搭建服務器并實現React Native應用的動態更新相關的方案。
參考:
http://microsoft.github.io/code-push/docs/getting-started.html
https://github.com/Microsoft/react-native-code-push