天天看點

react-native 熱更新expo-update使用心得前言API準備工作使用彩蛋深坑預警

目錄

  • 前言
  • API
    • 常數
    • 方法
    • 監聽
  • 準備工作
    • Native中的全局配置
    • 如何打包
    • Bundle内容
  • 使用
    • 自動更新
    • 手動更新
  • 彩蛋
  • 深坑預警

前言

沒想到expo能把熱更新做的如此簡單易用。曾經想看源碼學習研究一下,結果铩羽而歸。後續就想着把碰到的一些坑及官方文檔上遺漏的一些點給補充上去

API

常數

  1. Updates.isEmergencyLaunch

    目前啟動是否為緊急啟動。當熱更新過程出現問題時,比如:網絡波動,新bundle存儲失敗等,該庫會自動找到之前可用的bundle加載。
  2. Updates.manifest

    目前運作bundle清單對象。裡面包含着一些expo項目app.json的内容,也有該bundle的版本資訊。expo-updates裡有給清單定義類型,有興趣的可以檢視
  3. Updates.releaseChannel

    目前釋出管道的名稱,目前還沒有用過,不清楚具體是如何設定的
  4. Updates.updateId

    目前bundle的唯一辨別符,我通過這個值來判定目前Bundle是否為最新Bundle,在開發模式或者禁用熱更新時該值為null

方法

  1. Updates.checkForUpdateAsync(): Promise<UpdateCheckResult>

    檢查是否有可用的熱更新,判斷的關鍵是

    commitTime

    ,源碼如下:
    // UpdateCheckResult類型
    type UpdateCheckResult = {
        isAvailable: false;
    } | {
        isAvailable: true;
        manifest: Manifest; // 跟上面提到的Updates.manifest一緻
    };
               
  2. Updates.fetchUpdateAsync(): Promise<UpdateFetchResult>

  3. 将最新bundle下載下傳到本地,iOS/Android都是内置資料庫來管理Bundle。

    isNew

    為true代表新Bundle已下載下傳結束
  4. 在Native端Bundle的下載下傳沒有主動設定逾時時間(iOS預設是60s)。若網絡狀态不好,會等待好長時間,是以建議通過

    Promise.race

    給該方法增加一個逾時時間,我項目中設定的是5s
    // UpdateFetchResult類型
    type UpdateFetchResult = {
        isNew: false;
    } | {
        isNew: true;
        manifest: Manifest;
    };
               
  5. Updates.reloadAsync()

    Native查找本地資料庫中

    commitTime

    最大的Bundle,并加載

    切記:不要在該方法後加任何的邏輯,是以該方法開始執行就代表着本bundle的終結。

監聽

  1. Updates.addListener(eventListener)

    1. 監聽熱更新的結果,沒有進度隻有結果。要取消監聽調用傳回值的remove方法即可
    2. 其實該方法比較雞肋,在android端隻有開啟

      EXUpdatesCheckOnLaunch

      并且在Bundle初始時注冊監聽才可用。是以建議該方法用于後續提到的自動更新,不要使用者手動更新,手動更新判定Bundle的下載下傳狀态,強烈推薦

      fetchUpdateAsync

    // eventListener參數type
    type UpdateEvent = {
        type: UpdateEventType.NO_UPDATE_AVAILABLE;
    } | {
        type: UpdateEventType.UPDATE_AVAILABLE;
        manifest: Manifest;
    } | {
        type: UpdateEventType.ERROR;
        message: string;
    };
               

準備工作

expo-updates的API也就這些,十分簡易明。接下我就跟大家聊聊一些配置問題

備注:如下配置是基于expo建立的bare項目,與其他方式建立下的RN項目在使用方式上可能會有些出入。

Native中的全局配置

若正常安裝了expo-updates庫,在iOS項目下會有如下一個配置檔案

react-native 熱更新expo-update使用心得前言API準備工作使用彩蛋深坑預警

