一、熱更新的介紹
很多開發(fā)技術(shù)中,都會有熱更新的說法:
熱更新、熱啟動中的熱一般是指不停機/不停APP,或者說不重啟。
- 服務(wù)器中的熱更新:不需要關(guān)閉服務(wù)器,直接重新部署項目就行。冷的自然就是關(guān)閉服務(wù)后再操作。
- 移動端的熱啟動、冷啟動,這里熱就表示APP/服務(wù)正在運行中的狀態(tài)。
- 客戶端中的熱更新,稍微擴(kuò)展了一下,表示不需要重新安裝新版本的APP,用戶下載安裝APP之后,打開App時可以即時更新。
1.1 蘋果對熱更新的政策
蘋果允許使用熱更新Apple's developer agreement, 但是規(guī)定不能彈框提示用戶更新,影響用戶體驗。 Google Play也允許熱更新,但必須彈框告知用戶更新。在中國的android市場發(fā)布時,都必須關(guān)閉更新彈框,否則會在審核應(yīng)用時以“請上傳最新版本的二進(jìn)制應(yīng)用包”駁回應(yīng)用。
如何看待蘋果禁止 JSPatch 等 iOS APP 熱更新方案?
蘋果禁止的是“基于反射的熱更新“,而不是 “基于沙盒接口的熱更新”。而大部分的應(yīng)用框架(如 React-Native)和游戲引擎(比如 Unity ,Cocos2d-x,白鷺引擎等)都屬于后者,所以不在被警告范圍內(nèi)。
蘋果為什么要禁止 JSPatch 等熱更新技術(shù)?
JSPatch 的原理是,開發(fā)者編寫 JavaScript 代碼,利用蘋果內(nèi)置的 JavaScriptCore.Framework 執(zhí)行,以實現(xiàn)熱更新功能。這一點看似也符合標(biāo)準(zhǔn),但是在技術(shù)上,存在著重大安全隱患,參考 JSPatch 的業(yè)務(wù)邏輯:
require('UIView')
var view = UIView.alloc().init()
view.setBackgroundColor(require('UIColor').grayColor())
view.setAlpha(0.5)
簡單理解,JSPatch可以理解為所有的 Objective-C 的 API 進(jìn)行了映射,允許開發(fā)者在 JS 端調(diào)用任意原生代碼。這顯然是極其危險的。假設(shè)這段代碼是通過熱更新技術(shù)下載執(zhí)行的,如果在中間存在黑客,把這段代碼動態(tài)替換掉,比如修改為獲取用戶通訊錄并上傳到黑客的服務(wù)器,就會造成重大的安全問題。
為什么游戲熱更新技術(shù)可以被理解為是安全的
與 JSPatch 不同的是,游戲熱更新技術(shù)主要的實現(xiàn)方式是把動態(tài)腳本下載之后,讓動態(tài)腳本調(diào)用游戲引擎提供的接口實現(xiàn)缺陷修復(fù)。與 JSPatch不同的是,動態(tài)腳本并不能任意調(diào)用全部原生代碼,而是只能根據(jù)游戲引擎提供的接口調(diào)用相關(guān)功能。<font color='red'>本身能夠調(diào)用的功能是確定、有限的,而不是不確定、任意的系統(tǒng)API。</font>
在這個過程中,游戲引擎的原生端作為一個安全沙箱,提供了一個安全的保護(hù)層,只要游戲引擎不要對外提供獲取通訊錄的接口,黑客就無法通過替換動態(tài)腳本的方式獲取用戶的隱私資料。進(jìn)而可以被認(rèn)為是安全的,自然不在蘋果的禁止范圍內(nèi)。
1.2 客戶端熱更新的方案
目前針對react native 熱更新的方案比較成熟的選擇有 React Native 中文網(wǎng)的 Pushy、微軟的 CodePush 和用來搭建私服的 code-push-server。
二、CodePush
2.1 介紹
CodePush 是微軟的一項云服務(wù),使 Cordova 和 React Native 開發(fā)人員能夠?qū)⒁苿討?yīng)用程序的更新直接部署到他們用戶的設(shè)備上。它充當(dāng)中央存儲庫,開發(fā)人員可以向其發(fā)布某些更新(例如,JS、HTML、CSS 和圖像更改),并且應(yīng)用程序可以從中查詢更新(使用提供的客戶端 SDK)。使得你在處理bug、添加小功能時,不需要重新構(gòu)建二進(jìn)制文件,或者通過任何公共應(yīng)用商店重新發(fā)布。讓你擁有一個與你的最終用戶更確定和直接的互動模型。
2020年,CodePush is moving to App Center。Visual Studio App Center 將 CodePush 的強大功能與云托管構(gòu)建、自動化 UI 測試、崩潰報告、分析和推送服務(wù)相結(jié)合。
客戶端的命令行工具,也從 code-push-cli 更換成了 appcenter-cli 。前者的最終版本為3.0.0,之后不再提供支持。
- Visual Studio App Center 命令行界面 (CLI) 是從命令行運行 App Center 服務(wù)的統(tǒng)一工具。 我們的目標(biāo)是為我們的開發(fā)人員提供一個簡潔而強大的工具,讓他們可以使用 App Center 服務(wù)并輕松編寫他們想要執(zhí)行的一系列命令的腳本。 您可以在 App Center 中登錄并查看或配置您有權(quán)訪問的所有應(yīng)用程序。
CodePush的優(yōu)點:除了滿足基本更新功能外,還有統(tǒng)計,hash計算容錯和補丁更新功能。微軟的項目,大公司技術(shù)有保障,而且開源。近幾年微軟在擁抱開源方面,讓大家也是刮目相看。
2.2 code-push-server
默認(rèn)code-push 使用的服務(wù)器地址為微軟的服務(wù)器,但考慮到代碼安全、微軟在中國的速度等,我們需要使用 code-push-server 搭建自己的 服務(wù)器。
code-push-server支持以下存儲模式:
- 本地:storage bundle file in local machine
- 七牛: storage bundle file in qiniu
- s3(亞馬遜簡易存儲服務(wù)): storage bundle file in aws
- oss(阿里云對象存儲 Objec Storage Service): storage bundle file in aliyun
- 騰訊云: storage bundle file in tencentcloud
三、React-Native集成熱更新
3.1 大致流程與所需工具
流程圖:
由于我是在開發(fā)一個實驗性項目,所以工程化不完善,借用的網(wǎng)友公司的熱更新大致流程,如有不妥,麻煩評論一下,我刪除~

