likes
comments
collection
share

react-native热更新与冷更新踩坑指南

作者站长头像
站长
· 阅读数 17

从使用react-native开发app至今已经有小半年了,这过程中经历了一些版本迭代。其中app的迭代更新过程是需要关注的一点,本文总结我在使用react-native开发更新功能的经验,希望能给以后的开发者一些帮助。

在本文中我将主要以自己的项目经验作为背景,如有与读者所需要的背景不同,可通过相关思路在官方文档中查询相关的平台方案。

项目背景

  1. 只以android包为例,ios也同样适用。
  2. 使用 create-expo-app 创建项目
  3. 通过 expo prebuild 使用 bare workflow
  4. 请注意部分方法在开发模式下失效,只在生成的包中能看到效果
  5. 由于expo更新版本频繁,请注意本文时效性

热更新

expo的热更新功能原理可以简单这样理解:业务代码的js生成一个bundle.js的包,这个包涵盖了业务功能,react-native框架的目的就是解释运行这个js脚本。所以只要在应用中去获取服务器上更新的bundler文件,在再当前的apk中运行即可。

API

npx expo install expo-updates

import * as Updates from 'expo-updates';

Constants

Updates.isemergencylaunch 当前启动是否为紧急启动。当热更新过程出现问题时,比如:网络波动,新bundle存储失败等,该库会自动找到之前可用的bundle加载。 Updates.manifest 当前运行bundle清单对象。里面包含着一些expo项目app.json的内容,也有该bundle的版本信息。在开发模式下对象为空。 Updates.releaseChannel 当前发布渠道的名称,当使用expo更新时,此字段的值始终为"default"。 Updates.updateId 当前bundle的唯一标识符,通过这个值来判定当前Bundle是否为最新Bundle,在开发模式或者禁用热更新时该值为null.

Methods

Updates.checkForUpdateAsync(): Promise<UpdateCheckResult> 检查是否有可用的热更新,判断的关键是commitTimeUpdates.fetchUpdateAsync(): Promise<UpdateFetchResult> 将最新bundle下载到本地,iOS/Android都是内置数据库来管理Bundle。isNew为true代表新Bundle已下载结束。 Updates.reloadAsync() 指示应用程序使用最近下载的版本重新加载。这对于触发新下载的更新以启动非常有用,而无需用户手动重新启动应用。不建议在调用后放置任何有意义的逻辑。此方法不能在开发模式下使用。

前期准备

bare workflow 中android/app/src/main/AndroidManifest.xml是一个配置文件 react-native热更新与冷更新踩坑指南与热更新相关的配置信息有如下四个字段: EXPO_UPDATES_CHECK_ON_LAUNCH : default: ALWAYS 上述提到的checkForUpdateAsync仅仅会判定是否有可用的热更新,而这个字段一但开启会在启动时自动检查和下载(有的话)新Bundle。但需要重启才会加载新Bundle。 开启这个其实就在壳子里自动完成了热更新,RN层不需要写任何的监听代码。至于监听就看自己的业务需要了。 该字段有共有三个值:ALWAYS``NEVER``WIFI_ONLY

ENABLED : default: true 禁用的话不仅仅是让第一个字段失效,也不能调用上述提到的热更新API,会强制应用加载打包到apk中的Bundle文件。

EXPO_SDK_VERSION 当把bundle托管到expo服务器时,有一定的标识作用。

EXPO_UPDATE_URL 这里填的是我们托管的域名,这里不需要我们手动填写,在打包bundle的时候会自动填充,标准格式:https://xxx/android-index.json。 

如何打包

expo export -p https://xxxx/xxx --output-dir ./dist
  • -p : 打包结束后,会将Native中的EXPO_UPDATE_URL修改成指定内容。比如指定的是joseydon.com/update,则Nat… iOS:joseydon.com/update/ios-… android: joseydon.com/update/andr… 所以在打壳子之前一定先运行一下这个命令,将壳中的热更新地址改为对应环境的。 %%这个url参数需要一个https地址, 所以需要服务有一个证书,但经过实验不需要证书的http私有域也是可以,详细操作在后文相关章节详细阐述。%%

  • –output-dir:打包产物的输出路径

Bundle.js

使用expo export打包产出的产物将在我们指定的dist目录下: react-native热更新与冷更新踩坑指南入口文件是android-index.json,将根据json的配置找到相应的assets资源文件(一般是图片)和bundle.js。我理解android-xxxx.js当中xxxx这串随机数是根据commitTime来生成的,也是根据commitTIme来判断是否为新的bundle包。有时连续打包两个bundle生成的随机数一致,这时间隔长一些或者当中build一次apk可以刷新commitTime获得新的随机数。

最后将dist目录下的所有文件放到指定的热更新服务上。如果使用托管expo平台,那么将不需要手动放置,只需要运行expo export命令即可。

自动更新与手动更新

使用热更新时有两种方式,自动更新和手动更新,两者的差异是我们可否在react代码中控制在什么逻辑下发生热更新操作。

  • 启动下载,再次冷启动会加载最新的Bunlde 搞好Native配置即可。RN代码中不需要调用任何热更新API
  • 需要启动自动加载最新的Bundle 除了搞好Native配置外,需要在业务层使用api,最后调用Updates.reloadAsync()加载新Bundle即可。附上react代码:
try {
  const update = await Updates.checkForUpdateAsync();
  if (!update.isAvailable) {
    return;
  }
  const result =  await Updates.fetchUpdateAsync();
  if (result.isNew) {
    await Updates.reloadAsync();
    return;
  }
} catch(err) {
}

