零客戶端開發經驗 React Native 熱更新 CodePush 打包整合指北

rccoder發表於2017-05-09

本文永久地址:github.com/rccoder/blo…,其他平臺可能不是最新文章。文章評論等也希望去原文進行。

一、背景

我的畢業設計是用 React Native 寫一款校園 APP,服務端採用 egg + MongoDB。

選用 React Native 一來是想借助他更加的學習鞏固 React、Redux 生態系統;二來是做成 APP 而不是網站會在老師面前顯得不是那麼的 Low,同時藉助雙平臺為忽悠填一份色彩;三來是 React Native 確實在效能上是優於 H5,不需要 XX 核心(如 UC、QQ等)來抹平雜亂機型的效能與相容問題,同時還能和 H5 一樣保持熱更新。

為什麼不用 Weex?

曾經做過簡單的 Weex 開發,不使用 Weex 有以下幾點:一是定題目的時候(沒錯,題目中就有 React Native) Weex 的上層 DSL 還只有 Vue,當時尚未出現 Rax;二來是相比於 React Native,Weex 的生態與社群還比較年輕,害怕自己跳進去爬不出來導致畢業延期。

如果感興趣的話,程式碼在這裡:

二、熱更新原理

2.1、引言

React Native 的原理大致是上層寫 React 式的程式碼,然後利用相關的 loader 打包成 bundle 和相關的靜態檔案,然後利用 Android、iOS 裡的 SDK 解析 bundle,然後以 Native 的方式執行。

在大型 APP 中,針對 H5 中的靜態檔案會設定離線包,以達到秒出加速的目的,當然離線包的增大會導致 APP 體積的增大。針對一些場景(比如營銷頁面等),並不希望離線載入,這樣的場景就不使用離線包進行載入。

離線包也需要進行更新,這裡就需要一套比較完善的更新機制來保證(大公司自己造,小公司找有沒有開源的)。

2.2、正文

熱更新指的就是離線包(React Native 中的 bundle)更新的這個機制。我們可以在 APP 執行的過程中 “偷偷” 下載 bundle,然後在下次 APP 開啟的時候(或者某種自定義的時機,比如:彈窗提示、直接重啟等)使用新的 bundle。

三、CodePush 介紹

如何去維護這樣一個比較完善的更新機制呢?Microsoft 給出了一個很好的答案 —— CodePush。幸運地,他還沒被“牆“,我們可以直接的使用他的服務。

CodePush 整合了微軟的一個雲伺服器,他相當於是一箇中心釋出器,APP 可以詢問他是否有新的 bundle 更新,然後進行下載等操作。

CodePush 官方提供了 React Native 的整合方案,可以比較容易的整合到原有程式碼中(當然,也存在一些坑或者其他的地方,這也是本來出現的目的之一)。

四、實施流程

1、CodePush 服務配置

CodePush 的官網在

1.1、安裝 CodePush CLI

npm install -g code-push-cli複製程式碼

1.2、註冊 CodePush

code-push register複製程式碼

會自動開啟瀏覽器彈出註冊介面,註冊完成之後會得到一個 token,複製後填寫在 Terminal 裡面即可。

1.3、註冊應用

code-push app add WeHIT-Android
code-push app add WeHIT-iOS複製程式碼

每個 APP 都會得到 ProductionProduction 狀態的兩個 Key:

零客戶端開發經驗 React Native 熱更新 CodePush 打包整合指北

為什麼要註冊兩個 APP

CodePush 在釋出新 bundle 的時候目前只能一個一個平臺發,為了保證你的 history 看起來不是那麼的亂,建議直接分成兩個 APP 處理

2、SDK 整合

SDK 的整合包含三部分,分別是 JS 原始碼、iOS 客戶端、Android 客戶端。

SDK 直接用 CodePush 官方釋出的 react-native-code-push 即可。

