react-native热更新与冷更新踩坑指南
从使用react-native开发app至今已经有小半年了,这过程中经历了一些版本迭代。其中app的迭代更新过程是需要关注的一点,本文总结我在使用react-native开发更新功能的经验,希望能给以后的开发者一些帮助。
在本文中我将主要以自己的项目经验作为背景,如有与读者所需要的背景不同,可通过相关思路在官方文档中查询相关的平台方案。
项目背景
- 只以android包为例,ios也同样适用。
- 使用
create-expo-app
创建项目 - 通过
expo prebuild
使用bare workflow
- 请注意部分方法在开发模式下失效,只在生成的包中能看到效果
- 由于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>
检查是否有可用的热更新,判断的关键是commitTime
。Updates.fetchUpdateAsync(): Promise<UpdateFetchResult>
将最新bundle下载到本地,iOS/Android都是内置数据库来管理Bundle。isNew
为true代表新Bundle已下载结束。
Updates.reloadAsync()
指示应用程序使用最近下载的版本重新加载。这对于触发新下载的更新以启动非常有用,而无需用户手动重新启动应用。不建议在调用后放置任何有意义的逻辑。此方法不能在开发模式下使用。
前期准备
bare workflow
中android/app/src/main/AndroidManifest.xml是一个配置文件
与热更新相关的配置信息有如下四个字段:
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目录下:
入口文件是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平台,而不是私有服务的更新。
私有服务热更新
- 在app.json中,将updates.url设置为私有服务地址
请注意这里的url就是我们的托管服务的地址,后面我加上了android-index.json也是可以的。这个地址和expo export的地址是一致的,不过expo export会要求我们使用https,这里我使用了http,在对应章节我会进行说明。
- 打包app
eas build --profile preview --platform android --local
使用这个服务进行本地打包,请注意--local会进行本地打包,否则会默认进行远程打包,亲测远程打包会导致配置托管私有服务失效,走托管服务更新。
-
部署热更新服务端 官方提供了一个服务器端需要部署的demo,其实私有服务只有满足静态资源服务托管就可以了,即把export导出的东西放上去即可,不需要额外配置。
-
发布热更新
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相同,系统会去验证版本号,高版本的包会覆盖掉低版本的包从而实现冷更新。%%(覆盖过程会因设备的差异而不同,有的设备甚至不会覆盖,使用冷更新需要做好这部分的调研)%%
以下使用的方法在开发模式中无效,只有在打出来的包中才能看到效果。
- 下载apk
- 将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
-也可能需要配置以下内容
### 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更新的经验,如果文章有错误的地方或者任何问题环境在评论区指出。
转载自:https://juejin.cn/post/7190203133585260599