@bm-fe/react-native-multi-bundle 1.0.0-beta.4 → 1.0.0-beta.6

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
 
@@ -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
+
@@ -0,0 +1,474 @@
1
+ #import "ModuleLoaderModule.h"
2
+ #import <React/RCTBridge+Private.h>
3
+ #import <React/RCTBridge.h>
4
+ #import <React/RCTUtils.h>
5
+ #import <React/RCTLog.h>
6
+
7
+ @implementation ModuleLoaderModule
8
+
9
+ RCT_EXPORT_MODULE(ModuleLoader);
10
+
11
+ // 合成 bridge 属性(RCTBridgeModule 协议要求)
12
+ @synthesize bridge = _bridge;
13
+
14
+ // Bundle manifest file name
15
+ static NSString *const BundleManifestFileName = @"bundle-manifest.json";
16
+
17
+ #pragma mark - Bundle Path Resolution
18
+
19
+ /**
20
+ * 获取 bundle 文件的完整路径
21
+ */
22
+ - (NSString *)getFullBundlePath:(NSString *)bundlePath {
23
+ NSString *fileName = [bundlePath lastPathComponent];
24
+ NSString *relativePath = bundlePath;
25
+
26
+ if ([bundlePath containsString:@"modules/"]) {
27
+ fileName = [bundlePath lastPathComponent];
28
+ relativePath = [NSString stringWithFormat:@"modules/%@", fileName];
29
+ }
30
+
31
+ NSMutableArray<NSString *> *searchPaths = [NSMutableArray array];
32
+
33
+ // CodePush 路径
34
+ NSString *codePushPath = [self getCodePushBundlePath:bundlePath];
35
+ if (codePushPath) {
36
+ [searchPaths addObject:codePushPath];
37
+ }
38
+
39
+ // MainBundle/Bundles/modules/xxx.jsbundle
40
+ NSString *path1 = [[[NSBundle mainBundle] bundlePath] stringByAppendingPathComponent:[@"Bundles/" stringByAppendingString:relativePath]];
41
+ if (path1) [searchPaths addObject:path1];
42
+
43
+ // MainBundle/Bundles/xxx.jsbundle
44
+ NSString *path2 = [[[NSBundle mainBundle] bundlePath] stringByAppendingPathComponent:[@"Bundles/" stringByAppendingString:fileName]];
45
+ if (path2) [searchPaths addObject:path2];
46
+
47
+ // MainBundle 根目录
48
+ NSString *path3 = [[[NSBundle mainBundle] bundlePath] stringByAppendingPathComponent:bundlePath];
49
+ if (path3) [searchPaths addObject:path3];
50
+
51
+ NSString *path4 = [[[NSBundle mainBundle] bundlePath] stringByAppendingPathComponent:fileName];
52
+ if (path4) [searchPaths addObject:path4];
53
+
54
+ // resourcePath
55
+ NSString *path5 = [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent:bundlePath];
56
+ if (path5) [searchPaths addObject:path5];
57
+
58
+ NSString *path6 = [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent:fileName];
59
+ if (path6) [searchPaths addObject:path6];
60
+
61
+ // Documents 目录
62
+ NSString *documentsPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
63
+ if (documentsPath) {
64
+ NSString *path7 = [documentsPath stringByAppendingPathComponent:bundlePath];
65
+ if (path7) [searchPaths addObject:path7];
66
+ }
67
+
68
+ for (NSString *fullPath in searchPaths) {
69
+ if (fullPath && [[NSFileManager defaultManager] fileExistsAtPath:fullPath]) {
70
+ RCTLogInfo(@"[ModuleLoader] Found bundle at: %@", fullPath);
71
+ return fullPath;
72
+ }
73
+ }
74
+
75
+ RCTLogWarn(@"[ModuleLoader] Bundle not found: %@", bundlePath);
76
+ for (NSString *path in searchPaths) {
77
+ if (path) RCTLogWarn(@"[ModuleLoader] - %@", path);
78
+ }
79
+
80
+ return nil;
81
+ }
82
+
83
+ - (NSString *)getCodePushBundlePath:(NSString *)bundlePath {
84
+ return nil;
85
+ }
86
+
87
+ #pragma mark - Bridge Access
88
+
89
+ /**
90
+ * 获取当前的 RCTBridge
91
+ */
92
+ - (RCTBridge *)getCurrentBridge {
93
+ // 方法 1: 使用模块自带的 bridge
94
+ if (self.bridge) {
95
+ return self.bridge;
96
+ }
97
+
98
+ // 方法 2: 从 AppDelegate 获取
99
+ UIApplication *app = [UIApplication sharedApplication];
100
+ id delegate = app.delegate;
101
+
102
+ if ([delegate respondsToSelector:@selector(bridge)]) {
103
+ RCTBridge *bridge = [delegate performSelector:@selector(bridge)];
104
+ if (bridge) return bridge;
105
+ }
106
+
107
+ // 方法 3: 通过 reactNativeHost
108
+ if ([delegate respondsToSelector:@selector(reactNativeHost)]) {
109
+ id reactNativeHost = [delegate performSelector:@selector(reactNativeHost)];
110
+ if (reactNativeHost && [reactNativeHost respondsToSelector:@selector(bridge)]) {
111
+ RCTBridge *bridge = [reactNativeHost performSelector:@selector(bridge)];
112
+ if (bridge) return bridge;
113
+ }
114
+ }
115
+
116
+ // 方法 4: 通过 rootViewFactory (新架构)
117
+ if ([delegate respondsToSelector:@selector(rootViewFactory)]) {
118
+ id factory = [delegate performSelector:@selector(rootViewFactory)];
119
+ if (factory && [factory respondsToSelector:@selector(bridge)]) {
120
+ RCTBridge *bridge = [factory performSelector:@selector(bridge)];
121
+ if (bridge) return bridge;
122
+ }
123
+ }
124
+
125
+ return nil;
126
+ }
127
+
128
+ #pragma mark - Bundle Loading Methods
129
+
130
+ /**
131
+ * 尝试使用各种方法加载 bundle
132
+ */
133
+ - (BOOL)tryLoadBundle:(NSString *)bundleContent
134
+ sourceURL:(NSURL *)sourceURL
135
+ bridge:(RCTBridge *)bridge
136
+ error:(NSError **)outError {
137
+
138
+ // 获取 batchedBridge(在新旧架构中都可能存在)
139
+ id targetBridge = bridge;
140
+ @try {
141
+ if ([bridge respondsToSelector:@selector(batchedBridge)]) {
142
+ id batchedBridge = [bridge performSelector:@selector(batchedBridge)];
143
+ if (batchedBridge) {
144
+ targetBridge = batchedBridge;
145
+ RCTLogInfo(@"[ModuleLoader] Using batchedBridge");
146
+ }
147
+ }
148
+ } @catch (NSException *e) {
149
+ RCTLogWarn(@"[ModuleLoader] Could not get batchedBridge: %@", e.reason);
150
+ }
151
+
152
+ // 方法 1: 尝试 executeSourceCode:withSourceURL:onComplete: (新架构可能支持)
153
+ SEL executeWithCompleteSel = NSSelectorFromString(@"executeSourceCode:withSourceURL:onComplete:");
154
+ if ([targetBridge respondsToSelector:executeWithCompleteSel]) {
155
+ @try {
156
+ RCTLogInfo(@"[ModuleLoader] Trying executeSourceCode:withSourceURL:onComplete:");
157
+ NSData *sourceData = [bundleContent dataUsingEncoding:NSUTF8StringEncoding];
158
+
159
+ __block BOOL completed = NO;
160
+ __block NSError *execError = nil;
161
+
162
+ void (^onComplete)(NSError *) = ^(NSError *error) {
163
+ execError = error;
164
+ completed = YES;
165
+ };
166
+
167
+ NSMethodSignature *sig = [targetBridge methodSignatureForSelector:executeWithCompleteSel];
168
+ NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:sig];
169
+ [invocation setTarget:targetBridge];
170
+ [invocation setSelector:executeWithCompleteSel];
171
+ [invocation setArgument:&sourceData atIndex:2];
172
+ [invocation setArgument:&sourceURL atIndex:3];
173
+ [invocation setArgument:&onComplete atIndex:4];
174
+ [invocation invoke];
175
+
176
+ RCTLogInfo(@"[ModuleLoader] Bundle loaded via executeSourceCode:withSourceURL:onComplete:");
177
+ return YES;
178
+ } @catch (NSException *e) {
179
+ RCTLogWarn(@"[ModuleLoader] executeSourceCode:withSourceURL:onComplete: failed: %@", e.reason);
180
+ }
181
+ }
182
+
183
+ // 方法 2: 尝试通过 CatalystInstance(旧架构)
184
+ id catalystInstance = nil;
185
+ @try {
186
+ catalystInstance = [targetBridge valueForKey:@"_catalystInstance"];
187
+ } @catch (NSException *e) {
188
+ // 继续尝试其他方法
189
+ }
190
+
191
+ if (catalystInstance) {
192
+ RCTLogInfo(@"[ModuleLoader] Found CatalystInstance, trying legacy methods");
193
+
194
+ // 方法 2a: loadScriptFromString:sourceURL:
195
+ SEL loadScriptSel = NSSelectorFromString(@"loadScriptFromString:sourceURL:");
196
+ if ([catalystInstance respondsToSelector:loadScriptSel]) {
197
+ @try {
198
+ #pragma clang diagnostic push
199
+ #pragma clang diagnostic ignored "-Warc-performSelector-leaks"
200
+ [catalystInstance performSelector:loadScriptSel withObject:bundleContent withObject:sourceURL];
201
+ #pragma clang diagnostic pop
202
+ RCTLogInfo(@"[ModuleLoader] Bundle loaded via loadScriptFromString");
203
+ return YES;
204
+ } @catch (NSException *e) {
205
+ RCTLogWarn(@"[ModuleLoader] loadScriptFromString failed: %@", e.reason);
206
+ }
207
+ }
208
+
209
+ // 方法 2b: executeSourceCode:sync:
210
+ SEL executeSel = NSSelectorFromString(@"executeSourceCode:sync:");
211
+ if ([catalystInstance respondsToSelector:executeSel]) {
212
+ @try {
213
+ #pragma clang diagnostic push
214
+ #pragma clang diagnostic ignored "-Warc-performSelector-leaks"
215
+ [catalystInstance performSelector:executeSel withObject:bundleContent withObject:@NO];
216
+ #pragma clang diagnostic pop
217
+ RCTLogInfo(@"[ModuleLoader] Bundle loaded via executeSourceCode:sync:");
218
+ return YES;
219
+ } @catch (NSException *e) {
220
+ RCTLogWarn(@"[ModuleLoader] executeSourceCode:sync: failed: %@", e.reason);
221
+ }
222
+ }
223
+ } else {
224
+ RCTLogInfo(@"[ModuleLoader] CatalystInstance not available (likely new architecture)");
225
+ }
226
+
227
+ // 方法 3: 尝试使用 RCTCxxBridge 的方法
228
+ if ([NSStringFromClass([targetBridge class]) containsString:@"Cxx"]) {
229
+ RCTLogInfo(@"[ModuleLoader] Detected RCTCxxBridge, trying Cxx-specific methods");
230
+
231
+ SEL runJSBundleSel = NSSelectorFromString(@"runJSBundle:");
232
+ if ([targetBridge respondsToSelector:runJSBundleSel]) {
233
+ @try {
234
+ #pragma clang diagnostic push
235
+ #pragma clang diagnostic ignored "-Warc-performSelector-leaks"
236
+ [targetBridge performSelector:runJSBundleSel withObject:sourceURL];
237
+ #pragma clang diagnostic pop
238
+ RCTLogInfo(@"[ModuleLoader] Bundle loaded via runJSBundle:");
239
+ return YES;
240
+ } @catch (NSException *e) {
241
+ RCTLogWarn(@"[ModuleLoader] runJSBundle: failed: %@", e.reason);
242
+ }
243
+ }
244
+ }
245
+
246
+ // 所有方法都失败
247
+ if (outError) {
248
+ *outError = [NSError errorWithDomain:@"ModuleLoader"
249
+ code:1001
250
+ userInfo:@{NSLocalizedDescriptionKey: @"No suitable bundle loading method found"}];
251
+ }
252
+
253
+ return NO;
254
+ }
255
+
256
+ #pragma mark - Main Load Method
257
+
258
+ /**
259
+ * 加载子 bundle
260
+ */
261
+ RCT_EXPORT_METHOD(loadBusinessBundle:(NSString *)bundleId
262
+ bundlePath:(NSString *)bundlePath
263
+ resolver:(RCTPromiseResolveBlock)resolve
264
+ rejecter:(RCTPromiseRejectBlock)reject)
265
+ {
266
+ RCTLogInfo(@"[ModuleLoader] loadBusinessBundle: bundleId=%@, bundlePath=%@", bundleId, bundlePath);
267
+
268
+ NSString *fullPath = [self getFullBundlePath:bundlePath];
269
+ if (!fullPath) {
270
+ resolve(@{@"success": @NO, @"errorMessage": @"BUNDLE_PATH_NOT_FOUND"});
271
+ return;
272
+ }
273
+
274
+ dispatch_async(dispatch_get_main_queue(), ^{
275
+ RCTBridge *bridge = [self getCurrentBridge];
276
+
277
+ if (!bridge) {
278
+ RCTLogError(@"[ModuleLoader] Bridge not available");
279
+ resolve(@{@"success": @NO, @"errorMessage": @"BRIDGE_NOT_AVAILABLE"});
280
+ return;
281
+ }
282
+
283
+ // 读取 bundle 内容
284
+ NSError *readError = nil;
285
+ NSString *bundleContent = [NSString stringWithContentsOfFile:fullPath
286
+ encoding:NSUTF8StringEncoding
287
+ error:&readError];
288
+ if (readError || !bundleContent) {
289
+ RCTLogError(@"[ModuleLoader] Failed to read bundle: %@", readError.localizedDescription);
290
+ resolve(@{@"success": @NO, @"errorMessage": [NSString stringWithFormat:@"READ_ERROR: %@", readError.localizedDescription ?: @"Unknown"]});
291
+ return;
292
+ }
293
+
294
+ NSURL *sourceURL = [NSURL fileURLWithPath:fullPath];
295
+ NSError *loadError = nil;
296
+
297
+ BOOL success = [self tryLoadBundle:bundleContent sourceURL:sourceURL bridge:bridge error:&loadError];
298
+
299
+ if (success) {
300
+ RCTLogInfo(@"[ModuleLoader] Bundle loaded successfully: %@", bundleId);
301
+ resolve(@{@"success": @YES});
302
+ } else {
303
+ // 在 DEBUG 模式下返回成功(因为模块可能已经在主 bundle 中)
304
+ #if DEBUG
305
+ RCTLogWarn(@"[ModuleLoader] Native loading failed, but continuing (DEBUG mode)");
306
+ RCTLogWarn(@"[ModuleLoader] Note: In new architecture, dynamic bundle loading has limitations");
307
+ RCTLogWarn(@"[ModuleLoader] The module may need to be included in the main bundle");
308
+ resolve(@{@"success": @YES, @"warning": @"NATIVE_LOADING_FAILED_DEBUG_FALLBACK"});
309
+ #else
310
+ RCTLogError(@"[ModuleLoader] Failed to load bundle: %@", loadError.localizedDescription);
311
+ resolve(@{@"success": @NO, @"errorMessage": loadError.localizedDescription ?: @"UNKNOWN_ERROR"});
312
+ #endif
313
+ }
314
+ });
315
+ }
316
+
317
+ #pragma mark - Bundle Manifest Methods
318
+
319
+ + (NSString *)getApplicationSupportDirectory {
320
+ return [NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES) objectAtIndex:0];
321
+ }
322
+
323
+ + (NSString *)bundleAssetsPath {
324
+ return [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent:@"assets"];
325
+ }
326
+
327
+ - (NSString *)readFileContent:(NSString *)filePath {
328
+ @try {
329
+ NSError *error;
330
+ NSString *content = [NSString stringWithContentsOfFile:filePath encoding:NSUTF8StringEncoding error:&error];
331
+ if (error) {
332
+ RCTLogWarn(@"[ModuleLoader] Failed to read file: %@ - %@", filePath, error.localizedDescription);
333
+ return nil;
334
+ }
335
+ return content;
336
+ } @catch (NSException *exception) {
337
+ RCTLogWarn(@"[ModuleLoader] Failed to read file: %@ - %@", filePath, exception.reason);
338
+ return nil;
339
+ }
340
+ }
341
+
342
+ RCT_EXPORT_METHOD(getCurrentBundleManifest:(RCTPromiseResolveBlock)resolve
343
+ rejecter:(RCTPromiseRejectBlock)reject)
344
+ {
345
+ @try {
346
+ NSFileManager *fileManager = [NSFileManager defaultManager];
347
+
348
+ // 1. MainBundle/Bundles 目录
349
+ NSString *bundlesPath = [[[NSBundle mainBundle] bundlePath] stringByAppendingPathComponent:@"Bundles"];
350
+ NSString *bundlesManifestPath = [bundlesPath stringByAppendingPathComponent:BundleManifestFileName];
351
+ if ([fileManager fileExistsAtPath:bundlesManifestPath]) {
352
+ RCTLogInfo(@"[ModuleLoader] Found manifest at Bundles: %@", bundlesManifestPath);
353
+ resolve(bundlesManifestPath);
354
+ return;
355
+ }
356
+
357
+ // 2. Documents 目录
358
+ NSString *documentsPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
359
+ if (documentsPath) {
360
+ NSString *documentsManifestPath = [documentsPath stringByAppendingPathComponent:BundleManifestFileName];
361
+ if ([fileManager fileExistsAtPath:documentsManifestPath]) {
362
+ resolve(documentsManifestPath);
363
+ return;
364
+ }
365
+ }
366
+
367
+ // 3. Application Support 目录
368
+ NSString *appSupportPath = [[self class] getApplicationSupportDirectory];
369
+ NSString *appSupportManifestPath = [appSupportPath stringByAppendingPathComponent:BundleManifestFileName];
370
+ if ([fileManager fileExistsAtPath:appSupportManifestPath]) {
371
+ resolve(appSupportManifestPath);
372
+ return;
373
+ }
374
+
375
+ // 4. MainBundle assets 目录
376
+ NSString *assetsPath = [[self class] bundleAssetsPath];
377
+ NSString *assetsManifestPath = [assetsPath stringByAppendingPathComponent:BundleManifestFileName];
378
+ if ([fileManager fileExistsAtPath:assetsManifestPath]) {
379
+ resolve(assetsManifestPath);
380
+ return;
381
+ }
382
+
383
+ // 5. MainBundle 根目录
384
+ NSString *mainBundleManifestPath = [[[NSBundle mainBundle] bundlePath] stringByAppendingPathComponent:BundleManifestFileName];
385
+ if ([fileManager fileExistsAtPath:mainBundleManifestPath]) {
386
+ resolve(mainBundleManifestPath);
387
+ return;
388
+ }
389
+
390
+ RCTLogWarn(@"[ModuleLoader] Bundle manifest not found");
391
+ resolve(nil);
392
+ } @catch (NSException *exception) {
393
+ RCTLogError(@"[ModuleLoader] Failed to get manifest: %@", exception.reason);
394
+ reject(@"MANIFEST_ERROR", @"Failed to get manifest path", nil);
395
+ }
396
+ }
397
+
398
+ RCT_EXPORT_METHOD(getCurrentBundleManifestContent:(RCTPromiseResolveBlock)resolve
399
+ rejecter:(RCTPromiseRejectBlock)reject)
400
+ {
401
+ dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
402
+ @try {
403
+ NSFileManager *fileManager = [NSFileManager defaultManager];
404
+ NSString *manifestContent = nil;
405
+
406
+ // 1. MainBundle/Bundles 目录
407
+ NSString *bundlesPath = [[[NSBundle mainBundle] bundlePath] stringByAppendingPathComponent:@"Bundles"];
408
+ NSString *bundlesManifestPath = [bundlesPath stringByAppendingPathComponent:BundleManifestFileName];
409
+ if ([fileManager fileExistsAtPath:bundlesManifestPath]) {
410
+ manifestContent = [self readFileContent:bundlesManifestPath];
411
+ if (manifestContent) {
412
+ RCTLogInfo(@"[ModuleLoader] Read manifest from Bundles");
413
+ dispatch_async(dispatch_get_main_queue(), ^{ resolve(manifestContent); });
414
+ return;
415
+ }
416
+ }
417
+
418
+ // 2. Documents 目录
419
+ NSString *documentsPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
420
+ if (documentsPath) {
421
+ NSString *documentsManifestPath = [documentsPath stringByAppendingPathComponent:BundleManifestFileName];
422
+ if ([fileManager fileExistsAtPath:documentsManifestPath]) {
423
+ manifestContent = [self readFileContent:documentsManifestPath];
424
+ if (manifestContent) {
425
+ dispatch_async(dispatch_get_main_queue(), ^{ resolve(manifestContent); });
426
+ return;
427
+ }
428
+ }
429
+ }
430
+
431
+ // 3. Application Support 目录
432
+ NSString *appSupportPath = [[self class] getApplicationSupportDirectory];
433
+ NSString *appSupportManifestPath = [appSupportPath stringByAppendingPathComponent:BundleManifestFileName];
434
+ if ([fileManager fileExistsAtPath:appSupportManifestPath]) {
435
+ manifestContent = [self readFileContent:appSupportManifestPath];
436
+ if (manifestContent) {
437
+ dispatch_async(dispatch_get_main_queue(), ^{ resolve(manifestContent); });
438
+ return;
439
+ }
440
+ }
441
+
442
+ // 4. MainBundle assets 目录
443
+ NSString *assetsPath = [[self class] bundleAssetsPath];
444
+ NSString *assetsManifestPath = [assetsPath stringByAppendingPathComponent:BundleManifestFileName];
445
+ if ([fileManager fileExistsAtPath:assetsManifestPath]) {
446
+ manifestContent = [self readFileContent:assetsManifestPath];
447
+ if (manifestContent) {
448
+ dispatch_async(dispatch_get_main_queue(), ^{ resolve(manifestContent); });
449
+ return;
450
+ }
451
+ }
452
+
453
+ // 5. MainBundle 根目录
454
+ NSString *mainBundleManifestPath = [[[NSBundle mainBundle] bundlePath] stringByAppendingPathComponent:BundleManifestFileName];
455
+ if ([fileManager fileExistsAtPath:mainBundleManifestPath]) {
456
+ manifestContent = [self readFileContent:mainBundleManifestPath];
457
+ if (manifestContent) {
458
+ dispatch_async(dispatch_get_main_queue(), ^{ resolve(manifestContent); });
459
+ return;
460
+ }
461
+ }
462
+
463
+ RCTLogWarn(@"[ModuleLoader] Bundle manifest content not found");
464
+ dispatch_async(dispatch_get_main_queue(), ^{ resolve(nil); });
465
+ } @catch (NSException *exception) {
466
+ RCTLogError(@"[ModuleLoader] Failed to get manifest content: %@", exception.reason);
467
+ dispatch_async(dispatch_get_main_queue(), ^{
468
+ reject(@"MANIFEST_CONTENT_ERROR", @"Failed to read manifest content", nil);
469
+ });
470
+ }
471
+ });
472
+ }
473
+
474
+ @end
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bm-fe/react-native-multi-bundle",
3
- "version": "1.0.0-beta.4",
3
+ "version": "1.0.0-beta.6",
4
4
  "description": "React Native 多 Bundle 系统 - 支持模块按需加载和独立更新",