值得注意是,react-native-code-push 最近釋出的 2.0-beta 版本,而改版本只支援 React Native 0.43 及其以上的版本中。如果是 0.43 以下版本的需要使用 1.17.x(最新 1.17.4-beta)。

零客戶端開發經驗 React Native 熱更新 CodePush 打包整合指北

我這裡使用的 RN 版本是 0.40,所以安裝 1.17.4-beta 版本的 react-native-code-push。

yarn add react-native-code-push@1.17.4-beta複製程式碼

在進行下面的三個之前,我們先用 React Native 使用的 rnpm(React Native Package Manage) link 以下這個包 (自動改變 ios 和 android 目錄下的一些檔案)。

npm install rnpm -g
rnpm link react-native-code-push複製程式碼

期間會提示你輸入 iOS 和 Android 端的 key,這裡輸入你用 CodePush 註冊 APP 時產生的 key,這裡我們先可以都輸入每個 Staging 的 key。

如果不幸你忘記了之前產生的 key,可以輸入以下的命令檢視:

code-push deployment ls WeHIT-Android複製程式碼

2.1、React Native 原始碼整合

在 React Native 原始碼中,我們需要引入 react-native-code-push,然後用 CodePush 包裹一下最外層的元件。

沒有 CodePush 之前我們的程式碼這樣寫:

// index.ios[android].js
...
import { AppRegistry } from 'react-native';
import AppContainer from './src'
AppRegistry.registerComponent('WeHIT', () => AppContainer);

// src/index.js