環(huán)境
- React-Native:'0.64.2'
工具:
-
react-native-cli:react-native命令行工具,安裝后可以在終端使用
react-native命令。用于RN項目的初始化、本地調(diào)試、bundle及資源文件打包。本機中非全局安裝,npx調(diào)用。 - code-push-server 微軟云服務(wù)在中國太慢,可以用它搭建自己的服務(wù)端。
-
code-push-cli :連接微軟云端,管理發(fā)布更新版本的命令行工具,安裝后可以在終端使用
code-push命令 - react-native-code-push 集成到react-native項目
3.2 code-push-server 搭建私服
code-push-server 是個服務(wù)器上的工具,可以讓我們搭建自己的 CodePush 服務(wù),有兩種集成方式:
- docker集成(推薦)
- 手動操作
HOW TO INSTALL code-push-server 文檔很清楚,不是重點,略
注意因為code-push-server 是個人維護(hù)的,已經(jīng)好久沒更新,看 issue 有人說不支持 code-push-cli 3.0版本,要使用 2.1.9 版本,react-native-code-push 倒是沒限制,直接用的當(dāng)前最新的 7.0.1 版本(2021.08.26日)。
3.3 開發(fā)工作流
3.3.1 分支管理
每個熱更新版本都需要在一個新的分支上開發(fā),同時此分支也是版本開發(fā)完成后發(fā)布更新的分支。
分支名可以遵循如下規(guī)則,如:release/20190926_1.8.1.2_newActivity

不過如果不想這么麻煩,直接以版本號命名也可以。單獨維護(hù)一個 README.md 來記錄版本迭代信息。
3.3.2 變量替換
在業(yè)務(wù)完成后,開發(fā)者需要打包App交由測試人員測試。熱更發(fā)布通常需要開發(fā)人員提供三種包:
- QA環(huán)境的測試包
- 線上環(huán)境的測試包
- 線上環(huán)境的生產(chǎn)包
所以在每次打包之前,需要執(zhí)行腳本,根據(jù)參數(shù)來替換代碼中的Key值,如執(zhí)行npm run build --dev,會將CodePush的key和host指向qa環(huán)境。
3.3.3 打包靜態(tài)資源
執(zhí)行 react-native bundle 命令可以將js代碼打包成jsbundle文件,也可將靜態(tài)文件如圖片打包到文件夾中。
react-native bundle --platform ios
--entry-file index.js # 從index.js為入口
--bundle-output ./bundles/ios/main.jsbundle # 將打包的jsbundle輸出到 ./bundles/ios/main.jsbundle 文件
--assets-dest ./bundles/ios # 將靜態(tài)文件輸出到 ./bundles/ios 目錄下
--verbose
--dev false # 打包環(huán)境為生產(chǎn)環(huán)境。--dev默認(rèn)是true。如果為false,則禁用警告并縮小包。
注意:./bundles/ios 文件夾可以隨意指定更改,但要提前創(chuàng)建好目錄,否則會報錯。
這里打包輸出的jsbundle最終會上傳到code push服務(wù)端用于App端對比更新。
在開發(fā)端打包靜態(tài)資源主要是為了節(jié)省發(fā)布更新的時間,當(dāng)然總時間是不變的,(優(yōu)化了發(fā)布系統(tǒng)的體驗而已)
3.3.4 推送代碼
開發(fā)者將代碼推送到代碼服務(wù)器。
3.4 熱更新的發(fā)布和管理
3.4.1 直接使用code-push-cli
code-push-cli 完成應(yīng)用的創(chuàng)建、應(yīng)用更新的版本。相當(dāng)于是一個CLI形式的管理后臺。 npm文檔。
npm install code-push-cli@2.1.9 -g
常用code-push命令
# 注冊賬號
code-push register
# 登陸 在彈出的網(wǎng)頁中登錄,默認(rèn)賬號:admin, 默認(rèn)密碼:123456,然后獲取token,將token復(fù)制到控制臺中登錄即可。
code-push login <url:host>
# 顯示登陸的token
code-push access-key ls
# 注銷
code-push logout
# 添加項目 創(chuàng)建項目時,默認(rèn)會生成兩套部署環(huán)境:Staging(分階段)、Production
code-push app add <appName> <os> <platform>
code-push app add CodePushDemoIos ios react-native
code-push app add CodePushDemoAndroid android react-native
# 重命名應(yīng)用
code-push app rename <appName> <newAppName>
# 列出賬號下的所有項目
code-push app list
# 刪除項目
code-push app remove <appName>
# 部署一個環(huán)境
code-push deployment add <appName> <deploymentName>
# 重命名部署
code-push deployment rename <appName> <deploymentName> <newDeploymentName>
# 列出應(yīng)用的部署
code-push deployment ls <appName> # 加上 -k 參數(shù),將各個部署的 key 顯示出來
# 刪除部署
code-push deployment rm <appName> <deploymentName>
# 查看特定應(yīng)用程序部署的50個最新版本的歷史記錄
code-push deployment history <appName> <deploymentName>
# 無法刪除單個版本,可以使用以下命令清除與部署關(guān)聯(lián)的整個版本歷史記錄. 運行此命令后,客戶端設(shè)備將不再接收已清除的更新。此命令是不可逆的,因此不應(yīng)在生產(chǎn)部署中使用。
code-push deployment clear <appName> <deploymentName>
1. 創(chuàng)建應(yīng)用
# 添加項目 創(chuàng)建項目時,默認(rèn)會生成兩套部署環(huán)境:Staging(分階段)、Production
code-push app add <appName> <os> <platform>
code-push app add CodePushDemoIos ios react-native
code-push app add CodePushDemoAndroid android react-native
2. 發(fā)布新更新 release
code-push release <appName>
<updateContents> # 指定應(yīng)用更新的資源和代碼的位置就是打包后的jsbundle位置。 如 `/opt/www/bundle/ios`
<targetBinaryVersion>
[--deploymentName <deploymentName>]
[--description <description>]
[--disabled <disabled>] # 指定了最終用戶是否可以下載更新。 如果未指定,更新將不會被禁用
[--rollout <rolloutPercentage>] # 指定可以更新的用戶百分百,取值在1-100。默認(rèn)為100
[--mandatory] # 是否強制更新 強制更新參數(shù)有一個`動態(tài)轉(zhuǎn)換`的過程,假如用戶現(xiàn)在安裝了v1版本,服務(wù)端更新了v2版本是強制更新,
# 過后又上傳了不是強制更新的v3,這是用戶下載v3,v3就會變成強制更新(因為v2是強制更新的),這就是強制更新的動
# 態(tài)轉(zhuǎn)換.
targetBinaryVersion: 目標(biāo)二進(jìn)制的版本號,它的可選值規(guī)則如圖