5
5
  "main": "src/index.ts",
6
6
  "types": "src/index.ts",
@@ -20,11 +20,16 @@
20
20
  "src",
21
21
  "scripts",
22
22
  "templates",
23
- "android/moduleloader",
23
+ "android/moduleloader/src",
24
+ "android/moduleloader/*.gradle",
25
+ "android/moduleloader/*.pro",
26
+ "ios/ModuleLoader",
24
27
  "react-native.config.js",
28
+ "react-native-multi-bundle.podspec",
25
29
  "README.md",
26
30
  "INTEGRATION.md",
27
- "LICENSE"
31
+ "LICENSE",
32
+ "!android/moduleloader/build"
28
33
  ],
29
34
  "bin": {
30
35
  "multi-bundle-build": "./scripts/build-multi-bundle.js"
@@ -45,47 +50,61 @@
45
50
  },
46
51
  "peerDependencies": {
47
52
  "react": ">=18.0.0",
48
- "react-native": ">=0.70.0"
53
+ "react-native": ">=0.70.0",
54
+ "@react-navigation/native": ">=6.0.0",
55
+ "@react-navigation/native-stack": ">=6.0.0",
56
+ "react-native-fs": ">=2.0.0",
57
+ "react-native-safe-area-context": ">=4.0.0",
58
+ "react-native-screens": ">=3.0.0"
49
59
  },