export default function AppContainer () {
  return (
    <Navigator
      initialRoute={routeMap.home}
      configureScene={configureScene}
      renderScene={renderScene} />   
  )複製程式碼

現在我們只需要改動一下 src/index.js,用 CodePush 包裹以下這個元件即可:

// src/index.js

import codePush from "react-native-code-push";

class App extends Component{

  componentDidMount() {}

  render() {
    return (
      <Navigator
        initialRoute={routeMap.intro}
        configureScene={configureScene}
        renderScene={renderScene} />
    )
  }
}

/**
 * Configured with a MANUAL check frequency for easy testing. For production apps, it is recommended to configure a
 * different check frequency, such as ON_APP_START, for a 'hands-off' approach where CodePush.sync() does not
 * need to be explicitly called. All options of CodePush.sync() are also available in this decorator.
 */
let codePushOptions = { checkFrequency: codePush.CheckFrequency.ON_APP_RESUME };

const AppContainer = codePush(codePushOptions)(App);

export default AppContainer;複製程式碼

這裡 checkFrequency 定義了何時進行 bundle 更新,這裡 ON_APP_RESUME 是指在 APP 重新開啟的時候進行更新替換。更加詳細的情況可以參加 react-native-code-push 的 example 進行編寫。目前這樣僅僅是夠用。

為什麼這樣說?

聽說在 Android 端和 iOS 端對更新要求是不一樣的。Android 是要求你在更新的時候提示使用者更新,使用者需要點選確認之後才進行更新;而 iOS 端是希望這個更新是使用者無感知的(上面這種就行),默默的在後臺更新好並且使用最新的即可。

2.2、 iOS SDK 整合與打包

2.2.1、rnpm 自動配置

在上面使用 rnpm link 之後,其實大部分的問題都已經基本解決了。

這裡你需要開啟 git diff 看一下 rnpm link 有沒有做一些 ”壞事“ 可能會對你的程式碼造成影響。

這裡我們發現他改變了下面幾個檔案:

零客戶端開發經驗 React Native 熱更新 CodePush 打包整合指北

重點看一下是在可辨識的範圍內修改了 Info.plist,在裡面加入了 key 方便呼叫。

除此之外還重點修改了一下 AppDelegate.m

零客戶端開發經驗 React Native 熱更新 CodePush 打包整合指北

這是指在 debug 包中繼續之前的老套路遠端載入 bundle,在 release 模式中託管給 CodePush 管理 bundle 的載入。

對,這和我們之前預想的完全一樣。

接著,用 Xcode 開啟 iOS 工程,檢查一下版本號是不是三位,如果不是的話,修改為三位(比如:1.0.0):

為什麼要修改為三位

CodePush 只支援三位的,不然在推包的時候無法確定推包給哪個版本。

零客戶端開發經驗 React Native 熱更新 CodePush 打包整合指北

2.2.2、忽略 Test

這部分你可以先跳過,直接看 build 部分,如果有問題再回開看。

我在 build 的時候發生了錯誤,log 如下:

零客戶端開發經驗 React Native 熱更新 CodePush 打包整合指北

這看起來時測試出了問題,忽略掉他即可。

commmand + shift + ,,在彈出的框裡面選擇 build,然後取消掉 Test 部分的勾。

零客戶端開發經驗 React Native 熱更新 CodePush 打包整合指北

然後繼續 build。

2.2.3、打build 包

在之前的 AppDelegate.m 中,我們程式碼的意圖是在 debug 模式下通過遠端載入 bundle,在 release 模式下用 CodePush 來處理。這樣保證了我們在開發的時候不受 CodePush 的影響,所以要測試 CodePush,就打在 release 包。

command + shift + ,,在 run 裡面 Build Configuration 裡面選擇 release。

零客戶端開發經驗 React Native 熱更新 CodePush 打包整合指北

然後 command + R 或者點選按鈕進行打包。

理論上你會注意到打了一個不需要載入遠端 bundle 的包。

如何打包到物理機上

這裡不做討論,只做簡單的說明:首先需要一個開發者賬號,在專案工程的 Singning 裡面進行配置(不想買的話可以去淘寶看看),然後插上手機之後進行 build。

和其他 iOS 工程打包沒有什麼區別。

2.3、Android SDK 整合與打包

2.2.1、rnpm 自動配置

和 iOS 端一樣,利用 rnpm link 就能解決大部分的問題。rnpm link 之後,我們 git diff 一下看看有哪些變動:

零客戶端開發經驗 React Native 熱更新 CodePush 打包整合指北

核心是修改了上面的幾個檔案:

自動在 setting.gradle 裡面加入了 CodePush:

include ':react-native-code-push'
project(':react-native-code-push').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-code-push/android/app')複製程式碼

自動在 build.gradle 加入了 CodePush 的編譯依賴:

零客戶端開發經驗 React Native 熱更新 CodePush 打包整合指北

自動在 string.xml 裡面加入了 key,方便 java 程式碼中的呼叫。

自動在 MainActivity.java 裡面引入的 CodePush。

自動在 MainApplication.java 中重寫的 getJSBundleFile 函式。

零客戶端開發經驗 React Native 熱更新 CodePush 打包整合指北

到這裡,進行 run 的時候會提示找不到 bundle,出現下面的錯誤:

Caused by: com.microsoft.codepush.react.CodePushNotInitializedException: A CodePush instance has not been created yet. Have you added it to your app's list of ReactPackages?複製程式碼

模擬器也是紅紅的提示找不到 bundle。

這和我們猜測的一直,整個 rnpm link 之後的程式碼中沒有找到如何使用那個 key。

參考官方 example 中的程式碼,最終找到要在 MainApplication.java 中使用那個 Key:

零客戶端開發經驗 React Native 熱更新 CodePush 打包整合指北

那個 id 是 string.xml 裡面的 id。

這樣重新進行 run,可能會發現又提示:

Could not get BatchedBridge, make sure your bundle is packaged properly” on start of app複製程式碼

模擬是也是大大的紅色錯誤提示:

零客戶端開發經驗 React Native 熱更新 CodePush 打包整合指北

參考 React Native 的 issues#9336stackoverflow

不要使用 Android Studio 啟動,使用:

react-native run-android
react-native start --reset-cache複製程式碼

很好,能跑起來了!

2.3.2、打 build 包

現在已經打了 debug 的包,那如何打一個 release 包來測試我們的 CodePush 呢?

2.3.2.1、Android Studio 生成證照 jks 檔案

首先我們需要一個簽名,關於這個簽名,我們在開發的時候實際上是使用了 Android Studio 自帶的一個 debug 簽名,在打 release 包的時候,我們就需要自己簽名了:

如下圖,在 build 裡面選擇 Generator Singed Apk:

零客戶端開發經驗 React Native 熱更新 CodePush 打包整合指北

然後點選 Create New...

零客戶端開發經驗 React Native 熱更新 CodePush 打包整合指北

然後填寫即將生成的 patch 路徑,密碼,Alias的名字,密碼,還有一些公司個人相關的東西:

零客戶端開發經驗 React Native 熱更新 CodePush 打包整合指北

OK,看看你填寫的證照路徑裡是否有一個 xx.jks 的證照了。通過這個證照,我們就可以得到 App 的 Sha1 等用於一些第三方 SDK,更多細節可以參考 獲取Android SHA1 、生成jks金鑰、簽名Apk

2.3.2.2、Android Studio 自動打包

理論上在剛才的基礎下點選下一步,選擇 Release 包就能得到 Release 包了,但用:

adb install ./Android/app-release.apk複製程式碼

安裝之後,開啟會直接 crash(需要先解除安裝之前的 APP),解壓 APK,發現 asset 裡面沒有和 CodePush 有關的任何東西,猜測可能是這裡引起 Crash。

所以這種打包方式不能用。

部分解釋參見: www.jianshu.com/p/1cff76e20…

2.3.2.3、手動配置 Gradle 進行自動打包

找到之前生成的證照,複製到 app 目錄下,比如我的證照叫 WeHIT.jks

接著修改 android/gradle.properties,加入證照的相關資訊,方便其他檔案呼叫:

零客戶端開發經驗 React Native 熱更新 CodePush 打包整合指北

MYAPP_RELEASE_STORE_FILE=WeHIT.jks
MYAPP_RELEASE_KEY_ALIAS=WeHITKey
MYAPP_RELEASE_STORE_PASSWORD=123456
MYAPP_RELEASE_KEY_PASSWORD=123456複製程式碼

然後在 app/build.gradle 裡配置打包簽名:

零客戶端開發經驗 React Native 熱更新 CodePush 打包整合指北

這裡會用到之前儲存在 android/gradle.properties 裡的證照資訊。

然後,切換到 andriod 目錄下執行:

./gradlew assembleRelease複製程式碼

在 app/build/outputs/apk 裡面會得到 release 包:

adb install ./WeHIT/WeHIT/android/app/build/outputs/apk/app-release.apk複製程式碼

注1:

ADB 可能不在環境變數中,如果你是用 Android Studio 安裝的 Android SDK,先設定一下。

比如我用的是 zsh,修改 ~/.zsh.rc,加入:

export ANDROID_HOME=${HOME}/Library/Android/sdk
export PATH=${PATH}:${ANDROID_HOME}/tools
export PATH=${PATH}:${ANDROID_HOME}/platform-tools複製程式碼

注2:

理論上 Android Studio 整合了 gradlew 的功能,在 IDE 右側 Gradle 中就有,但打包之後依然缺失 CodePush,所以還是用命令列打包吧.

零客戶端開發經驗 React Native 熱更新 CodePush 打包整合指北

2.3.2.4、額外小記

在執行模擬器的時候,提示:

Starting emulator for AVD 'x86_QVGA_Level10'
emulator: device fd:1044
HAX is working and emulator runs in fast virt mode
emulator: Failed to sync vcpu reg
emulator: Failed to sync HAX vcpu context複製程式碼

發現是 Docker 這種虛擬機器在跑,關掉即可正常開啟模擬器。

3、CodePush 推送新 bundle

改完程式碼後,我們需要更新 bundle,這樣就需要我們打包然後把 bundle 推送到 CodePush 伺服器上。

3.1、打包推送一步流

在專案根目錄執行:

Android 推包

code-push release-react WeHIT-Android android複製程式碼

iOS 推包

code-push release-react WeHIT-ios ios複製程式碼

看到 log:

Detecting android app version:

Using the target binary version value "1.0.0" from "android/app/build.gradle".

Running "react-native bundle" command:

node node_modules/react-native/local-cli/cli.js bundle --assets-dest /var/folders/qv/3gzgsb153qj0gk8ljwl0kg0w0000gn/T/CodePush --bundle-output /var/folders/qv/3gzgsb153qj0gk8ljwl0kg0w0000gn/T/CodePush/index.android.bundle --dev false --entry-file index.android.js --platform android
[05/08/2017, 23:06:39] <START> Initializing Packager
[05/08/2017, 23:06:40] <START> Building Haste Map
[05/08/2017, 23:06:41] <END>   Building Haste Map (1632ms)
[05/08/2017, 23:06:41] <END>   Initializing Packager (2679ms)
[05/08/2017, 23:06:41] <START> Transforming files
[05/08/2017, 23:07:14] <END>   Transforming files (32488ms)
bundle: start
bundle: finish
bundle: Writing bundle output to: /var/folders/qv/3gzgsb153qj0gk8ljwl0kg0w0000gn/T/CodePush/index.android.bundle
bundle: Done writing bundle output
bundle: Copying 15 asset files
bundle: Done copying assets

Releasing update contents to CodePush:

Upload progress:[==================================================] 100% 0.0s
Successfully released an update containing the "/var/folders/qv/3gzgsb153qj0gk8ljwl0kg0w0000gn/T/CodePush" directory to the "Staging" deployment of the "WeHIT" app.複製程式碼

表示已經成功。

3.2、打包推送兩步流

上面的一步流是把 打包和推送 結合到了一起,從 log 中也能簡單看到先是執行打包,然後在 Push 的。

在 APP 的測試包中,我們可能希望能保留開發 Log 等,或者說我們想比較好的自定義 history 等。這樣就需要我們手動的兩次進行打包和推送了。

3.2.1、打包

首先我們建立一個 bundles 資料夾來儲存打包後的 bundles

mkdir bundles複製程式碼

然後進行打包

// react-native bundle --platform 平臺 --entry-file 啟動檔案 --bundle-output 打包js輸出檔案 --assets-dest 資源輸出目錄 --dev 是否除錯

react-native bundle --platform android --entry-file index.android.js --bundle-output ./bundles/index.android.bundle --dev false

react-native bundle --platform ios --entry-file index.ios.js --bundle-output ./bundles/index.ios.bundle --dev false複製程式碼

這樣就在 bundles 資料夾下面生成 bundle

零客戶端開發經驗 React Native 熱更新 CodePush 打包整合指北

3.2.2、推送

bundle 已經打好,現在需要推送到 CodePush 平臺上。

// code-push release <應用名稱> <Bundles所在目錄> <對應的應用版本>
--deploymentName 更新環境 staging時不需要這個
--description 更新描述
--mandatory 是否強制更新 預設否

code-push release WeHIT-Android ./bundles 1.0.0 --description “Android Update”
code-push release WeHIT-iOS ./bundles 1.0.0 --description “iOS Update”複製程式碼

3.3、暗中觀察

code-push deployment history WeHIT-Android Staging複製程式碼

零客戶端開發經驗 React Native 熱更新 CodePush 打包整合指北

如果不想看這麼多的版本,可以用:

code-push deployment ls WeHIT-Android -k複製程式碼

零客戶端開發經驗 React Native 熱更新 CodePush 打包整合指北

同時,你會發現裝了 release 包的客戶端會自動更新。

四、後記

如果之前沒有客戶端的開發經驗,配置 CodePush SDK 還是有點複雜,如果你在配置過程中遇到了什麼問題,或者發現文章中有錯誤,歡迎指出。

與文章的互動請去原文 github.com/rccoder/blo…

五、參考文章

相關文章