如果元數(shù)據(jù)文件中的二進(jìn)制版本缺少補丁版本,例如 2.0,它將被視為補丁版本為 0,即 2.0 -> 2.0.0。 對于等于純整數(shù)的二進(jìn)制版本也是如此,在這種情況下,1 將被視為 1.0.0。
3. 發(fā)布新更新 release-react
此命令用于一鍵發(fā)布,其實是將react-native bundle命令和code-push release命令結(jié)合起來使用。
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>]
4. 補丁更新(patch)
在發(fā)布更新之后,如果想要修改此次更新的參數(shù)可以使用patch命令(給更新打補?。?,如:你想增加更新的首次展示百分比。
code-push patch MyApp Product --label v10 --rollout 100
code-push patch <appName> <deploymentName>
[--label <releaseLabel>]
[--mandatory <isMandatory>]
[--description <description>]
[--rollout <rolloutPercentage>]
[--disabled <isDisabled>]
[--targetBinaryVersion <targetBinaryVersion>]
- label:指定的部署環(huán)境里更新哪個發(fā)布版本(如:v10)
5. 促進(jìn)更新(promote)
有一個場景, 當(dāng)我們在線上的Staging環(huán)境下測試完畢后,我們可以執(zhí)行promote命令將之推進(jìn)到Product環(huán)境,而不是重新執(zhí)行release命令,然后重新設(shè)置參數(shù)。我們只需執(zhí)行promote命令進(jìn)行一個拷貝即可。
code-push promote <appName> <sourceDeploymentName> <destDeploymentName>
[--description <description>]
[--disabled <disabled>]
[--mandatory]
[--rollout <rolloutPercentage>]
[--targetBinaryVersion <targetBinaryVersion]
使用promote命令的優(yōu)勢
- 速度更快,不需要重新裝配資源
- 可靠性高,不會出錯,因為這只是一個推進(jìn)的過程
6. 回滾更新(rollback)
當(dāng)某個版本出現(xiàn)重大問題時,需要將版本回滾到老的正常版本去,可以使用rollback命令
code-push rollback <appName> <deploymentName> [--targetRelease/-r <label>]
code-push rollback MyApp Production --targetRelease v10
targetRelease參數(shù)指定需要回滾的版本,默認(rèn)為上個版本。
3.4.2 搭建GUI管理后臺
微軟的 CodePush 提供了 CodePush Management SDK(Node.js) 。其是一個JavaScript庫,用于以編程方式管理CodePush帳戶(例如創(chuàng)建應(yīng)用程序、發(fā)布更新版本),該庫允許編寫基于Node.js的構(gòu)建和/或部署腳本,而無需使用CLI。
1. 搭建服務(wù)端
基于CodePush Management SDK搭建一個node的Http服務(wù),為熱更新發(fā)布后臺管理系統(tǒng)提供服務(wù)。使用示例:
npm i code-push -S
var CodePush = require("code-push");
var codePush = new CodePush("YOU_ACCESS_KEY", null, "YOUR_CODE_PUSH_HOST");
/**
* 獲取歷史部署
* @param {string} appName
* @param {string} deploymentName
*/
async function getDeploymentHistory(appName, deploymentName) {
let rs = await codePush.getDeploymentHistory(appName, deploymentName);
console.log(rs);
}
getDeploymentHistory("YOUR_APP_NAME", "Production")
通過上方代碼就可以直接使用此服務(wù)了,這里說以下很坑的點。
在官方文檔中YOU_ACCESS_KEY的值是通過code-push access-key add "YOU_ACCESS_KEY"來的,但通過實驗發(fā)現(xiàn)此key無效。并輸出錯誤401 Unauthorized
解決:執(zhí)行
cat ~/.code-push.config,使用輸出的accessKey作為YOU_ACCESS_KEY
繼續(xù)執(zhí)行還是輸出了錯誤:
The session or access key being used is invalid; please run “code-push login” again. If you are on an older version of the CLI, you may need to run “code-push logout” first to clear the session cache.
這個問題我在github上查了很久都沒有答案,最后翻看源碼終于發(fā)現(xiàn)了問題所在,CodePush構(gòu)造函數(shù)的第三個參數(shù)接收的是你的
codepush服務(wù)所在的地址,國內(nèi)的環(huán)境想要使用微軟的code-push云服務(wù)也會有很多問題。所以都在自己的服務(wù)器上搭建,所以會遇到此問題,而國外的程序員一般來說都是使用微軟提供的云服務(wù)所以沒有碰到相關(guān)問題,所以在使用時給第三個參數(shù)傳入自己code push地址即可。
new CodePush("test", null, "http://127.0.0.1:3000")
具體這個基于node的服務(wù)如何搭建取決于你們公司的實際情況而定。

此發(fā)布熱更系統(tǒng)基本已經(jīng)包含了所有常用的熱更新功能,包括了最常用的release、patch、promote、rollback命令。
2. 版本號設(shè)計
在熱更系統(tǒng)中維護(hù)一個版本號,開發(fā)者希望這個版本號能夠反映出對應(yīng)的二進(jìn)制包的版本如2.2.0,同時亦能對應(yīng)到熱更的版本號。
在code push的服務(wù)端執(zhí)行code-push deployment ls appName能查看到如下部署信息

顯然這個V9版本號不能滿足目前的需求,需要自己設(shè)計一套版本號規(guī)則,同時和這個V9對應(yīng)起來。
最終的版本號 = 二進(jìn)制版本號 + 熱更新版本號,如這版熱更是針對1.8.1版本的二進(jìn)制包發(fā)布的第三個熱更版本,則最終版本號為1.8.1.3。
在App內(nèi)部通過維護(hù)此版本號幫助快速定位版本問題version = 1.8.1.3,同時會在個人中心展示此版本號,同時在接口中帶上此版本號。
3. 版本號對應(yīng)
上面設(shè)計了一個新的版本號來代替 code push 提供的 V9,但是最終還是需要為這兩個版本號建立對應(yīng)關(guān)系,才能保證系統(tǒng)的正常運行,比如需要回滾某個有嚴(yán)重 bug 的 1.9.0.5 版本到 1.9.0.4,最終需要執(zhí)行
code-push rollback MyApp Production --targetRelease v34
這里需要建立1.9.0.4和v34版本的一一對應(yīng)。
新建version_control數(shù)據(jù)表存儲此關(guān)系。
每次發(fā)布新版的熱更新時,運營人員只需要選澤熱更的二機制的版本即可
1.8.0,后續(xù)的最終版本號由系統(tǒng)按照熱更版本自動加一的規(guī)則自動生成。
4. 查看發(fā)布?xì)v史
運營人員通過版本號等條件可以查看發(fā)布?xì)v史信息,版本歷史相關(guān)數(shù)據(jù)如下:
{
description: '',
isDisabled: false,
isMandatory: false,
rollout: 100,
appVersion: '1.8.0',
packageHash: '0e616848b4ac4f77617fe51d2c7271dfdde1cad2fa478b2d2b75c6f9d274ae02',
blobUrl: 'http://192.168.1.1:3000/download/fj/Fji6Hx1buh-sd7f6o9x2BtCLv4MT',
size: 1656590,
manifestBlobUrl: 'http://192.168.1.1:3000/download/fs/FswRb5CD9YCbMjzdyQ3EQYI7QUC7',
diffPackageMap: null,
releaseMethod: 'Upload',
uploadTime: 1567362630000,
originalLabel: '',
originalDeployment: '',
label: 'v5'
}
3.5 客戶端檢查更新
3.5.1 集成 react-native-code-push
官方文檔。與所有其他 React Native 插件一樣,iOS 和 Android 的集成體驗不同,因此請根據(jù)您的目標(biāo)平臺執(zhí)行以下設(shè)置步驟。(Android略)
npm install --save react-native-code-push@latest #安裝 react-native-code-push 至 RN 項目
在0.6之前,React Native庫需要使用 rnpm 進(jìn)行Link。不支持 rnpm 的還需要手動集成。
0.60之后是采用 CocoaPods 管理的相關(guān)依賴。
1. pod install
運行cd ios && pod install && cd ..以安裝所有必需的CocoaPods依賴項。
2. 修改 URLForBridge
修改 AppDelegate.m 中的 sourceURLForBridge 方法:
// 打開 AppDelegate.m 文件,并為CodePush標(biāo)頭添加導(dǎo)入語句:
#import <CodePush/CodePush.h>
- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge
{
#if DEBUG
return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index" fallbackResource:nil];
#else
// 為生產(chǎn)版本設(shè)置 bridge 的源URL
// -- return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];
return [CodePush bundleURL];
#endif
}
此更改將您的應(yīng)用配置為始終加載應(yīng)用的 JS 包的最新版本。在第一次啟動時,這將對應(yīng)于使用應(yīng)用程序編譯的文件。但是,在通過 CodePush 推送更新后,這將返回最近安裝的更新的位置。
注意:
- bundleURL 方法假定您的應(yīng)用程序的 JS 包被命名為 main.jsbundle。如果您已將應(yīng)用程序配置為使用不同的文件名,只需調(diào)用
bundleURLForResource:方法(假設(shè)您使用的是 .jsbundle 擴(kuò)展名)或bundleURLForResource:withExtension:方法,以覆蓋該默認(rèn)行為。 - 通常,您只想使用 CodePush 來解析發(fā)布版本中的 JS 包位置,因此,我們建議使用 DEBUG 預(yù)處理器宏在使用打包服務(wù)器和 CodePush 之間動態(tài)切換,具體取決于您是否調(diào)試與否。這將使確保您在生產(chǎn)中獲得所需的正確行為變得更加簡單,同時仍然能夠在調(diào)試時使用 Chrome 開發(fā)工具、實時重新加載等。
3. 設(shè)置部署環(huán)境的密鑰
CodePush 運行時,會根據(jù)指定的密鑰,針對對應(yīng)的部署環(huán)境查詢更新,
方法一:在 info.plist 中固定寫死
在 APP 的 Info.plist 文件中添加一個名為 CodePushDeploymentKey 的新條目,其值是針對此應(yīng)用程序配置的部署環(huán)境對應(yīng)的key。
可以通過 code-push deployment ls <appName> -k 來查看應(yīng)用每個部署環(huán)境的 key,(該 -k 標(biāo)志是必需的,因為默認(rèn)情況下不會顯示鍵),然后復(fù)制相對應(yīng)的 Deployment Key 即可。
請注意,使用部署的名稱(如 Staging)將不起作用。 該“友好名稱”僅用于 CLI 中經(jīng)過身份驗證的管理使用,而不用于你應(yīng)用程序中的公共使用。
- 如果需要動態(tài)使用不同的部署,還可以使用 Code-Push options 在JS代碼中覆蓋部署密鑰
方法二:多部署測試
為了有效利用與 CodePush 應(yīng)用程序一起創(chuàng)建的 Staging 和 Production 部署,請在實際將你的應(yīng)用程序?qū)?CodePush 的使用移入生產(chǎn)環(huán)境之前,進(jìn)行多部署測試的配置。
簡單來說,在 Info.plist 中添加名稱為 CodePushDeploymentKey 的字段,將值設(shè)置為各個部署環(huán)境的 key。詳細(xì)步驟,看文檔吧
方法三:動態(tài)部署分配
如果您希望能夠執(zhí)行 A/B 測試,或配置某些用戶提前訪問到新版本的應(yīng)用程序(灰度測試),那么能夠在運行時將特定用戶動態(tài)放置到特定部署中被證明是非常有用的。
為了實現(xiàn)這種工作流,您需要做的就是在調(diào)用 codePush 方法時指定您希望當(dāng)前用戶同步的部署 key。 指定后,此 key 將覆蓋應(yīng)用程序的 Info.plist (iOS) 或 MainActivity.java (Android) 文件中提供的“默認(rèn)” key。 這允許您生成用于 staging 或 production 的構(gòu)建,也能夠根據(jù)需要動態(tài)“重定向”。
// 假設(shè)“userProfile”是這個組件收到的一個 prop, 其中包括當(dāng)前用戶應(yīng)使用的部署密鑰。
codePush({deploymentKey: userProfile.CODEPUSH_KEY})(App)
codePush.sync({ deploymentKey: userProfile.CODEPUSH_KEY });
4. 修改服務(wù)器地址
步驟同多部署測試,然后在 Info.plist 中添加名稱為 CodePushServerURL 的字段,將值設(shè)置為各個環(huán)境的code-push服務(wù)器的地址(IP:host)。
5. 代碼簽名
文檔:從 CLI 2.1.0 版開始,您可以在發(fā)布期間對包進(jìn)行自簽名,并在安裝更新之前驗證其簽名。 有關(guān)代碼簽名的更多信息,請參閱相關(guān)的代碼推送文檔部分。
為了配置用于捆綁驗證的公鑰,您需要在 Info.plist 中添加名稱為 CodePushPublicKey 的字段和公鑰內(nèi)容的字符串值。
6. 調(diào)試/故障排除
sync 方法包括許多開箱即用的診斷日志記錄,因此如果您在使用它時遇到問題,最好首先嘗試檢查應(yīng)用程序的輸出日志。 這將告訴您應(yīng)用程序是否配置正確(例如插件能否找到您的部署密鑰?),如果應(yīng)用程序能夠訪問服務(wù)器,是否發(fā)現(xiàn)可用更新,是否成功下載/安裝更新, 等等。我們希望繼續(xù)改進(jìn)日志記錄,使其盡可能直觀/全面,因此如果您發(fā)現(xiàn)它令人困惑或遺漏任何內(nèi)容,請告訴我們。
查看這些日志的最簡單方法是添加標(biāo)志 --debug。 這將輸出一個被過濾為僅 CodePush 消息的日志流。 這使得識別問題變得容易,而無需使用特定于平臺的工具,或涉足潛在的大量日志。(code debug ios只支持模擬器,code debug android不限)

