@bm-fe/react-native-multi-bundle 1.0.0-beta.3 → 1.0.0-beta.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/INTEGRATION.md CHANGED
@@ -120,25 +120,90 @@ class MainApplication : Application(), ReactApplication {
120
120
 
121
121
  </details>
122
122
 
123
- ### iOS
123
+ ### iOS(自动链接,无需手动配置)
124
124
 
125
- #### 1. 复制 Native 模块文件
125
+ #### 自动链接工作原理
126
+
127
+ 安装包后,React Native CLI 和 CocoaPods 会自动:
128
+ 1. 读取包中的 `react-native-multi-bundle.podspec`
129
+ 2. 将 `ModuleLoader` 原生模块添加到项目中
130
+ 3. 在构建时自动集成
131
+
132
+ #### 安装依赖
133
+
134
+ 安装 npm 包后,运行 pod install:
135
+
136
+ ```bash
137
+ cd ios && pod install
138
+ ```
139
+
140
+ #### 验证自动链接
141
+
142
+ 安装包后,可以通过以下命令验证自动链接是否生效:
143
+
144
+ ```bash
145
+ npx react-native config
146
+ ```
147
+
148
+ 在输出中应该能看到 `@bm-fe/react-native-multi-bundle` 的 iOS 配置信息。
149
+
150
+ #### 准备 Bundles 目录
151
+
152
+ 确保 iOS 项目中有 `Bundles` 目录用于存放 bundle 文件:
153
+
154
+ 1. 在 Xcode 中,右键点击项目 → New Group,创建 `Bundles` 目录
155
+ 2. 或者在终端中创建:
156
+ ```bash
157
+ mkdir -p ios/YourProject/Bundles/modules
158
+ ```
126
159
 
127
- 将以下文件从 `templates/native/ios/` 添加到你的 iOS 项目:
160
+ 3. **重要**:确保 `Bundles` 目录已添加到 Xcode 项目的 Resources 中:
161
+ - 在 Xcode 中,右键点击 `Bundles` 目录 → Add Files to "YourProject"
162
+ - 确保 "Create folder references" 已选中(蓝色文件夹图标)
163
+ - 这样 bundle 文件才会被打包到 app 中
164
+
165
+ #### Bundle 文件结构
166
+
167
+ 构建完成后,iOS bundle 文件应放置在:
168
+
169
+ ```
170
+ ios/YourProject/Bundles/
171
+ ├── bundle-manifest.json
172
+ ├── main.jsbundle
173
+ └── modules/
174
+ ├── home.jsbundle
175
+ ├── details.jsbundle
176
+ └── settings.jsbundle
177
+ ```
178
+
179
+ #### 手动集成(可选,仅在自动链接不工作时使用)
180
+
181
+ <details>
182
+ <summary>点击展开手动集成步骤</summary>
183
+
184
+ ##### 1. 复制 Native 模块文件
185
+
186
+ 将以下文件从 `node_modules/@bm-fe/react-native-multi-bundle/ios/ModuleLoader/` 复制到你的 iOS 项目:
128
187
 
129
188
  - `ModuleLoaderModule.h`
130
- - `ModuleLoaderModule.m`
189
+ - `ModuleLoaderModule.mm`
131
190
 
132
191
  在 Xcode 中:
133
192
  1. 右键点击项目 → Add Files to "YourProject"
134
193
  2. 选择这两个文件
135
194
  3. 确保 "Copy items if needed" 已勾选
136
195
 
137
- #### 2. 注册模块
196
+ ##### 2. 验证编译
138
197
 
139
198
  iOS 端的模块会自动注册(通过 `RCT_EXPORT_MODULE`),无需额外配置。
140
199
 
141
- **注意**:iOS 端的 bundle 加载实现可能需要根据你的具体需求调整。当前模板提供了基础框架。
200
+ 重新构建项目:
201
+
202
+ ```bash
203
+ npx react-native run-ios
204
+ ```
205
+
206
+ </details>
142
207
 
143
208
  ## 初始化多 Bundle 系统
144
209
 
@@ -23,8 +23,7 @@ import java.io.InputStreamReader
23
23
 
24
24
  /**
25
25
  * 简单的 Bundle 加载模块
26
- * 用于在同一个 JS 运行时中动态加载子 bundle
27
- *
26
+ *
28
27
  * 新架构下通过反射获取 ReactInstance,然后调用 loadJSBundle 方法
29
28
  * 注意:ReactInstance 是 internal 类,必须完全通过反射操作
30
29
  */
@@ -34,11 +33,11 @@ class ModuleLoaderModule(private val reactContext: ReactApplicationContext) : Re
34
33
  companion object {
35
34
  const val NAME = "ModuleLoader"
36
35
  private const val TAG = "ModuleLoaderModule"
37
-
36
+
38
37
  // 缓存反射获取的类和方法,避免重复反射
39
38
  private var reactHostImplClass: Class<*>? = null
40
39
  private var reactInstanceClass: Class<*>? = null
41
-
40
+
42
41
  init {
43
42
  try {
44
43
  reactHostImplClass = Class.forName("com.facebook.react.runtime.ReactHostImpl")
@@ -49,9 +48,6 @@ class ModuleLoaderModule(private val reactContext: ReactApplicationContext) : Re
49
48
  }
50
49
  }
51
50
 
52
- // 记录已加载的 bundle,避免重复加载
53
- private val loadedBundles = mutableSetOf<String>()
54
-
55
51
  // 协程作用域,用于异步操作
56
52
  private val moduleScope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
57
53
 
@@ -59,71 +55,77 @@ class ModuleLoaderModule(private val reactContext: ReactApplicationContext) : Re
59
55
 
60
56
  /**
61
57
  * 加载子 bundle
62
- *
63
- * @param bundleId 模块 ID(如 "home", "details", "settings")
64
- * @param bundlePath bundle 文件路径(如 "modules/home.bundle"
65
- * @param promise 加载结果回调
58
+ *
59
+ * 加载策略(按优先级):
60
+ * 1. 旧架构:CatalystInstance(当前 RN 0.79.5
61
+ * 2. 新架构:ReactHostImpl(完全 Bridgeless)
66
62
  */
67
63
  @ReactMethod
68
64
  fun loadBusinessBundle(bundleId: String, bundlePath: String, promise: Promise) {
69
65
  Log.d(TAG, "loadBusinessBundle: bundleId=$bundleId, bundlePath=$bundlePath")
70
66
 
71
- // 已经加载过的 bundle,直接返回成功
72
- if (loadedBundles.contains(bundleId)) {
73
- Log.d(TAG, "Bundle already loaded: $bundleId")
74
- val result = Arguments.createMap().apply {
75
- putBoolean("success", true)
76
- }
77
- promise.resolve(result)
67
+ // 1️⃣ 优先:旧架构 CatalystInstance
68
+ if (tryLoadWithCatalystInstance(bundleId, bundlePath, promise)) {
78
69
  return
79
70
  }
80
71
 
81
- try {
82
- // 获取 ReactApplication
83
- val application = reactContext.applicationContext as? android.app.Application
84
- val reactApplication = application as? com.facebook.react.ReactApplication
85
-
86
- if (reactApplication == null) {
87
- Log.e(TAG, "Application is not a ReactApplication")
88
- val result = Arguments.createMap().apply {
89
- putBoolean("success", false)
90
- putString("errorMessage", "Application is not a ReactApplication")
91
- }
92
- promise.resolve(result)
93
- return
94
- }
72
+ // 2️⃣ 其次:新架构 ReactHostImpl
73
+ if (tryLoadWithReactHost(bundleId, bundlePath, promise)) {
74
+ return
75
+ }
95
76
 
96
- val reactHost = reactApplication.reactHost
97
-
98
- // 构建 assets 路径
99
- val assetUrl = "assets://$bundlePath"
100
- Log.d(TAG, "Loading bundle from: $assetUrl")
101
-
102
- // 创建 JSBundleLoader
103
- val bundleLoader = JSBundleLoader.createAssetLoader(
104
- reactContext,
105
- assetUrl,
106
- false // 异步加载
107
- )
108
-
109
- // 检查是否是新架构(ReactHostImpl)
110
- val hostImplClass = reactHostImplClass
111
- if (reactHost != null && hostImplClass != null && hostImplClass.isInstance(reactHost)) {
112
- loadBundleViaReflection(reactHost, bundleLoader, bundleId, promise)
113
- } else {
114
- // 旧架构回退方案:使用 CatalystInstance
115
- Log.d(TAG, "Falling back to CatalystInstance (old architecture)")
116
- loadBundleWithCatalystInstance(bundleId, bundlePath, promise)
117
- }
77
+ // 都失败
78
+ Log.e(TAG, "No suitable runtime found")
79
+ promise.resolve(Arguments.createMap().apply {
80
+ putBoolean("success", false)
81
+ putString("errorMessage", "No CatalystInstance or ReactHostImpl available")
82
+ })
83
+ }
84
+
85
+ /**
86
+ * 尝试使用旧架构 CatalystInstance 加载
87
+ * @return true 如果成功处理(包括成功加载或失败),false 如果 CatalystInstance 不可用
88
+ */
89
+ @Suppress("DEPRECATION")
90
+ private fun tryLoadWithCatalystInstance(bundleId: String, bundlePath: String, promise: Promise): Boolean {
91
+ val catalystInstance = reactContext.catalystInstance ?: return false
118
92
 
93
+ Log.d(TAG, "Using CatalystInstance (same JS Runtime guaranteed)")
94
+ try {
95
+ val assetPath = "assets://$bundlePath"
96
+ catalystInstance.loadScriptFromAssets(reactContext.assets, assetPath, false)
97
+ Log.d(TAG, "Bundle loaded successfully: $bundleId")
98
+ promise.resolve(Arguments.createMap().apply { putBoolean("success", true) })
119
99
  } catch (e: Exception) {
120
100
  Log.e(TAG, "Failed to load bundle: $bundleId", e)
121
- val result = Arguments.createMap().apply {
101
+ promise.resolve(Arguments.createMap().apply {
122
102
  putBoolean("success", false)
123
103
  putString("errorMessage", e.message ?: "Unknown error")
124
- }
125
- promise.resolve(result)
104
+ })
105
+ }
106
+ return true
107
+ }
108
+
109
+ /**
110
+ * 尝试使用新架构 ReactHostImpl 加载
111
+ * @return true 如果成功处理,false 如果 ReactHostImpl 不可用
112
+ */
113
+ private fun tryLoadWithReactHost(bundleId: String, bundlePath: String, promise: Promise): Boolean {
114
+ val application = reactContext.applicationContext as? android.app.Application
115
+ val reactApplication = application as? com.facebook.react.ReactApplication ?: return false
116
+
117
+ val reactHost = reactApplication.reactHost
118
+ val hostImplClass = reactHostImplClass
119
+
120
+ if (reactHost == null || hostImplClass == null || !hostImplClass.isInstance(reactHost)) {
121
+ return false
126
122
  }
123
+
124
+ Log.d(TAG, "Using ReactHostImpl (new architecture)")
125
+ val assetUrl = "assets://$bundlePath"
126
+ val bundleLoader = JSBundleLoader.createAssetLoader(reactContext, assetUrl, false)
127
+ loadBundleViaReflection(reactHost, bundleLoader, bundleId, promise)
128
+ return true
127
129
  }
128
130
 
129
131
  /**
@@ -138,12 +140,12 @@ class ModuleLoaderModule(private val reactContext: ReactApplicationContext) : Re
138
140
  try {
139
141
  val hostImplClass = reactHostImplClass ?: throw Exception("ReactHostImpl class not found")
140
142
  val instanceClass = reactInstanceClass ?: throw Exception("ReactInstance class not found")
141
-
143
+
142
144
  // 通过反射获取 private 的 reactInstance 字段
143
145
  val reactInstanceField = hostImplClass.getDeclaredField("reactInstance")
144
146
  reactInstanceField.isAccessible = true
145
147
  val reactInstance = reactInstanceField.get(reactHost)
146
-
148
+
147
149
  if (reactInstance == null) {
148
150
  Log.e(TAG, "ReactInstance is null")
149
151
  val result = Arguments.createMap().apply {
@@ -157,59 +159,16 @@ class ModuleLoaderModule(private val reactContext: ReactApplicationContext) : Re
157
159
  // 通过反射调用 ReactInstance.loadJSBundle 方法
158
160
  val loadJSBundleMethod = instanceClass.getMethod("loadJSBundle", JSBundleLoader::class.java)
159
161
  loadJSBundleMethod.invoke(reactInstance, bundleLoader)
160
-
161
- // 标记为已加载
162
- loadedBundles.add(bundleId)
162
+
163
163
  Log.d(TAG, "Bundle loaded successfully via ReactInstance: $bundleId")
164
164
 
165
165
  val result = Arguments.createMap().apply {
166
166
  putBoolean("success", true)
167
167
  }
168
168
  promise.resolve(result)
169
-
170
- } catch (e: Exception) {
171
- Log.e(TAG, "Failed to load bundle via ReactInstance: $bundleId", e)
172
- val result = Arguments.createMap().apply {
173
- putBoolean("success", false)
174
- putString("errorMessage", e.message ?: "Unknown error")
175
- }
176
- promise.resolve(result)
177
- }
178
- }
179
-
180
- /**
181
- * 旧架构回退方案:使用 CatalystInstance 加载 bundle
182
- */
183
- @Suppress("DEPRECATION")
184
- private fun loadBundleWithCatalystInstance(bundleId: String, bundlePath: String, promise: Promise) {
185
- try {
186
- val catalystInstance = reactContext.catalystInstance
187
- if (catalystInstance == null) {
188
- Log.e(TAG, "CatalystInstance is null")
189
- val result = Arguments.createMap().apply {
190
- putBoolean("success", false)
191
- putString("errorMessage", "CatalystInstance not available")
192
- }
193
- promise.resolve(result)
194
- return
195
- }
196
-
197
- val assetPath = "assets://$bundlePath"
198
- catalystInstance.loadScriptFromAssets(
199
- reactContext.assets,
200
- assetPath,
201
- false
202
- )
203
169
 
204
- loadedBundles.add(bundleId)
205
- Log.d(TAG, "Bundle loaded successfully via CatalystInstance: $bundleId")
206
-
207
- val result = Arguments.createMap().apply {
208
- putBoolean("success", true)
209
- }
210
- promise.resolve(result)
211
170
  } catch (e: Exception) {
212
- Log.e(TAG, "Failed to load bundle via CatalystInstance: $bundleId", e)
171
+ Log.e(TAG, "Failed to load bundle via ReactInstance: $bundleId", e)
213
172
  val result = Arguments.createMap().apply {
214
173
  putBoolean("success", false)
215
174
  putString("errorMessage", e.message ?: "Unknown error")
@@ -218,24 +177,6 @@ class ModuleLoaderModule(private val reactContext: ReactApplicationContext) : Re
218
177
  }
219
178
  }
220
179
 
221
- /**
222
- * 检查 bundle 是否已加载
223
- */
224
- @ReactMethod
225
- fun isBundleLoaded(bundleId: String, promise: Promise) {
226
- promise.resolve(loadedBundles.contains(bundleId))
227
- }
228
-
229
- /**
230
- * 获取已加载的 bundle 列表
231
- */
232
- @ReactMethod
233
- fun getLoadedBundles(promise: Promise) {
234
- val array = Arguments.createArray()
235
- loadedBundles.forEach { array.pushString(it) }
236
- promise.resolve(array)
237
- }
238
-
239
180
  // ==================== Bundle Manifest Support ====================
240
181
 
241
182
  /**
@@ -253,7 +194,7 @@ class ModuleLoaderModule(private val reactContext: ReactApplicationContext) : Re
253
194
  val codePushDir = "${reactContext.filesDir.absolutePath}/CodePush"
254
195
  val assetsBaseManifestPath = "$codePushDir/assets-base/bundle-manifest.json"
255
196
  val assetsBaseManifestFile = File(assetsBaseManifestPath)
256
-
197
+
257
198
  if (assetsBaseManifestFile.exists()) {
258
199
  promise.resolve(assetsBaseManifestPath)
259
200
  return
@@ -283,7 +224,7 @@ class ModuleLoaderModule(private val reactContext: ReactApplicationContext) : Re
283
224
  val codePushDir = "${reactContext.filesDir.absolutePath}/CodePush"
284
225
  val assetsBaseManifestPath = "$codePushDir/assets-base/bundle-manifest.json"
285
226
  val assetsBaseManifestFile = File(assetsBaseManifestPath)
286
-
227
+
287
228
  if (assetsBaseManifestFile.exists()) {
288
229
  val content = readFileContent(assetsBaseManifestFile)
289
230
  if (content != null) {
@@ -445,5 +386,3 @@ class ModuleLoaderModule(private val reactContext: ReactApplicationContext) : Re
445
386
  }
446
387
  }
447
388
  }
448
-
449
-
@@ -0,0 +1,12 @@
1
+ #import <React/RCTBridgeModule.h>
2
+
3
+ /**
4
+ * ModuleLoader - iOS 原生模块
5
+ * 用于在同一个 JS 运行时中动态加载子 bundle
6
+ *
7
+ * 新架构下通过 RCTHost 加载 bundle
8
+ */
9
+ @interface ModuleLoaderModule : NSObject <RCTBridgeModule>
10
+
11
+ @end
12
+