在android的AndroidManifest.xml裡也有同樣的配置

react-native 熱更新expo-update使用心得前言API準備工作使用彩蛋深坑預警

不管是iOS還是Android關于熱更新的配置檔案都有如下四個字段:

  1. EXUpdatesCheckOnLaunch

    /

    EXPO_UPDATES_CHECK_ON_LAUNCH

    : default: ALWAYS
    • 上訴提到的checkForUpdateAsync僅僅會判定是否有可用的熱更新,而這個字段一但開啟會在啟動時自動檢查和下載下傳(有的話)新Bundle。但需要重新開機才會加載新Bundle。
    • 開啟這個其實就在殼子裡自動完成了熱更新,RN層不需要寫任何的監聽代碼。至于監聽就看自己的業務需要了。
    • 該字段有共有三個值:ALWAYS/NEVER/WIFI_ONLY
  2. EXUpdatesEnabled

    /

    ENABLED

    :default: true

    禁用的話不僅僅是讓第一個字段失效,也不能調用上訴提到的熱更新API,會強制應用加載打包到ipa/apk中的Bundle檔案

  3. EXUpdatesLaunchWaitMs

    /

    EXPO_UPDATES_LAUNCH_WAIT_MS

    : default: 0,機關是毫秒

    等待Native熱更新的時間,配合SplashScreen使等待時間螢幕上一直顯示啟動頁。下載下傳完之後并不會直接加載新Bundle,而是加載之前的可用版本

  4. EXUpdatesSDKVersion

    /

    EXPO_SDK_VERSION

    當把bundle托管到expo伺服器時,有一定的辨別作用,我這裡是将bundle托管到自己伺服器,就沒有關心他,按預設的就行
  5. EXUpdatesURL

    /

    EXPO_UPDATE_URL

    重點來了,這裡填的是我們托管的域名,這裡不需要我們手動填寫,在打包bundle的時候會自動填充,标準格式:

    https://xxx/ios-index.json

    。 ios-index.json是啥呢,等下面看到我們打包的産物便可得知。

如何打包

若使用expo-updates便不可以直接使用

react-native bundle

的打包方式,需按照如下方式:

# android/iOS兩個平台會同時打出
expo export -p https://xxxx/xxx --output-dir ./dist --config app.json --force
           

expo expor

更多的指令,有興趣的小夥伴檢視expo-腳本翻譯。接下來分析一下這裡用到的每個參數:

  • -p: 打包結束以後,會自動将Native中的

    EXUpdatesURL

    /

    EXPO_UPDATE_URL

    修改成指定内容。

    舉個🌰:-p: 指定的是https://juzi.com/juzi,則Native中的熱更新位址分别為:

    iOS: https://juzi.com/juzi/ios-index.json

    android: https://juzi.com/juzi/android-index.json

    是以在打殼子之前一定先運作一下這個指令,将殼子中的熱更新位址改為對應環境的
  • –output-dir:打包産物的輸出路徑
  • –config:指定expo項目的配置檔案,在expo的bare項目中,這個檔案好像沒啥用
  • –force:強制覆寫之前output-dir的檔案

在上訴指令裡面需要我們注意的是,需要我們自己想辦法把

output-dir

中的檔案給轉移到

-p

下指定路徑

Bundle内容

-p

裡面我們提到了ios-index.json及android-index.json,接下來我們主要介紹下

expo export

的産物。首先看看目錄結構

.

├── android-index.json

├── ios-index.json

├── assets

│ └── 1eccbc4c41d49fd81840aef3eaabe862

└── bundles

├── android-01ee6e3ab3e8c16a4d926c91808d5320.js

└── ios-ee8206cc754d3f7aa9123b7f909d94ea.js

要特别說明的是index.json,這其實是

package.json

/

app.json

/

manifest

三個檔案的集合,給大家粘一個自己體會一下