此外,還可以啟動 Chrome DevTools 控制臺、Xcode 控制臺 (iOS)、OS X 控制臺 (iOS) 和/或 ADB logcat (Android),并查找以 [CodePush] 為前綴的消息。
3.5.2 功能介紹
任何涉及到原生代碼的更改都不能通過 CodePush 分發(fā),必須通過商店進(jìn)行更新。
請注意,如果您同時針對兩個平臺,建議為每個平臺創(chuàng)建單獨的 CodePush 應(yīng)用程序。
1. 差異更新
Releasing Updates:CodePush 客戶端支持差異更新,因此即使每次更新時都發(fā)布了 JS bundle 和 assets ,最終用戶實際上只會下載他們需要的文件。 該服務(wù)會自動處理此問題,優(yōu)化最終用戶的下載。
2. 回滾功能
CodePush在實現(xiàn)發(fā)布敏捷性的同時,同時也實現(xiàn)了強大的回滾功能。
服務(wù)器端回滾:允許您在發(fā)現(xiàn)錯誤版本后阻止其他用戶安裝。
客戶端回滾:為了確保您的最終用戶始終擁有您的應(yīng)用程序的正常運行版本,該插件會維護(hù)一個先前更新的副本,以便在您不小心推送包含崩潰的更新時,它可以自動回滾。這樣,也保證不會在服務(wù)器端回滾之前,會導(dǎo)致用戶會被阻塞。
3.5.3 API — 檢查更新
react-native-code-push 由兩部分組成:
- JavaScript 模塊,可以
import/require,并允許應(yīng)用程序在運行時與服務(wù)交互(例如檢查更新,檢查有關(guān)當(dāng)前運行的應(yīng)用程序更新的元數(shù)據(jù))。官方文檔 - 原生 API(Objective-C 和 Java),它允許 React Native 應(yīng)用程序主機使用正確的 JS 包位置引導(dǎo)(bootstrap啟動)自身。
code-push的最簡單的檢查更新如下:
codePush(options: CodePushOptions)(rootComponent: React.Component): React.Component;
// 普通方式
import CodePush from "react-native-code-push";
class App extends React.Component {}
export default CodePush(App);
// ES7 裝飾器的方式加載
@CodePush
class App extends React.Component {}
export default App;
使用CodePush高階函數(shù)包裹根組件, 這樣會在每次啟動App時檢查,下載,安裝App。 使用高階組件可以實現(xiàn)App自動更新。
CodePush也可以接受一個檢查更新相關(guān)的配置對象CodePushOptions,使用如下:
import CodePush from "react-native-code-push";
class App extends React.Component {}
export default CodePush(CodePushOptions)(App);
3.5.4 API — CodePushOptions對象
CodePushOptions配置對象有如下屬性:
1. deploymentKey
指定要查詢更新的部署密鑰。一般來說 code-push 會從 info.plist 或者 MainActivity.java 文件中獲取,但是我們可以使用此屬性覆蓋文件中的key值。
2. checkFrequency
指定檢查更新的時間,可取值如下:
/*(默認(rèn)值) */
codePush.CheckFrequency.ON_APP_START // 當(dāng)app完全初始化時(或者更具體地說,當(dāng)根組件被掛載時)??梢岳斫鉃閼?yīng)用進(jìn)程啟動時
codePush.CheckFrequency.ON_APP_RESUME // 當(dāng)應(yīng)用程序重新進(jìn)入前臺(包含ON_APP_START的場景)
codePush.CheckFrequency.MANUAL // 禁用自動檢查更新,僅在調(diào)用sync方法時檢查
3. installMode、mandatoryInstallMode
兩者取值都是 CodePush.InstallMode ,表示應(yīng)用程序應(yīng)該何時安裝更新。
// 以下說的重啟restart the app,都是說的是刷新APP組件,不是整個應(yīng)用程序進(jìn)程重啟。
// 無論當(dāng)前是在任何頁面,更新后還是在當(dāng)前頁面,不過當(dāng)返回時就到了根頁面(App組件重新掛載嘛)。
// 如果就是在根頁面,會看到閃的一下刷新效果。
enum InstallMode {
// 安裝更新并立即重啟 app。此模式通常使用在提示用戶更新時,因為用戶在點擊更新后往往希望馬上看到更新,也常用于強制更新。
IMMEDIATE,
// 安裝更新,但不重啟 app 。當(dāng)程序下次啟動時會自然更新。
ON_NEXT_RESTART,
// 安裝更新,但不重啟 app,當(dāng)程序從后臺恢復(fù)后自然更新(也就是常用的resume事件)
// 當(dāng)應(yīng)用程序在后臺超過minimumBackgroundDuration秒后恢復(fù)到前臺,其實會相當(dāng)于重啟 codePush.restartApp 方法
ON_NEXT_RESUME,
// 應(yīng)用程序需要在后臺 minimumBackgroundDuration 秒后才開始安裝更新, minimumBackgroundDuration 默認(rèn)為0;
ON_NEXT_SUSPEND
}
installMode指定可選更新(沒有標(biāo)記為強制性)的安裝模式。默認(rèn)值:codePush.InstallMode.ON_NEXT_RESTARTmandatoryInstallMode:指定被標(biāo)記為強制更新的安裝模式。默認(rèn)為:codePush.InstallMode.IMMEDIATE
4. minimumBackgroundDuration
指定在重新啟動應(yīng)用程序之前應(yīng)用程序需要處于后臺的最小秒數(shù)。 此屬性僅適用于使用 InstallMode.ON_NEXT_RESUME 或 InstallMode.ON_NEXT_SUSPEND 安裝的更新,并且有助于更快地將更新呈現(xiàn)在最終用戶面前,而不會太突兀。 默認(rèn)為“0”,它具有在恢復(fù)后立即應(yīng)用更新的效果。
5. updateDialog
null // 默認(rèn)值,不展示對話框
任一真值 // 啟用具有默認(rèn)字符串的對話框
UpdateDialog // 傳入 UpdateDialog類型的對象 啟用對話框以及覆蓋一個或多個默認(rèn)字符串。
// 可以設(shè)置強制更新、可選更新時的描述文案、標(biāo)題、按鈕文字
根據(jù)地區(qū)和平臺不同,各大應(yīng)用市場對更新確認(rèn)框有不同限制,目前只有g(shù)oogle play需要更新確認(rèn)提示, app store和中國大陸應(yīng)用市場不允許彈更新確認(rèn)框。
一般如果需要做彈框提醒更新,往往會自定義彈框樣式,不會使用原本的彈框, 在啟動 app 時調(diào)用 codePush.checkForUpdate() 方法,在有更新時提醒更新。
6. rollbackRetryOptions
回滾重試機制允許應(yīng)用程序嘗試重新安裝先前回滾的更新。
null // 默認(rèn)值,具有禁用重試機制的效果
任一真值 // 啟用具有默認(rèn)設(shè)置的重試機制
RollbackRetryOptions // 傳入 RollbackRetryOptions 類型對象,啟用回滾重試以及覆蓋一個或多個默認(rèn)值。
3.5.5 API — codePush.常用方法
除了使用高階組件的方式檢查安裝更新,我們也可以使用調(diào)用方法的方式檢查更新, CodePush既是個方法,也是個namespace,其中定義了一些檢查更新相關(guān)的方法。一下都是使用 CodePush. 形式調(diào)用的。
1. sync()
/*
* codePush.sync方法是檢測更新、下載更新、安裝更新為一體方法,它接收三個參數(shù)。調(diào)用該方法即可自動更新
* @param option 為配置對象,和 CodePushOptions 一致, 只是沒有 checkFrequency 指定檢查時間方法,因為在調(diào)用sync方法后
馬上就會去檢查更新。
* @param statusDidChange 為更新過程狀態(tài)改變的回調(diào)函數(shù),
* @param downloadDidProgress 為從code-push服務(wù)器下載更新時定時調(diào)用的回調(diào)函數(shù),通常可以用于向用戶展示進(jìn)度。
*/
codePush.sync(option, statusDidChange, downloadDidProgress)
statusDidChange ((syncStatus: Number) => void)
statusDidChange回調(diào)會返回app的安裝更新情況, 每個階段都會觸發(fā),syncStatus一共有如下情況:
// 應(yīng)用程序與配置的部署完全一致
codePush.SyncStatus.UP_TO_DATE
// 已安裝可用更新,將在此函數(shù)返回后立即運行,或者在下次應(yīng)用程序恢復(fù)/重新啟動時運行,具體取決于installMode的值
codePush.SyncStatus.UPDATE_INSTALLED
// 應(yīng)用程序有一個可選的更新,最終用戶選擇忽略。(僅在updateDialog使用時適用)
codePush.SyncStatus.UPDATE_IGNORED
// 同步操作遇到未知錯誤
codePush.SyncStatus.UNKNOWN_ERROR
// 正在查詢code-push服務(wù)器以進(jìn)行更新
codePush.SyncStatus.CHECKING_FOR_UPDATE
// 有可用更新,并向最終用戶顯示確認(rèn)對話框(僅在updateDialog使用時適用)
codePush.SyncStatus.AWAITING_USER_ACTION
// 正在從服務(wù)器下載可用更新
codePush.SyncStatus.DOWNLOADING_PACKAGE
// 已下載更新,即將安裝
codePush.SyncStatus.INSTALLING_UPDATE
// 用法如下:
codePush.sync({...}, this.codePushStatusDidChange )
codePushStatusDidChange = syncStatus => {
switch(syncStatus) {
case CodePush.SyncStatus.CHECKING_FOR_UPDATE:
consloe.log("Checking for update.");
}
...
}
downloadDidProgress((progress: DownloadProgress) => void)
下載更新過程中定時調(diào)用此回調(diào)函數(shù), DownloadProgress參數(shù)是返回的進(jìn)度,其中包含了兩個屬性:
- totalBytes: 此次更新的從字節(jié)數(shù)
- receivedBytes: 當(dāng)前已經(jīng)接收的字節(jié)數(shù)
codePush.sync({...}, this.codePushStatusDidChange, this.codePushDownloadDidProgress)
codePushDownloadDidProgress = progress => {
console.log(progress)
}
在使用高階函數(shù)包裹根組件的方式中,也會有這兩個回調(diào), 只不過是以生命周期函數(shù)出現(xiàn)的, 用發(fā)是在App根組件中添加兩個生命周期方法, 用法如下。
import CodePush from "react-native-code-push";
class App extends React.Component {
codePushStatusDidChange(state){
//...
}
codePushDownloadDidProgress(progress){
//...
}
}
export default CodePush(App);
一般來說,我們使用高階函數(shù)或者sync方法配合一些配置已經(jīng)可以完成檢查更新的大部份需求, 但有時我們需要手動去控制整個過程(檢查更新, 下載更新, 安裝更新), 這時我們可能會用到下面的一些高級方法。
2. disallowRestart()
由于安裝了更新, 在下次啟動時安裝的更新會被應(yīng)用。 在這期間(安裝了更新但還未重啟),調(diào)用codePush.disallowRestart()可以禁止通過程序重啟App。
什么時候會用到此方法呢?當(dāng)您的應(yīng)用程序中的某個組件(例如有一個載入過程)需要確保在其生命周期內(nèi)不會發(fā)生最終用戶中斷時非常有用。
適用于當(dāng)installMode的值為IMMEDIATE,或ON_NEXT_RESUME,或者手動調(diào)用codePush.restart()方法時。也可以理解為codePush.disallowRestart()方法阻止codePush.restart()的調(diào)用。
在調(diào)用codePush.disallowRestart()方法后,仍然可以獲取和安裝更新, 但必須等待allowRestart方法被調(diào)用后才會重啟。
3. allowRestart()
允許因安裝更新而發(fā)生程序化重啟。如果之前調(diào)用了disallowRestart方法,導(dǎo)致有需要重啟的更新 未重啟(被掛起),那么調(diào)用 allowRestart 方法將立即重啟程序。
如果在 allowRestart() 之前:
- 這期間沒有更新,所以無需重啟
- installMode 為 ON_NEXT_RESTART (下次啟動更新), 所以無需重啟
- installMode 為 ON_NEXT_RESUME,但程序一直在前臺,所以無需重啟
- 這期間沒有調(diào)用過 restartApp() 方法
class App extends Component {
componentWillMount(){
// 組件活動狀態(tài)不允許重啟
codePush.disallowRestart();
}
componentWillUnmount(){
// 組件卸載時可以運行重啟更新了
codePush.allowRestart();
}
//...
}
4. checkForUpdate()
/*
* 用于查詢code-push服務(wù)器是否有可用更新,
* @param deploymentKey 可用于覆蓋配置文件中的key
* @param handleBinaryVersionMismatchCallback 第二個為查詢的回調(diào)函數(shù)。
*/
codePush.checkForUpdate(deploymentKey, handleBinaryVersionMismatchCallback)
handleBinaryVersionMismatchCallback 返回一個promise表示查詢結(jié)果, 有兩種情況:
- null 表示無更新 可能是如下幾種情況造成的:
- 服務(wù)器上該部署還沒有任何版本
- 配置部署的二進(jìn)制版本和當(dāng)前用戶版本不一致(二進(jìn)制版本更新需重新上傳應(yīng)用商店)
- 已經(jīng)是最新版本
- 部署中的版本被標(biāo)記為禁用
- 部署中的最新版本是活動部署狀態(tài),當(dāng)前用戶不在百分百范圍內(nèi)(也就是灰度發(fā)布)
- 可用的更新實例RemotePackage (遠(yuǎn)端包的實例)。這個實例中包含了一些包的基礎(chǔ)信息和下載信息, 另外提供了一個下載方法,用于我們調(diào)用此方法下載更新。具體如下:
- appVersion: 二進(jìn)制包的版本號
- deploymentKey: 秘鑰
- packageSize: 包的大小
- downloadUrl: 包的地址
- download(downCallBack ? function) : Promise, 下載的回調(diào)。
- 將遠(yuǎn)端的包下載到本地后,可以拿到LocalPackage本地包的實例;
- 本地包實例包含了和LocalPackage包相似的屬性方法, 另外提供了一個install方法用于安裝更新。
- 其他的屬性不說了…
codePush.checkForUpdate().then(update => {
if (!update) {
console.log("上面那五種失敗情況之一");
} else {
console.log("有可用更新");
// 下載遠(yuǎn)端的包到本地。
update.download(this.downCallBack);
}
});
5. notifyAppReady()
調(diào)用此方法通知codePush服務(wù)器新的安裝已經(jīng)成功,此方法用在手動下載更新時,如果沒有調(diào)用此方法通知,那么在下一次啟動app時,code-push服務(wù)器會認(rèn)為上一次安裝失敗了,然后會回滾更新。 在使用sync方法或者高階函數(shù)時不需要調(diào)用此方法。
6. getUpdateMetadata()
/**
* 檢索已安裝更新的元數(shù)據(jù) (比如 description, isMandatory, appVersion, deploymentKey等).
* @param updateState 默認(rèn)是 UpdateState.RUNNING ,表示獲取用戶當(dāng)前正在運行的更新版本的信息
*/
function getUpdateMetadata(updateState?: UpdateState) : Promise<LocalPackage|null>;
7. restartApp()
立即重啟應(yīng)用程序, 但有可能被阻止。
3.6 未完待續(xù)
從開發(fā)者端、熱更新發(fā)布端、熱更新服務(wù)端、App端分析了Code Push的熱更流程,以及每個環(huán)節(jié)應(yīng)該做什么事情,這其中涉及到的點主要有:
- 開發(fā)環(huán)境搭建和發(fā)布前準(zhǔn)備
- 熱更新版本號的設(shè)計和對應(yīng)關(guān)系
- 依賴于CodePush Management SDK的發(fā)布系統(tǒng)設(shè)計
- App端采用的更新模式選則
還差什么?
- 監(jiān)控和報警系統(tǒng)(大面積更新失敗等嚴(yán)重問題)