【React Native 自界说热更新(iOS端)】

打印 上一主题 下一主题

主题 843|帖子 843|积分 2531

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. **** 主要流程 ****
  2. 1.        打包更新文件(JS Bundle 和资源) → 上传到服务器
  3. 2.        服务器维护一个 JSON 文件 → 包含最新版本信息和下载链接
  4. 3.        App 启动时检查更新 → 下载 ZIP 文件 → 解压并保存 → 替换本地 JS Bundle 文件
  5. 4.        动态加载新的 JS Bundle 文件
复制代码
1.配置入口文件,打包 React Native Bundle 和资源

React Native 默认期望入口文件名为 index.js,并且位于项目根目录
• 检查项目根目录是否存在 index.js 文件。
• 假如入口文件使用了其他名字(如 App.js 或 src/index.js),请在package.json显式指定路径:
  1. import { AppRegistry } from 'react-native'
  2. import { registerRootComponent } from 'expo'
  3. import App from './App'
  4. AppRegistry.registerComponent('App', () => App)
  5. registerRootComponent(App)
复制代码

2. 打包 React Native Bundle 和资源

通过 React Native 提供的 React Native Hermes CLI 直接打包天生 Hermes 字节码格式的 .hbc 文件。
--entry-file是你的入口文件名称
  1. # 终端运行:
  2. # 打包 iOS 平台
  3. mkdir -p ./update/assets
  4. npx react-native bundle \
  5.   --platform ios \
  6.   --dev false \
  7.   --entry-file AppEntry.js \
  8.   --bundle-output ./update/index.ios.bundle \
  9.   --assets-dest ./update/assets \
  10.   --minify true \
  11.   --reset-cache \
  12.   --sourcemap-output ./update/index.ios.bundle.map \
  13.   --transformer node_modules/react-native-hermes-engine/src/transformer.js
  14. cd update
  15. zip -r update.zip index.ios.bundle assets
  16. # 打包 Android 平台
  17. npx react-native bundle \
  18.   --platform android \
  19.   --dev false \
  20.   --entry-file AppEntry.js \
  21.   --bundle-output ./update/index.android.bundle \
  22.   --assets-dest ./update/assets \
  23.   --minify true \
  24.   --reset-cache \
  25.   --sourcemap-output ./update/index.android.bundle.map \
  26.   --transformer node_modules/react-native-hermes-engine/src/transformer.js
  27. cd update
  28. zip -r update.zip index.ios.bundle assets
复制代码
假如觉得以上打包命令麻烦,可以在package.json加入以下代码,直接终端运行build-both
  1. "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/",
  2. "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/",
  3. "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. 1.        在服务器维护一个配置文件(JSON 文件),记录当前最新版本的热更新包信息(版本号、下载链接、MD5 校验值等)。
复制代码
示例:https://yourserver.com/update.json
  1. {
  2.   "version": "1.0.1",
  3.   "url": "https://yourserver.com/updates/update_v1.0.1.zip",
  4.   "md5": "a1b2c3d4e5f67890abcdef1234567890"
  5. }
复制代码
5. 动态加载新的 JS Bundle 文件

依赖的第三方库
  1. 1.        react-native-zip-archive:用于解压下载的更新包
  2. 2.        rn-fetch-blob:用于下载更新包。
  3. 3.        react-native-md5(可选):用于校验 MD5
复制代码
yarn add react-native-zip-archive rn-fetch-blob
yarn add react-native-md5
6.下载Zip文件

  1. •        目标路径:你应该将 index.ios.bundle 和 assets 放置到应用能够访问到的位置,通常是沙盒目录下的某个文件夹。
  2. •        index.ios.bundle 应该放置到 FileSystem.documentDirectory 或其他可访问的目录。
  3. •        assets 文件夹也应放置到同样的路径下,确保资源能够被引用。
复制代码
  1. // 指定下载文件名
  2. const downloadPath = `${fs.dirs.DocumentDir}/update.zip`
  3. // 解压后的根目录
  4. const extractPath = `${fs.dirs.DocumentDir}/update`