{
  "name": "xxxx",
  "slug": "xxxx",
  "version": "1.0.0",
  "orientation": "portrait",
  "icon": "./src/assets/images/icon.png",
  "scheme": "myapp",
  "userInterfaceStyle": "automatic",
  "updates": {
    "fallbackToCacheTimeout": 0
  },
  "ios": {
    "supportsTablet": true,
    "bundleIdentifier": "com.kaikeba.lumiere.xiaochao"
  },
  "web": {
    "favicon": "./assets/images/favicon.png"
  },
  "android": {
    "package": "com.kaikeba.lumiere.xiaochao"
  },
  "entryPoint": "node_modules/expo/AppEntry.js",
  "sdkVersion": "38.0.0",
  "platforms": [
    "ios",
    "android",
    "web"
  ],
  "locales": {},
  "iconUrl": "https://www.xxxx.com/assets/2f893d5432f4c59aff316878db740f41",
  "bundledAssets": [
    "asset_7d40544b395c5949f4646f5e150fe020.png",
    "asset_647543ebfccf6e5495434383598453d1.json",
    "asset_5cdf883b18a5651a29a4d1ef276d2457.ttf",
  ],
  "assetUrlOverride": "./assets",
  "publishedTime": "2020-10-20T07:10:42.731Z",
  "commitTime": "2020-10-20T07:10:42.732Z",
  "releaseId": "308a52d0-88c1-43fd-bd8e-5bea37c4d845",
  "revisionId": "ZJQjABE6Pz",
  "id": "@xxxx/xxxx",
  "bundleUrl": "https://www.xxxx.com/bundles/ios-107355c4860396020a50cc99c1e16578.js",
  "platform": "ios",
  "dependencies": [
    "@ant-design/icons-react-native",
    "@ant-design/react-native",
    "@expo/vector-icons",
    "@react-native-community/async-storage",
  ]
}
           

使用

自動更新

分兩種:

  1. 啟動下載下傳,但不Rload最新的Bundle,再次冷啟動會加載最新的Bunlde

    搞好Native配置即可。RN代碼中不需要調用任何熱更新API

  2. 需要啟動自動加載最新的Bundle

    除了搞好Native配置外,要在Bundle初始化時調用

    Updates.addListener

    來監聽Bundle下載下傳狀态,下載下傳完事調用

    Updates.reloadAsync()

    加載新Bundle即可

手動更新

手動更新要将

EXUpdatesCheckOnLaunch

設為

NEVER

,其他按需配置即可,附上RN層的邏輯代碼

try {
  const update = await Updates.checkForUpdateAsync();
  if (!update.isAvailable) {
    this.jumpLogic();
    return;
  }
  const result =  await Updates.fetchUpdateAsync();
  if (result.isNew) {
    await Updates.reloadAsync();
    return;
  }
  this.jumpLogic();
} catch(err) {
  this.jumpLogic();
}
           

彩蛋

在項目開發中,可以在app中内置切換環境的後門,是以想根據開發環境也動态切換熱更新的環境,比如:

  1. 通過後門切換到QA環境,熱更新連結也對應切換成測試的
  2. 通過後門切換到生産環境,熱更新連結也對應切換生産的

但由于該庫熱更新的url是寫在apk/ipa中的,也沒有提供對應的API來修改。是以隻能通過改動源碼來操作了,我的思路如下:

  1. 橋接一個類,當環境切換時将對應的熱更新連結傳給Native,Native存儲到本地

    NSUserDefaults

    SharedPreferences

  2. 改動expo-updates源碼,hotUpdateUrl從本地擷取,當沒有擷取到時,使用在清單檔案中配置的(建議配置成生産環境的),下面附上我改動iOS/Android源碼的dif
