Skip to main content

React Native 多 Bundle 方案

本文档说明在 brownfield 架构下,如何让不同 RN 页面使用不同的 JS bundle,以及各方案的适用场景与实现路径。

当前架构回顾

项目采用 @callstack/react-native-brownfield 集成模式:

  • rn_app/ — React Native 生产者(打包成 XCFramework / BrownfieldLib)
  • ios_native/ — 原生 iOS 消费者(通过 NavigationStack push RN 页面)

关键代码位置:

文件作用
ios_native/ios_native/ios_nativeApp.swift初始化 ReactNativeBrownfield.shared,加载 JS bundle
ios_native/ios_native/ReactNativeScreenView.swiftReactNativeView(moduleName: "rn_app")
rn_app/index.jsAppRegistry.registerComponent 注册根组件

当前初始化方式:

ReactNativeBrownfield.shared.bundle = ReactNativeBundle
ReactNativeBrownfield.shared.preferEmbeddedBundleInDebug = false // Debug 连 Metro
ReactNativeBrownfield.shared.startReactNative { ... }

先澄清概念:moduleName ≠ bundle

ReactNativeView(moduleName: "rn_app") 中的 moduleNameAppRegistry.registerComponent 注册的组件名,不是 bundle 文件名。

Brownfield 的 ReactNativeBrownfield.shared单例——整个 App 共享一个 JS Runtime + 一个主 bundle。以下配置均为全局设置,在 startReactNative 时生效,无法给每个 ReactNativeView 单独指定 bundle:

  • bundlePath — 内嵌 bundle 路径(默认 main.jsbundle
  • entryFile — Metro 入口(默认 index
  • bundleURLOverride — 自定义 bundle URL 回调

因此需要先明确业务目标,再选方案:

目标推荐方案
不同原生入口 → 不同 RN 页面 UI单 bundle + 多 moduleName(最简单)
按需加载、减小首包体积主 bundle + Split Bundle 懒加载
各业务线独立发版 / OTASplit Bundle + Remote manifest(见 dynamic-multi-bundle.md)或 Re.Pack
完全隔离(不同 RN 版本/依赖)多个独立 RN 工程 + 多 XCFramework(成本最高)

产品 vs 技术(Remote 与方案编号)

原生壳里常见两组 RN 入口,不要和下面的「方案编号」混用:

原生菜单(产品)技术实现是否 OTA
核心 RN(Scheme 1)单 bundle + 多 moduleName
远程业务(Remote entries)Split Bundle + manifest + FeatureHost✅ 仅 Remote 子 bundle

文档中的 方案二 = Split Bundle 技术,用来交付 Remote 远程业务块,不是 Scheme 1 的远程副本。

方案三(Re.Pack)、方案四(多 XCFramework)见下文;Remote OTA 不需要 Re.Pack,除非多团队微前端。

Remote OTA 设计与路线图见 dynamic-multi-bundle.md


方案一:单 Bundle + 多 moduleName(推荐起步)

若需求是「首页 RN / 个人中心 RN / 设置 RN」等不同页面,多数情况下不需要真多 bundle

JS 侧:一个入口,注册多个组件

// rn_app/index.js
import { AppRegistry } from 'react-native';
import HomeScreen from './screens/HomeScreen';
import ProfileScreen from './screens/ProfileScreen';
import SettingsScreen from './screens/SettingsScreen';

AppRegistry.registerComponent('HomeScreen', () => HomeScreen);
AppRegistry.registerComponent('ProfileScreen', () => ProfileScreen);
AppRegistry.registerComponent('SettingsScreen', () => SettingsScreen);

原生侧:不同入口传不同 moduleName

// 首页 RN
ReactNativeView(moduleName: "HomeScreen")

// 个人中心 RN
ReactNativeView(moduleName: "ProfileScreen")

优点

  • 与现有 brownfield 流程完全兼容
  • Debug 仍只需一个 Metro(npm start),热重载正常
  • 打包仍是一个 brownfield:package:ios,改动最小

局限

  • 所有页面代码打进同一个 main.jsbundle
  • 无法按业务线独立发版或 OTA 单页更新

方案二:主 Bundle + Split Bundle(真正的多 Bundle)

RN 官方支持的「一个 Runtime、多个 JS bundle」模式:App 启动加载主 bundle(公共依赖 + 路由壳),进入具体页时再 lazy load 子 bundle。

App 启动
└─ 加载 main.jsbundle(React/RN 核心 + 公共代码)
├─ 进入 Profile → loadBundle(profile.jsbundle) → runApplication('ProfileScreen')
└─ 进入 Settings → loadBundle(settings.jsbundle) → runApplication('SettingsScreen')

目录结构示例

rn_app/
├── index.js # 主 bundle 入口(公共依赖)
├── bundles/
│ ├── profile/index.js # profile.jsbundle 入口
│ └── settings/index.js # settings.jsbundle 入口
└── scripts/
└── build-bundles.sh # 多 bundle 打包脚本

主 bundle 入口(index.js

负责初始化公共依赖,并提供按需加载能力:

import { AppRegistry, NativeModules } from 'react-native';

async function loadFeatureBundle(bundleName) {
// 需自定义 Native Module,封装 loadAndExecuteSplitBundleURL
// 或评估 callstack/react-native-multibundle(较老,需验证 RN 0.86 兼容性)
await NativeModules.MultiBundle.load(bundleName);
}

AppRegistry.registerComponent('RNLoader', () => RNLoaderScreen);

子 bundle 入口(bundles/profile/index.js

import { AppRegistry } from 'react-native';
import ProfileScreen from '../../screens/ProfileScreen';

AppRegistry.registerComponent('ProfileScreen', () => ProfileScreen);

打包脚本

# 主 bundle(含 React/RN 核心)
npx react-native bundle \
--entry-file index.js \
--bundle-output ios/BrownfieldLib/main.jsbundle \
--platform ios --dev false

# 子 bundle(不含 core,需 Metro 配置 moduleId 一致)
npx react-native bundle \
--entry-file bundles/profile/index.js \
--bundle-output ios/BrownfieldLib/profile.jsbundle \
--platform ios --dev false

Metro 关键配置

多 bundle 必须共享 module ID,否则运行时会模块错乱:

// metro.config.js
const { getDefaultConfig, mergeConfig } = require('@react-native/metro-config');

const config = {
serializer: {
createModuleIdFactory: () => {
const fileToIdMap = new Map();
let nextId = 0;
return (path) => {
if (!fileToIdMap.has(path)) {
fileToIdMap.set(path, nextId++);
}
return fileToIdMap.get(path);
};
},
},
};

module.exports = mergeConfig(getDefaultConfig(__dirname), config);

原生侧加载子 bundle

Brownfield 目前没有内置 split bundle API,需要自行扩展。思路:在 push RN 页面前,先加载对应 bundle,再展示 ReactNativeView

func openProfileRN() {
if let url = ReactNativeBundle.url(forResource: "profile", withExtension: "jsbundle") {
// 通过 Bridge 加载 split bundle
bridge?.loadAndExecuteSplitBundleURL(url)
}
// 再展示 ReactNativeView(moduleName: "ProfileScreen")
}

Debug 下 Metro 支持 split bundle:RCTBundleURLProvider.jsBundleURLForSplitBundleRoot("bundles/profile/index")

需要改造的部分

  1. 扩展 Xcode Build Phase,打出 main.jsbundle + 各子 bundle
  2. 子 bundle 作为资源放进 BrownfieldLib framework
  3. 编写薄 Native Module 封装 loadAndExecuteSplitBundleURL
  4. 原生 push 前先 load bundle,再 ReactNativeView(moduleName:)

优点

  • 首包体积小,按需加载
  • 子 bundle 可独立 OTA 更新
  • 仍是一个 RN Runtime,内存可控

缺点

  • Metro 配置、打包脚本、原生加载逻辑需自行维护
  • brownfield CLI 默认只打 main.jsbundle,需扩展 Build Phase
  • callstack/react-native-multibundle 对 RN 0.86 / 新架构支持需自行验证

方案三:Re.Pack + Module Federation(微前端)

注意: 这是 multi-bundle 文档的 方案三(Re.Pack),与原生菜单里的 「Remote 远程业务块」 不是同一概念。Remote OTA 用 方案二 Split Bundle 即可,无需 Re.Pack,除非要多团队独立构建/部署。

若目标是多团队独立开发、独立部署

  • Re.Pack 替代 Metro
  • 各业务线产出 remote bundle,主 App 运行时 import() 远程模块
  • Callstack 在大型项目里常用此模式

与 brownfield 的冲突

  • brownfield CLI 基于 Metro + Xcode Build Phase
  • 接入 Re.Pack 需改打包链路,brownfield:package:ios 可能无法直接复用
  • 更适合 RN 为主、原生为壳,或自建 CI 打包的场景

方案四:多个 XCFramework(完全隔离)

每个 RN 业务单独一个工程,各自执行 brownfield:package:ios

rn_home/ → HomeBrownfieldLib.xcframework
rn_profile/ → ProfileBrownfieldLib.xcframework

问题

ReactNativeBrownfield.shared 是单例,不能同时跑两个 Runtime。可选做法:

  1. 每次进页面前 stopReactNative() → 换 bundle → 再 startReactNative()(切换慢、状态丢失)
  2. 自行维护多个 RCTBridge(brownfield 不支持,工作量大)

除非业务必须不同 RN 版本或完全不同 native 依赖,否则不推荐


推荐实施路径

结合当前项目结构(原生 NavigationStack + 单 BrownfieldLib):

阶段 1 — 现在就能做

单 bundle + 多 moduleName

  • ContentView 增加多个 RN 入口,各传不同 moduleName
  • JS 侧拆成 screens/HomeScreen.tsx
  • 零 brownfield 改造,Debug 热重载不受影响

阶段 2 — Remote 远程业务 + OTA

主 bundle + Split Bundle + manifest(交付 Remote 入口,不是 Scheme 1 的远程副本)

  1. 扩展 Build Phase,打出 Remote 子 bundle
  2. bundle-server manifest + 上传 + OTA
  3. SplitBundleLoader + bundleUpdater 客户端
  4. 原生 Remote 菜单 + FeatureHost

详见 dynamic-multi-bundle.md

阶段 3 — 多团队微前端

评估 Re.Pack,调整 CI,不一定继续依赖 brownfield 默认打包流程。


Debug 开发体验

模式Metro热重载
单 bundle 多 module一个 npm start全部页面可用
Split bundle主 bundle 连 Metro,子 bundle 用 jsBundleURLForSplitBundleRoot子 bundle 需单独配置 entry
多 XCFramework每个工程一个 Metro 端口极难维护

当前 preferEmbeddedBundleInDebug = false 适合单 bundle 开发。若采用 split bundle,Debug 时需为主/子 bundle 分别配置 Metro entry。


小结

要点说明
moduleName ≠ bundlemoduleName 只是换根组件,仍在同一 bundle 内
Brownfield 单例 Runtime真多 bundle 应在同一 Runtime 内 lazy load,而非多个 XCFramework 各起一个 RN
最务实路径先用方案一验证多 RN 页;有体积或独立发版需求再上方案二

参考