复制代码
7.解压文件

  1. // 解压更新包
  2. const unzippedPath = await unzip(downloadPath, extractPath)
  3. // 拼接解压后 Bundle 文件的正确路径
  4. const bundlePath = `${extractPath}/ios/index.${Platform.OS}.bundle`
复制代码
*假如解压失败,查看沙盒目录下是否存在文件:Xcode查看手机沙盒文件
8.加载新的 index.ios.bundle

  1. // 使用 AppRegistry 启动新 Bundle
  2. AppRegistry.runApplication('App', {
  3.    rootTag: 1, // rootTag 通常是 AppRegistry 的容器 ID,1 为默认的根标签
  4.       initialProps: {}, // 你可以传递 props 传递给新应用
  5.     })
  6. // 强制重启应用
  7. RNRestart.Restart()
复制代码
*报错Invariant Violation: "YourAppName" has not been registered. This can happen if
检查 app.json 文件中,AppRegistry.runApplication和 AppRegistry.registerComponent 使用的名称是否一致(我的统一是是'App')
  1. ERROR  加载新版本失败 [Invariant Violation: "YourAppName" has not been registered. This can happen if:
  2. * 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.
  3. * 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 引擎:
  1. {
  2.   "expo": {
  3.     "jsEngine": "hermes"
  4.   }
  5. }
复制代码
在 ios/Podfile 中添加 Hermes 支持
  1. use_react_native!(
  2.   :path => config[:reactNativePath],
  3.   :hermes_enabled => true
  4. )
复制代码
9.源代码(仅供参考)

1、在 iOS 的 AppDelegate.mm 文件中增加支持动态加载自界说热更新的代码。
2、核心代码 放在 utils/UpdateUtil.ts 文件中,是检查更新、下载、解压、动态加载逻辑的核心文件。
3、checkForUpdate函数,根据业务需求来定入口。我是放在 App.tsx 的 useEffect 钩子里,确保应用启动时自动检查更新。
UpdateUtil.ts文件
  1. /*
  2. * @Date: 2024-12-16
  3. * @LastEditors: min
  4. * @LastEditTime: 2024-12-18 18:14:04
  5. * @Description: 更新逻辑,包括检查更新、下载 ZIP 包、解压和动态加载新版本
  6. */
  7. import { Platform, Alert, NativeModules } from 'react-native'
  8. import RNFetchBlob from 'rn-fetch-blob'
  9. import { unzip } from 'react-native-zip-archive'
  10. import { AppRegistry } from 'react-native'
  11. import RNRestart from 'react-native-restart'
  12. import { version as appVersion } from '../../package.json' // 引入package.json的version字段
  13. /**
  14. * 检查服务器是否有新版本
  15. */
  16. export const checkForUpdate = async () => {
  17.   try {
  18.     const updateInfo = {
  19.       hasUpdate: true, // 是否有更新
  20.       version: '1.5.0', // 更新的版本号
  21.       description: '修复了一些已知问题并提升了性能', // 更新描述
  22.       downloadUrl:
  23.         'https://drive.usercontent.google.com/download?id=xxx&export=download&authuser=0', // 更新包下载地址
  24.       checksum: 'abc123xyz', // 更新包的校验值,用于验证完整性
  25.       isMandatory: false, // 是否是强制更新
  26.     }
  27.     if (updateInfo.version > appVersion) {
  28.       Alert.alert('发现新版本', `更新内容:${updateInfo.description}`, [
  29.         { text: '取消', style: 'cancel' },
  30.         { text: '更新', onPress: () => downloadAndUpdate(updateInfo.downloadUrl) },
  31.       ])
  32.     }
  33.   } catch (error) {
  34.     console.error('检查更新失败', error)
  35.   }
  36. }
  37. /**
  38. * 下载新版本 ZIP 包并解压
  39. * @param downloadUrl - 更新包下载地址
  40. */
  41. const downloadAndUpdate = async (downloadUrl) => {
  42.   try {
  43.     const { fs } = RNFetchBlob
  44.     const downloadPath = `${fs.dirs.DocumentDir}/update.zip` // 指定下载文件名
  45.     const extractPath = `${fs.dirs.DocumentDir}/update` // 解压后的根目录
  46.     // 下载更新包
  47.     const res = await RNFetchBlob.config({ path: downloadPath }).fetch('GET', downloadUrl)
  48.     // 解压更新包
  49.     const unzippedPath = await unzip(downloadPath, extractPath)
  50.     // 拼接解压后 Bundle 文件的正确路径
  51.     const bundlePath = `${extractPath}/ios/index.${Platform.OS}.bundle`
  52.     console.log('加载新版本的路径:', bundlePath)
  53.     // 加载新 Bundle 文件
  54.     loadNewBundle(bundlePath)
  55.   } catch (error) {
  56.     console.error('更新失败', error)
  57.   }
  58. }
  59. /**
  60. * 动态加载新的 Bundle 文件
  61. * @param bundlePath - 本地解压后的 Bundle 文件路径
  62. */
  63. const loadNewBundle = (bundlePath: string) => {
  64.   try {
  65.     // 保存新版本的 Bundle 路径到本地存储
  66.     // NativeModules.SettingsManager.settings = {
  67.     //   ...NativeModules.SettingsManager.settings,
  68.     //   hotUpdateBundlePath: bundlePath,
  69.     // }
  70.     // 使用 AppRegistry 启动新 Bundle
  71.     AppRegistry.runApplication('App', {
  72.       rootTag: 1, // rootTag 通常是 AppRegistry 的容器 ID,1 为默认的根标签
  73.       initialProps: {}, // 你可以传递 props 传递给新应用
  74.     })
  75.     // 强制重启应用
  76.     RNRestart.Restart()
  77.     console.log('加载新版本成功', bundlePath)
  78.     // 强制重启应用,加载新的 Bundle
  79.     RNRestart.Restart()
  80.   } catch (error) {
  81.     console.log('加载新版本失败', error, bundlePath)
  82.   }
  83. }
复制代码
AppDelegate.mm文件
  1. #import <GoogleMaps/GoogleMaps.h>
  2. #import "AppDelegate.h"
  3. #import <React/RCTBundleURLProvider.h>
  4. #import <React/RCTLinkingManager.h>
  5. #import <React/RCTBridge.h>
  6. #import <React/RCTBundleURLProvider.h>
  7. #import <React/RCTRootView.h>
  8. #import <Foundation/Foundation.h>
  9. #import <React/RCTBridge.h>
  10. #import <React/RCTReloadCommand.h>
  11. @implementation AppDelegate
  12. - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
  13. {
  14.   [GMSServices provideAPIKey:@"AIzaSyAupzighBWDDbeLrwl5IC4HC4yu0CmlaNw"]; // Google Maps API Key
  15.   self.moduleName = @"main";
  16.   // You can add your custom initial props in the dictionary below.
  17.   // They will be passed down to the ViewController used by React Native.
  18.   self.initialProps = @{};
  19.   // 调用父类方法
  20.   return [super application:application didFinishLaunchingWithOptions:launchOptions];
  21. }
  22. - (NSURL *)sourceURLForBridge:(RCTBridge *)bridge
  23. {
  24. #if DEBUG
  25.   // 开发模式下使用 Metro 服务器的 JS Bundle
  26.   return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index"];
  27. #else
  28.   // 从传入的路径加载热更新的 Bundle 文件
  29.   NSString *hotUpdateBundlePath = [[NSUserDefaults standardUserDefaults] stringForKey:@"hotUpdateBundlePath"];
  30.   if (hotUpdateBundlePath && [[NSFileManager defaultManager] fileExistsAtPath:hotUpdateBundlePath]) {
  31.     return [NSURL fileURLWithPath:hotUpdateBundlePath];
  32.   } else {
  33.     // 回退到默认的 Bundle 文件
  34.     return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];
  35.   }
  36. #endif
  37. }
  38. // 自定义热更新包加载路径
  39. - (NSURL *)bundleURL
  40. {
  41. #if DEBUG
  42.   // Debug 模式下加载 Metro 服务器
  43.   return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index"];
  44. #else
  45.   // Release 模式下加载热更新或默认 JS Bundle
  46.   NSString *hotUpdateBundlePath = [self getHotUpdateBundlePath];
  47.   if (hotUpdateBundlePath != nil && [[NSFileManager defaultManager] fileExistsAtPath:hotUpdateBundlePath]) {
  48.     NSURL *hotUpdateURL = [NSURL fileURLWithPath:hotUpdateBundlePath];
  49.     NSLog(@"加载热更新包: %@", hotUpdateURL);
  50.     return hotUpdateURL;
  51.   } else {
  52.     NSLog(@"加载默认 JS Bundle");
  53.     return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];
  54.   }
  55. #endif
  56. }
  57. // 获取热更新包路径
  58. - (NSString *)getHotUpdateBundlePath
  59. {
  60. // 热更新包存放在沙盒中的 Documents/update 文件夹下
  61. NSString *documentsPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
  62. NSString *updateFolder = [documentsPath stringByAppendingPathComponent:@"update"];
  63. NSString *bundlePath = [updateFolder stringByAppendingPathComponent:@"index.ios.bundle"];
  64.   if ([[NSFileManager defaultManager] fileExistsAtPath:bundlePath]) {
  65.     NSLog(@"热更新包路径存在: %@", bundlePath);
  66.     return bundlePath;
  67.   } else {
  68.     NSLog(@"热更新包路径不存在: %@", bundlePath);
  69.     return nil;
  70.   }
  71. }
  72. // 重新加载 JS Bundle
  73. - (void)reloadJSBundle {
  74.   NSLog(@"开始重新加载 JS Bundle...");
  75.   NSURL *bundleURL = [self sourceURLForBridge:nil];
  76.   if (bundleURL == nil) {
  77.     NSLog(@"无法加载 JS Bundle,路径为空");
  78.     return;
  79.   }
  80.   RCTBridge *bridge = [[RCTBridge alloc] initWithBundleURL:bundleURL
  81.                                            moduleProvider:nil
  82.                                             launchOptions:nil];
  83.   if (bridge) {
  84.     NSLog(@"JS Bundle 重新加载成功");
  85.     [bridge reload];
  86.   } else {
  87.     NSLog(@"创建 RCTBridge 失败");
  88.   }
  89. }
  90. // Linking API
  91. - (BOOL)application:(UIApplication *)application openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options {
  92.   return [super application:application openURL:url options:options] || [RCTLinkingManager application:application openURL:url options:options];
  93. }
  94. // Universal Links
  95. - (BOOL)application:(UIApplication *)application continueUserActivity:(nonnull NSUserActivity *)userActivity restorationHandler:(nonnull void (^)(NSArray<id<UIUserActivityRestoring>> * _Nullable))restorationHandler {
  96.   BOOL result = [RCTLinkingManager application:application continueUserActivity:userActivity restorationHandler:restorationHandler];
  97.   return [super application:application continueUserActivity:userActivity restorationHandler:restorationHandler] || result;
  98. }
  99. // Explicitly define remote notification delegates to ensure compatibility with some third-party libraries
  100. - (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken
  101. {
  102.   return [super application:application didRegisterForRemoteNotificationsWithDeviceToken:deviceToken];
  103. }
  104. // Explicitly define remote notification delegates to ensure compatibility with some third-party libraries
  105. - (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error
  106. {
  107.   return [super application:application didFailToRegisterForRemoteNotificationsWithError:error];
  108. }
  109. // Explicitly define remote notification delegates to ensure compatibility with some third-party libraries
  110. - (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler
  111. {
  112.   return [super application:application didReceiveRemoteNotification:userInfo fetchCompletionHandler:completionHandler];
  113. }
  114. @end
复制代码
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

tsx81428

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表