天天看点

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数据来解决。后者暂没有时间测试,后续有时间在确定一下。

继续阅读