@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 +71 -6
- package/ios/ModuleLoader/ModuleLoaderModule.h +12 -0
- package/ios/ModuleLoader/ModuleLoaderModule.mm +474 -0
- package/package.json +54 -35
- package/react-native-multi-bundle.podspec +29 -0
- package/react-native.config.js +4 -1
- package/scripts/sync-bundles-to-assets.js +3 -3
- package/src/multi-bundle/LocalBundleManager.ts +32 -38
- package/src/multi-bundle/ModuleRegistry.ts +16 -16
- package/src/multi-bundle/README.md +1 -0
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
|
-
####
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
196
|
+
##### 2. 验证编译
|
|
138
197
|
|
|
139
198
|
iOS 端的模块会自动注册(通过 `RCT_EXPORT_MODULE`),无需额外配置。
|
|
140
199
|
|
|
141
|
-
|
|
200
|
+
重新构建项目:
|
|
201
|
+
|
|
202
|
+
```bash
|
|
203
|
+
npx react-native run-ios
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
</details>
|
|
142
207
|
|
|
143
208
|
## 初始化多 Bundle 系统
|
|
144
209
|
|
|
@@ -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.
|
|
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
|
-
"
|
|
51
|
-
"@react-navigation/native":
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
"react-native-
|
|
55
|
-
|
|
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": "
|
|
59
|
-
"@babel/preset-env": "
|
|
60
|
-
"@babel/runtime": "
|
|
61
|
-
"@react-native-community/cli": "
|
|
62
|
-
"@react-native-community/cli-platform-android": "
|
|
63
|
-
"@react-native-community/cli-platform-ios": "
|
|
64
|
-
"@react-native/babel-preset": "0.
|
|
65
|
-
"@react-native/eslint-config": "0.
|
|
66
|
-
"@react-native/gradle-plugin": "0.
|
|
67
|
-
"@react-native/metro-config": "0.
|
|
68
|
-
"@react-native/typescript-config": "0.
|
|
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": "
|
|
72
|
-
"@types/react": "
|
|
73
|
-
"@types/react-test-renderer": "
|
|
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": "
|
|
77
|
-
"jest": "
|
|
78
|
-
"patch-package": "
|
|
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.
|
|
81
|
-
"react
|
|
82
|
-
"react": "
|
|
83
|
-
"react-test-renderer": "
|
|
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.
|
|
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
|
package/react-native.config.js
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* 同步 Bundle 到 Assets 目录脚本
|
|
5
5
|
* 将构建好的 bundle 文件同步到 Android/iOS 的 assets 目录
|
|
6
6
|
*
|
|
7
|
-
* Android: android/app/src/main/assets/
|
|
8
|
-
* iOS: ios/DemoProject/
|
|
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/
|
|
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
|
-
* -
|
|
60
|
-
* -
|
|
59
|
+
* - 优先从 Native 模块获取(支持 CodePush 等热更新场景)
|
|
60
|
+
* - 如果 Native 返回 null,在开发环境下降级到开发服务器获取
|
|
61
|
+
* - 生产环境下如果 Native 返回 null,抛出异常
|
|
61
62
|
*/
|
|
62
63
|
async function getCurrentBundleManifest(): Promise<BundleManifest> {
|
|
63
|
-
//
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
//
|
|
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
|
|
211
|
+
let loader = NativeModules.ModuleLoader;;
|
|
212
212
|
|
|
213
|
-
if (!loader) {
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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;
|