托管expo平台与私有服务热更新

我们需要更新的bundle.js文件该从哪个服务下载?expo提供了免费的托管服务,但有时我们需要将更新的资源文件放在私有服务上。尤其是考虑到可控性、安全问题、是否能访问外网、并发性等等...

经过实验,只要打包app是通过远程服务打包,那么即使进行了相关配置热更新都是走的托管expo平台,而不是私有服务的更新。

私有服务热更新

  1. 在app.json中,将updates.url设置为私有服务地址 react-native热更新与冷更新踩坑指南请注意这里的url就是我们的托管服务的地址,后面我加上了android-index.json也是可以的。这个地址和expo export的地址是一致的,不过expo export会要求我们使用https,这里我使用了http,在对应章节我会进行说明。
  2. 打包app
eas build --profile preview --platform android --local

使用这个服务进行本地打包,请注意--local会进行本地打包,否则会默认进行远程打包,亲测远程打包会导致配置托管私有服务失效,走托管服务更新。

  1. 部署热更新服务端 官方提供了一个服务器端需要部署的demo,其实私有服务只有满足静态资源服务托管就可以了,即把export导出的东西放上去即可,不需要额外配置。

  2. 发布热更新

expo export -p https://xxxx/xxx --output-dir ./dist

如何在私有服务为http的情况下打包

由于我的项目的服务器是http,没有认证的证书,不希望每台终端都手动去安装了自定义签名,所以研究了本种方案,不确定之后会不会被官方当作bug处理,但目前此种方案认证过是可行的。

会去校验https的地方只有expo export这条命令行,所以即使你的服务是http,也可以先写https,打包后找到对应dist文件,将android.json中涉及https的url都改成url即可。后续过程中不会再有校验https的地方。

冷更新

所谓冷更新,就是在当前app中下载这个app的更新版本,由于下载的app相同,系统会去验证版本号,高版本的包会覆盖掉低版本的包从而实现冷更新。%%(覆盖过程会因设备的差异而不同,有的设备甚至不会覆盖,使用冷更新需要做好这部分的调研)%%

以下使用的方法在开发模式中无效,只有在打出来的包中才能看到效果。

  1. 下载apk
  2. 将apk安装
import * as FileSystem from 'expo-file-system';
import * as IntentLauncher from 'expo-intent-launcher';

FileSystem.downloadAsync(url,FileSystem.documentDirectory +'hrp'.apk').then(({ uri }) => {

 IntentLauncher.startActivityAsync('android.intent.action.VIEW', {

 data: 'file:///storage/emulated/0/Download/DCIM/' + 'hrp.apk',

 // type: 'application/vnd.android.package-archive',

 flags: 1

 });


})

.catch(error => {

console.error(error);

})

还有一种方式就是将文件下载到指定目录

import * as FileSystem from 'expo-file-system';
import * as MediaLibrary from 'expo-media-library';

FileSystem.downloadAsync(url,FileSystem.documentDirectory +'hrp'.apk').then(({ uri }) => {

 const permission = await MediaLibrary.getPermissionsAsync();

MediaLibrary.createAssetAsync(uri).then(asset => {

console.log('asset', asset);

MediaLibrary.createAlbumAsync('Download', asset,false)

.then(() => {

});


})

.catch(error => {

// showMessage({

// message: t('general.success'),

// description: t('download.failed'),

// type: 'danger'

// });

});

})

}
}

.catch(error => {

console.error(error);

})

EAS踩坑指南

由于我之前都是使用expo build进行打包的,后因为此打包方式将废弃迁移使用eas,所以这里主要总结迁移eas过程中的坑点。

  • eas打包最后一步使用Gradle容易报错 Gradle build failed with unknow error -通常是Gradle环境有问题,有可能是因为有些包国内被屏下载不了,如果是 distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-all.zip可以下载下来替换到对应的文件目录下:根目录下 打开.gradle react-native热更新与冷更新踩坑指南 -也可能需要配置以下内容


### gradle-wrapper.properties

distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-all.zip

替换为
-`./gradlew wrapper --gradle-version 7.5.1 --distribution-type=all`

### build.gradle
//        if (findProperty('android.kotlinVersion')) {
//            kotlinVersion = findProperty('android.kotlinVersion')
//        }
        kotlinVersion = "1.6.20"

替换为
dependencies {
    classpath("com.android.tools.build:gradle:7.1.1")
    classpath("com.facebook.react:react-native-gradle-plugin")
    classpath("de.undercouch:gradle-download-task:5.0.1")
    classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.20")
    // NOTE: Do not place your application dependencies here; they belong
    // in the individual module build.gradle files
}

-打包过程中可能需要fbjs但本地没有安装,可以npm i fbjs一下

  • expo和eas的差别

    对比两者,后者打出的apk更小(60+mb vs 30+mb),并且支持本地打包,expo build打出来的包是默认走托管服务更新的。并且react层的业务代码有些表现形式也不同,可以看做eas修复了一些expo打包的bug。所以如果你的项目以前是使用expo打包后面迁移eas,一定要检查业务逻辑。

    expo会默认增加,而eas没添加就没有

    使用栈导航的时候,如果设置导航栏透明expo会使它失去高度,从而不存在;而eas中就不会失去高度,如果当中嵌套导航的话,里层导航就无法点击,需要设置导航shown为false。

  • eas打包只支持mac环境和linux环境,不支持windows。

以上就是这段时间开发react-native更新的经验,如果文章有错误的地方或者任何问题环境在评论区指出。