50
- "dependencies": {
51
- "@react-navigation/native": "^7.1.21",
52
- "@react-navigation/native-stack": "^7.6.4",
53
- "react-native-fs": "^2.20.0",
54
- "react-native-safe-area-context": "^5.6.2",
55
- "react-native-screens": "^4.18.0"
60
+ "peerDependenciesMeta": {
61
+ "@react-navigation/native": {
62
+ "optional": true
63
+ },
64
+ "@react-navigation/native-stack": {
65
+ "optional": true
66
+ },
67
+ "react-native-fs": {
68
+ "optional": true
69
+ },
70
+ "react-native-safe-area-context": {
71
+ "optional": true
72
+ },
73
+ "react-native-screens": {
74
+ "optional": true
75
+ }
56
76
  },
77
+ "dependencies": {},
57
78
  "devDependencies": {
58
- "@babel/core": "^7.28.5",
59
- "@babel/preset-env": "^7.28.5",
60
- "@babel/runtime": "^7.28.4",
61
- "@react-native-community/cli": "20.0.2",
62
- "@react-native-community/cli-platform-android": "20.0.2",
63
- "@react-native-community/cli-platform-ios": "20.0.2",
64
- "@react-native/babel-preset": "0.82.1",
65
- "@react-native/eslint-config": "0.82.1",
66
- "@react-native/gradle-plugin": "0.82.1",
67
- "@react-native/metro-config": "0.82.1",
68
- "@react-native/typescript-config": "0.82.1",
69
- "@rnx-kit/babel-preset-metro-react-native": "^3.0.0",
79
+ "@babel/core": "7.25.2",
80
+ "@babel/preset-env": "7.25.2",
81
+ "@babel/runtime": "7.25.0",
82
+ "@react-native-community/cli": "18.0.0",
83
+ "@react-native-community/cli-platform-android": "18.0.0",
84
+ "@react-native-community/cli-platform-ios": "18.0.0",
85
+ "@react-native/babel-preset": "0.79.0",
86
+ "@react-native/eslint-config": "0.79.0",
87
+ "@react-native/gradle-plugin": "0.79.0",
88
+ "@react-native/metro-config": "0.79.0",
89
+ "@react-native/typescript-config": "0.79.0",
70
90
  "@testing-library/react-native": "^13.3.3",
71
- "@types/jest": "^30.0.0",
72
- "@types/react": "^19.2.6",
73
- "@types/react-test-renderer": "^19.1.0",
74
- "babel-jest": "^30.2.0",
91
+ "@types/jest": "29.5.13",
92
+ "@types/react": "19.0.0",
93
+ "@types/react-test-renderer": "19.0.0",
75
94
  "babel-plugin-transform-inline-environment-variables": "^0.4.4",
76
- "eslint": "^9.39.1",
77
- "jest": "^30.2.0",
78
- "patch-package": "^8.0.1",
95
+ "eslint": "8.22.0",
96
+ "jest": "29.7.0",
97
+ "patch-package": "8.0.0",
79
98
  "postinstall-postinstall": "^2.1.0",
80
- "prettier": "3.6.2",
81
- "react-native": "^0.82.1",
82
- "react": "19.1.1",
83
- "react-test-renderer": "^19.1.1",
99
+ "prettier": "3.2.5",
100
+ "react": "19.0.0",
101
+ "react-native": "0.79.5",
102
+ "react-test-renderer": "19.0.0",
84
103
  "standard-version": "^9.5.0",
85
- "typescript": "5.9.3"
104
+ "typescript": "5.5.4"
86
105
  },
87
106
  "engines": {
88
- "node": ">=18"
107
+ "node": ">=18.0.0 <22.0.0"
89
108
  },
90
109
  "publishConfig": {
91
110
  "access": "public"
@@ -0,0 +1,29 @@
1
+ require "json"
2
+
3
+ package = JSON.parse(File.read(File.join(__dir__, "package.json")))
4
+
5
+ Pod::Spec.new do |s|
6
+ s.name = "react-native-multi-bundle"
7
+ s.version = package["version"]
8
+ s.summary = package["description"]
9
+ s.description = <<-DESC
10
+ React Native 多 Bundle 系统 - 支持模块按需加载和独立更新。
11
+ 提供 iOS 原生模块 ModuleLoader,用于在同一个 JS 运行时中动态加载子 bundle。
12
+ DESC
13
+ s.homepage = "https://github.com/AntonyMei/react-native-multiple-bundle-demo"
14
+ s.license = { :type => package["license"], :file => "LICENSE" }
15
+ s.author = package["author"]
16
+ s.platforms = { :ios => "13.0" }
17
+ s.source = { :git => "https://github.com/AntonyMei/react-native-multiple-bundle-demo.git", :tag => "v#{s.version}" }
18
+
19
+ s.source_files = "ios/ModuleLoader/**/*.{h,m,mm}"
20
+
21
+ # 编译选项
22
+ s.pod_target_xcconfig = {
23
+ "CLANG_CXX_LANGUAGE_STANDARD" => "c++17",
24
+ "DEFINES_MODULE" => "YES"
25
+ }
26
+
27
+ # 依赖 React Native Core
28
+ s.dependency "React-Core"
29
+ end
@@ -16,7 +16,10 @@ module.exports = {
16
16
  packageInstance: 'new ModuleLoaderPackage()',
17
17
  buildTypes: ['debug', 'release'],
18
18
  },
19
- ios: null, // iOS 暂未实现
19
+ ios: {
20
+ // iOS 使用 podspec 自动链接
21
+ podspecPath: './react-native-multi-bundle.podspec',
22
+ },
20
23
  },
21
24
  },
22
25
  };
@@ -4,8 +4,8 @@
4
4
  * 同步 Bundle 到 Assets 目录脚本
5
5
  * 将构建好的 bundle 文件同步到 Android/iOS 的 assets 目录
6
6
  *
7
- * Android: android/app/src/main/assets/bundles/
8
- * iOS: ios/DemoProject/RNModules/Bundles/
7
+ * Android: android/app/src/main/assets/
8
+ * iOS: ios/DemoProject/Bundles/
9
9
  */
10
10
 
11
11
  const fs = require('fs');
@@ -14,7 +14,7 @@ const path = require('path');
14
14
  const PROJECT_ROOT = path.join(__dirname, '..');
15
15
  const BUILD_DIR = path.join(PROJECT_ROOT, 'build/bundles');
16
16
  const ANDROID_ASSETS_DIR = path.join(PROJECT_ROOT, 'android/app/src/main/assets');
17
- const IOS_ASSETS_DIR = path.join(PROJECT_ROOT, 'ios/DemoProject/RNModules/Bundles');
17
+ const IOS_ASSETS_DIR = path.join(PROJECT_ROOT, 'ios/DemoProject/Bundles');
18
18
 
19
19
  /**
20
20
  * ANSI 颜色工具函数
@@ -56,47 +56,37 @@ function convertNativeManifestToBundleManifest(nativeManifest: any): BundleManif
56
56
 
57
57
  /**
58
58
  * 获取当前激活包的 manifest
59
- * - 开发环境:从开发服务器获取 manifest(HTTP 模式)
60
- * - 生产环境:从 Native CodePush 模块获取 manifest(直接返回 JSON 对象)
59
+ * - 优先从 Native 模块获取(支持 CodePush 等热更新场景)
60
+ * - 如果 Native 返回 null,在开发环境下降级到开发服务器获取
61
+ * - 生产环境下如果 Native 返回 null,抛出异常
61
62
  */
62
63
  async function getCurrentBundleManifest(): Promise<BundleManifest> {
63
- // 生产环境:从 Native 模块获取 manifest
64
- // if (!__DEV__) {
65
- // try {
66
- // const nativeManifest = await NativeModules.ModuleLoader.getCurrentBundleManifestContent();
67
- // if (nativeManifest) {
68
- // return convertNativeManifestToBundleManifest(nativeManifest);
69
- // } else {
70
- // // Native 模块返回 null 或 undefined
71
- // throw new Error(
72
- // '[LocalBundleManager] Native module returned null manifest. ' +
73
- // 'Please ensure bundle-manifest.json exists in the app bundle.'
74
- // );
75
- // }
76
- // } catch (error) {
77
- // console.error(
78
- // `[LocalBundleManager] Failed to get manifest from Native: ${error}`
79
- // );
80
- // // 生产环境无法获取 manifest 时抛出异常
81
- // throw new Error(
82
- // `[LocalBundleManager] Failed to get manifest from Native module: ${error}`
83
- // );
84
- // }
85
- // }
86
- console.log("ReactNativeJS","nativeManifest getCurrentBundleManifestContent")
87
- const nativeManifest = await NativeModules.ModuleLoader.getCurrentBundleManifestContent();
88
- console.log("ReactNativeJS","nativeManifest "+nativeManifest)
89
- if (nativeManifest) {
90
- return convertNativeManifestToBundleManifest(nativeManifest);
91
- } else {
92
- // Native 模块返回 null 或 undefined
93
- throw new Error(
94
- '[LocalBundleManager] Native module returned null manifest. ' +
95
- 'Please ensure bundle-manifest.json exists in the app bundle.'
96
- );
97
- }
64
+ // 先尝试从 Native 模块获取 manifest(支持 CodePush 等热更新场景)
65
+ try {
66
+ console.log('[LocalBundleManager] Trying to get manifest from Native module...');
67
+ const nativeManifest = await NativeModules.ModuleLoader.getCurrentBundleManifestContent();
68
+
69
+ if (nativeManifest) {
70
+ console.log('[LocalBundleManager] Got manifest from Native module');
71
+ return convertNativeManifestToBundleManifest(nativeManifest);
72
+ }
73
+
74
+ console.log('[LocalBundleManager] Native module returned null manifest');
75
+ } catch (error) {
76
+ console.warn(`[LocalBundleManager] Failed to get manifest from Native: ${error}`);
77
+ }
98
78
 
99
- // 开发环境:从开发服务器获取 manifest
79
+ // 生产环境下,如果 Native 返回 null,抛出异常
80
+ if (!__DEV__) {
81
+ throw new Error(
82
+ '[LocalBundleManager] Native module returned null manifest. ' +
83
+ 'Please ensure bundle-manifest.json exists in the app bundle.'
84
+ );
85
+ }
86
+
87
+ // 开发环境:降级到从开发服务器获取 manifest
88
+ console.log('[LocalBundleManager] Dev mode: trying to fetch manifest from dev server...');
89
+
100
90
  const config = getGlobalConfig();
101
91
  const devServer = config?.devServer;
102
92
 
@@ -105,9 +95,11 @@ async function getCurrentBundleManifest(): Promise<BundleManifest> {
105
95
  const protocol = devServer.protocol || 'http';
106
96
  const platform = Platform.OS;
107
97
  const manifestUrl = `${protocol}://${devServer.host}:${devServer.port}/bundle-manifest.json?platform=${platform}`;
98
+ console.log(`[LocalBundleManager] Fetching manifest from: ${manifestUrl}`);
108
99
  const response = await fetch(manifestUrl);
109
100
  if (response.ok) {
110
101
  const manifest: BundleManifest = await response.json();
102
+ console.log('[LocalBundleManager] Got manifest from dev server');
111
103
  return manifest;
112
104
  } else {
113
105
  console.warn(
@@ -125,9 +117,11 @@ async function getCurrentBundleManifest(): Promise<BundleManifest> {
125
117
  const host = Platform.OS === 'android' ? '10.0.2.2' : 'localhost';
126
118
  const platform = Platform.OS;
127
119
  const manifestUrl = `http://${host}:8081/bundle-manifest.json?platform=${platform}`;
120
+ console.log(`[LocalBundleManager] Fetching manifest from default: ${manifestUrl}`);
128
121
  const response = await fetch(manifestUrl);
129
122
  if (response.ok) {
130
123
  const manifest: BundleManifest = await response.json();
124
+ console.log('[LocalBundleManager] Got manifest from default dev server');
131
125
  return manifest;
132
126
  } else {
133
127
  console.warn(
@@ -208,23 +208,23 @@ async function loadModule(moduleId: string): Promise<void> {
208
208
  moduleState[moduleId] = 'loading';
209
209
 
210
210
  // Step 3:选择 ModuleLoader
211
- let loader: any = injectedModuleLoader;
211
+ let loader = NativeModules.ModuleLoader;;
212
212
 
213
- if (!loader) {
214
- // 开发环境:优先使用 ModuleLoaderMock(HTTP 模式)
215
- if (__DEV__) {
216
- loader = ModuleLoaderMock;
217
- } else {
218
- // 生产环境:使用 Native CodePush 模块
219
- if (NativeModules?.ModuleLoader.loadBusinessBundle) {
220
- loader = NativeModules.ModuleLoader;
221
- } else {
222
- throw new Error(
223
- 'ModuleLoader not available: Native ModuleLoader module not found in production'
224
- );
225
- }
226
- }
227
- }
213
+ // if (!loader) {
214
+ // // 开发环境:优先使用 ModuleLoaderMock(HTTP 模式)
215
+ // if (__DEV__) {
216
+ // loader = ModuleLoaderMock;
217
+ // } else {
218
+ // // 生产环境:使用 Native CodePush 模块
219
+ // if (NativeModules?.ModuleLoader.loadBusinessBundle) {
220
+ // loader = NativeModules.ModuleLoader;
221
+ // } else {
222
+ // throw new Error(
223
+ // 'ModuleLoader not available: Native ModuleLoader module not found in production'
224
+ // );
225
+ // }
226
+ // }
227
+ // }
228
228
 
229
229
  // 传入 bundleId(即 moduleId)和 bundlePath(从 manifest 中获取)
230
230
  const bundlePath = moduleMeta[moduleId].file;
@@ -342,3 +342,4 @@ function preloadModule(moduleId: string): Promise<void>
342
342
  - [多 Bundle 架构设计](../docs/React%20Native%20多%20bundle%20技术方案.md)
343
343
 
344
344
 
345
+