diff --git a/node_modules/expo-updates/android/src/main/java/expo/modules/updates/UpdatesConfiguration.java b/node_modules/expo-updates/android/src/main/java/expo/modules/updates/UpdatesConfiguration.java
index 90b5323..48ddb65 100644
--- a/node_modules/expo-updates/android/src/main/java/expo/modules/updates/UpdatesConfiguration.java
+++ b/node_modules/expo-updates/android/src/main/java/expo/modules/updates/UpdatesConfiguration.java
@@ -1,6 +1,7 @@
 package expo.modules.updates;
 
 import android.content.Context;
+import android.content.SharedPreferences;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
 import android.net.Uri;
@@ -71,8 +72,15 @@ public class UpdatesConfiguration {
     try {
       ApplicationInfo ai = context.getPackageManager().getApplicationInfo(context.getPackageName(), PackageManager.GET_META_DATA);
 
-      String urlString = ai.metaData.getString("expo.modules.updates.EXPO_UPDATE_URL");
-      mUpdateUrl = urlString == null ? null : Uri.parse(urlString);
+
+      // ********* 這是原先邏輯 String urlString = ai.metaData.getString("expo.modules.updates.EXPO_UPDATE_URL");
+
+      // ********* 為了動态修改url連結,特改成如下,清單檔案裡的updateUrl當做預設的使用
+      String defaultUrlString = ai.metaData.getString("expo.modules.updates.EXPO_UPDATE_URL");
+      SharedPreferences hotUpdateInfo = context.getSharedPreferences("hotUpdateInfo", Context.MODE_PRIVATE);
+      String urlString = hotUpdateInfo.getString("url", defaultUrlString);
+      mUpdateUrl = Uri.parse(urlString);
+      Log.i("*****hotUpdateUrl*****", "" + urlString);
 
       mIsEnabled = ai.metaData.getBoolean("expo.modules.updates.ENABLED", true);
       mSdkVersion = ai.metaData.getString("expo.modules.updates.EXPO_SDK_VERSION");
diff --git a/node_modules/expo-updates/ios/EXUpdates/EXUpdatesConfig.m b/node_modules/expo-updates/ios/EXUpdates/EXUpdatesConfig.m
index e285f1d..63b4ac4 100644
--- a/node_modules/expo-updates/ios/EXUpdates/EXUpdatesConfig.m
+++ b/node_modules/expo-updates/ios/EXUpdates/EXUpdatesConfig.m
@@ -77,6 +77,11 @@ - (void)loadConfigFromDictionary:(NSDictionary *)config
 
   id updateUrl = config[kEXUpdatesConfigUpdateUrlKey];
   if (updateUrl && [updateUrl isKindOfClass:[NSString class]]) {
+      NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
+      NSString *switchUpdateUrl =  [defaults valueForKey:@"hotUpdateUrl"];
+      if (switchUpdateUrl) {
+          updateUrl = switchUpdateUrl;
+      }
     NSURL *url = [NSURL URLWithString:(NSString *)updateUrl];
     NSAssert(url, @"EXUpdatesURL must be a valid URL");
     _updateUrl = url;

           

深坑預警

  1. 改完源碼打完殼子以後,此時殼子内置的Bundle的

    commitTime

    是最新的,是以必須要在殼子打完以後,再打新Bundle,才會使熱更新生效。總之,每次熱更新都會拿目前Bundle的commitTime和對應熱更新連結的commitTime進行比較,當熱更新失敗時要優先排查這兩個commitTime的先後順序是否有問題。
  2. 最開始我想使用releaseId是否相等來判定是否有新包,這樣就不需要考慮commitTime的先後問題,便可以解決切換熱更新連結時的Bundle新舊問題。但因為每次

    reloadAsync

    時并不是從資料庫裡找最後放置的,而是做周遊找commitTime最大的,因為擔心後續對源碼改動越來越多,是以就放棄了。
  3. 上面提到的切換熱更新連結時的Bundle新舊問題,可以通過删除重裝來解決,也可以嘗試清除App資料來解決。後者暫沒有時間測試,後續有時間在确定一下。

繼續閱讀