React Native生产环境中的热更新涉及动态分发新代码到用户设备,而无需用户通过 App Store 或 Google Play 更新。
一、更新方式选型
1. Microsoft CodePush
• 原理:CodePush 是一个基于云的服务,可以将 JavaScript 和资源包更新直接推送到用户设备。
长处:
• 避免频仍的应用商店更新。
• 更新小型功能和 Bug 修复服从高。
缺点:
• 仅支持 JavaScript 和资源更新,不实用于原生代码(如修改原生模块需要重新发布应用)
• 微软已经正式宣布 Visual Studio App Center 将于 2025 年 3 月 31 日 停止服务
2. Expo OTA Updates
• 原理:Expo 提供了开箱即用的 OTA(Over-the-Air)更新服务,无需额外的配置。
长处:
• 无需使用原生代码即可快速发布更新。
• 与 Expo 环境深度集成。
缺点:
• 需要使用 Expo 构建工具。
3. 手动资源摆设
• 将打包的资源分发到服务器,供客户端通过 HTTP 拉取。
• 避免频仍发布 App 的问题
二、步骤(手动资源摆设)
- **** 主要流程 ****
- 1. 打包更新文件(JS Bundle 和资源) → 上传到服务器
- 2. 服务器维护一个 JSON 文件 → 包含最新版本信息和下载链接
- 3. App 启动时检查更新 → 下载 ZIP 文件 → 解压并保存 → 替换本地 JS Bundle 文件
- 4. 动态加载新的 JS Bundle 文件
复制代码 1.配置入口文件,打包 React Native Bundle 和资源
React Native 默认期望入口文件名为 index.js,并且位于项目根目录
• 检查项目根目录是否存在 index.js 文件。
• 假如入口文件使用了其他名字(如 App.js 或 src/index.js),请在package.json显式指定路径:
- import { AppRegistry } from 'react-native'
- import { registerRootComponent } from 'expo'
- import App from './App'
- AppRegistry.registerComponent('App', () => App)
- registerRootComponent(App)
复制代码
2. 打包 React Native Bundle 和资源
通过 React Native 提供的 React Native Hermes CLI 直接打包天生 Hermes 字节码格式的 .hbc 文件。
--entry-file是你的入口文件名称
- # 终端运行:
- # 打包 iOS 平台
- mkdir -p ./update/assets
- npx react-native bundle \
- --platform ios \
- --dev false \
- --entry-file AppEntry.js \
- --bundle-output ./update/index.ios.bundle \
- --assets-dest ./update/assets \
- --minify true \
- --reset-cache \
- --sourcemap-output ./update/index.ios.bundle.map \
- --transformer node_modules/react-native-hermes-engine/src/transformer.js
- cd update
- zip -r update.zip index.ios.bundle assets
- # 打包 Android 平台
- npx react-native bundle \
- --platform android \
- --dev false \
- --entry-file AppEntry.js \
- --bundle-output ./update/index.android.bundle \
- --assets-dest ./update/assets \
- --minify true \
- --reset-cache \
- --sourcemap-output ./update/index.android.bundle.map \
- --transformer node_modules/react-native-hermes-engine/src/transformer.js
- cd update
- zip -r update.zip index.ios.bundle assets
复制代码 假如觉得以上打包命令麻烦,可以在package.json加入以下代码,直接终端运行build-both
- "build-ios": "rm -rf ~/Desktop/output/ios_update && mkdir -p ~/Desktop/output/ios_update/assets && npx react-native bundle --platform ios --dev false --entry-file AppEntry.js --bundle-output ~/Desktop/output/ios_update/index.ios.bundle --assets-dest ~/Desktop/output/ios_update/assets --minify true --reset-cache --sourcemap-output ~/Desktop/output/ios_update/index.ios.bundle.map --transformer node_modules/react-native-hermes-engine/src/transformer.js && cd ~/Desktop/output/ios_update && zip -r ios_update.zip index.ios.bundle assets && mv ios_update.zip ~/Desktop/output/",
- "build-android": "rm -rf ~/Desktop/output/android_update && mkdir -p ~/Desktop/output/android_update/assets && npx react-native bundle --platform android --dev false --entry-file AppEntry.js --bundle-output ~/Desktop/output/android_update/index.android.bundle --assets-dest ~/Desktop/output/android_update/assets --minify true --reset-cache --sourcemap-output ~/Desktop/output/android_update/index.android.bundle.map --transformer node_modules/react-native-hermes-engine/src/transformer.js && cd ~/Desktop/output/android_update && zip -r android_update.zip index.android.bundle assets && mv android_update.zip ~/Desktop/output/",
- "build-both": "yarn build-ios && yarn build-android"
复制代码 3.将打包文件上传到服务器
打包完成后,你会得到:
• index.ios.bundle(iOS 的 JS Bundle 文件)
• index.android.bundle(Android 的 JS Bundle 文件)
• assets 目录(图片、字体等资源)
将这些文件打包成一个 ZIP 文件,比方 update_v1.0.1.zip,然后上传到你的服务器
4. App 端拉取最新的 Bundle 文件
步骤:
- 1. 在服务器维护一个配置文件(JSON 文件),记录当前最新版本的热更新包信息(版本号、下载链接、MD5 校验值等)。
复制代码 示例:https://yourserver.com/update.json
- {
- "version": "1.0.1",
- "url": "https://yourserver.com/updates/update_v1.0.1.zip",
- "md5": "a1b2c3d4e5f67890abcdef1234567890"
- }
复制代码 5. 动态加载新的 JS Bundle 文件
依赖的第三方库
- 1. react-native-zip-archive:用于解压下载的更新包
- 2. rn-fetch-blob:用于下载更新包。
- 3. react-native-md5(可选):用于校验 MD5
复制代码 yarn add react-native-zip-archive rn-fetch-blob
yarn add react-native-md5
6.下载Zip文件
- • 目标路径:你应该将 index.ios.bundle 和 assets 放置到应用能够访问到的位置,通常是沙盒目录下的某个文件夹。
- • index.ios.bundle 应该放置到 FileSystem.documentDirectory 或其他可访问的目录。
- • assets 文件夹也应放置到同样的路径下,确保资源能够被引用。
复制代码- // 指定下载文件名
- const downloadPath = `${fs.dirs.DocumentDir}/update.zip`
- // 解压后的根目录
- const extractPath = `${fs.dirs.DocumentDir}/update`
复制代码 7.解压文件
- // 解压更新包
- const unzippedPath = await unzip(downloadPath, extractPath)
- // 拼接解压后 Bundle 文件的正确路径
- const bundlePath = `${extractPath}/ios/index.${Platform.OS}.bundle`
复制代码 *假如解压失败,查看沙盒目录下是否存在文件:Xcode查看手机沙盒文件
8.加载新的 index.ios.bundle
- // 使用 AppRegistry 启动新 Bundle
- AppRegistry.runApplication('App', {
- rootTag: 1, // rootTag 通常是 AppRegistry 的容器 ID,1 为默认的根标签
- initialProps: {}, // 你可以传递 props 传递给新应用
- })
- // 强制重启应用
- RNRestart.Restart()
复制代码 *报错Invariant Violation: "YourAppName" has not been registered. This can happen if
检查 app.json 文件中,AppRegistry.runApplication和 AppRegistry.registerComponent 使用的名称是否一致(我的统一是是'App')
- ERROR 加载新版本失败 [Invariant Violation: "YourAppName" has not been registered. This can happen if:
- * Metro (the local dev server) is run from the wrong folder. Check if Metro is running, stop it and restart it in the current project.
- * A module failed to load due to an error and AppRegistry.registerComponent wasn't called.]
复制代码 *假如加载新版本的 index.ios.bundle 路径已经正确传入,热更新包也乐成存在,但现在运行组件崩溃或者非常,检查热更新包是否使用Hermes,请确保正确转换字节码。
需要确保热更新的 JS Bundle 和引擎保持一致,否则会导致 加载失败 或 桥接失效 问题
1、 确认 expo.jsEngine 的值
在你的 app.json 或 app.config.js 中,检查是否配置了 Hermes 引擎:
- {
- "expo": {
- "jsEngine": "hermes"
- }
- }
复制代码 在 ios/Podfile 中添加 Hermes 支持
- use_react_native!(
- :path => config[:reactNativePath],
- :hermes_enabled => true
- )
复制代码 9.源代码(仅供参考)
1、在 iOS 的 AppDelegate.mm 文件中增加支持动态加载自界说热更新的代码。
2、核心代码 放在 utils/UpdateUtil.ts 文件中,是检查更新、下载、解压、动态加载逻辑的核心文件。
3、checkForUpdate函数,根据业务需求来定入口。我是放在 App.tsx 的 useEffect 钩子里,确保应用启动时自动检查更新。
UpdateUtil.ts文件
- /*
- * @Date: 2024-12-16
- * @LastEditors: min
- * @LastEditTime: 2024-12-18 18:14:04
- * @Description: 更新逻辑,包括检查更新、下载 ZIP 包、解压和动态加载新版本
- */
- import { Platform, Alert, NativeModules } from 'react-native'
- import RNFetchBlob from 'rn-fetch-blob'
- import { unzip } from 'react-native-zip-archive'
- import { AppRegistry } from 'react-native'
- import RNRestart from 'react-native-restart'
- import { version as appVersion } from '../../package.json' // 引入package.json的version字段
- /**
- * 检查服务器是否有新版本
- */
- export const checkForUpdate = async () => {
- try {
- const updateInfo = {
- hasUpdate: true, // 是否有更新
- version: '1.5.0', // 更新的版本号
- description: '修复了一些已知问题并提升了性能', // 更新描述
- downloadUrl:
- 'https://drive.usercontent.google.com/download?id=xxx&export=download&authuser=0', // 更新包下载地址
- checksum: 'abc123xyz', // 更新包的校验值,用于验证完整性
- isMandatory: false, // 是否是强制更新
- }
- if (updateInfo.version > appVersion) {
- Alert.alert('发现新版本', `更新内容:${updateInfo.description}`, [
- { text: '取消', style: 'cancel' },
- { text: '更新', onPress: () => downloadAndUpdate(updateInfo.downloadUrl) },
- ])
- }
- } catch (error) {
- console.error('检查更新失败', error)
- }
- }
- /**
- * 下载新版本 ZIP 包并解压
- * @param downloadUrl - 更新包下载地址
- */
- const downloadAndUpdate = async (downloadUrl) => {
- try {
- const { fs } = RNFetchBlob
- const downloadPath = `${fs.dirs.DocumentDir}/update.zip` // 指定下载文件名
- const extractPath = `${fs.dirs.DocumentDir}/update` // 解压后的根目录
- // 下载更新包
- const res = await RNFetchBlob.config({ path: downloadPath }).fetch('GET', downloadUrl)
- // 解压更新包
- const unzippedPath = await unzip(downloadPath, extractPath)
- // 拼接解压后 Bundle 文件的正确路径
- const bundlePath = `${extractPath}/ios/index.${Platform.OS}.bundle`
- console.log('加载新版本的路径:', bundlePath)
- // 加载新 Bundle 文件
- loadNewBundle(bundlePath)
- } catch (error) {
- console.error('更新失败', error)
- }
- }
- /**
- * 动态加载新的 Bundle 文件
- * @param bundlePath - 本地解压后的 Bundle 文件路径
- */
- const loadNewBundle = (bundlePath: string) => {
- try {
- // 保存新版本的 Bundle 路径到本地存储
- // NativeModules.SettingsManager.settings = {
- // ...NativeModules.SettingsManager.settings,
- // hotUpdateBundlePath: bundlePath,
- // }
- // 使用 AppRegistry 启动新 Bundle
- AppRegistry.runApplication('App', {
- rootTag: 1, // rootTag 通常是 AppRegistry 的容器 ID,1 为默认的根标签
- initialProps: {}, // 你可以传递 props 传递给新应用
- })
- // 强制重启应用
- RNRestart.Restart()
- console.log('加载新版本成功', bundlePath)
- // 强制重启应用,加载新的 Bundle
- RNRestart.Restart()
- } catch (error) {
- console.log('加载新版本失败', error, bundlePath)
- }
- }
复制代码 AppDelegate.mm文件
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |