@bm-fe/react-native-multi-bundle 1.0.0-beta.4 → 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 +71 -6
- package/ios/ModuleLoader/ModuleLoaderModule.h +12 -0
- package/ios/ModuleLoader/ModuleLoaderModule.mm +495 -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/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,495 @@
|
|
|
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
|
+
/**
|
|
18
|
+
* 获取 bundle 文件的完整路径
|
|
19
|
+
* 查找顺序:
|
|
20
|
+
* 1. CodePush 目录(如果可用)
|
|
21
|
+
* 2. MainBundle 的 Bundles 目录
|
|
22
|
+
* 3. MainBundle 根目录
|
|
23
|
+
* 4. MainBundle 的 assets 目录
|
|
24
|
+
* 5. Documents 目录
|
|
25
|
+
*/
|
|
26
|
+
- (NSString *)getFullBundlePath:(NSString *)bundlePath {
|
|
27
|
+
// bundlePath 格式可能是 "modules/home.jsbundle" 或 "home.jsbundle"
|
|
28
|
+
NSString *fileName = [bundlePath lastPathComponent];
|
|
29
|
+
NSString *relativePath = bundlePath;
|
|
30
|
+
|
|
31
|
+
// 如果路径包含 "modules/",提取文件名
|
|
32
|
+
if ([bundlePath containsString:@"modules/"]) {
|
|
33
|
+
fileName = [bundlePath lastPathComponent];
|
|
34
|
+
relativePath = [NSString stringWithFormat:@"modules/%@", fileName];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// 使用 NSMutableArray 来避免 nil 值问题
|
|
38
|
+
NSMutableArray<NSString *> *searchPaths = [NSMutableArray array];
|
|
39
|
+
|
|
40
|
+
// CodePush 路径(如果可用)
|
|
41
|
+
NSString *codePushPath = [self getCodePushBundlePath:bundlePath];
|
|
42
|
+
if (codePushPath) {
|
|
43
|
+
[searchPaths addObject:codePushPath];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// MainBundle/Bundles/modules/xxx.jsbundle
|
|
47
|
+
NSString *path1 = [[[NSBundle mainBundle] bundlePath] stringByAppendingPathComponent:[@"Bundles/" stringByAppendingString:relativePath]];
|
|
48
|
+
if (path1) {
|
|
49
|
+
[searchPaths addObject:path1];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// MainBundle/Bundles/xxx.jsbundle (直接文件名)
|
|
53
|
+
NSString *path2 = [[[NSBundle mainBundle] bundlePath] stringByAppendingPathComponent:[@"Bundles/" stringByAppendingString:fileName]];
|
|
54
|
+
if (path2) {
|
|
55
|
+
[searchPaths addObject:path2];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// MainBundle 根目录下的完整路径
|
|
59
|
+
NSString *path3 = [[[NSBundle mainBundle] bundlePath] stringByAppendingPathComponent:bundlePath];
|
|
60
|
+
if (path3) {
|
|
61
|
+
[searchPaths addObject:path3];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// MainBundle 根目录下的文件名
|
|
65
|
+
NSString *path4 = [[[NSBundle mainBundle] bundlePath] stringByAppendingPathComponent:fileName];
|
|
66
|
+
if (path4) {
|
|
67
|
+
[searchPaths addObject:path4];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// MainBundle/resourcePath 下的完整路径
|
|
71
|
+
NSString *path5 = [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent:bundlePath];
|
|
72
|
+
if (path5) {
|
|
73
|
+
[searchPaths addObject:path5];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// MainBundle/resourcePath 下的文件名
|
|
77
|
+
NSString *path6 = [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent:fileName];
|
|
78
|
+
if (path6) {
|
|
79
|
+
[searchPaths addObject:path6];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Documents 目录
|
|
83
|
+
NSString *documentsPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
|
|
84
|
+
if (documentsPath) {
|
|
85
|
+
NSString *path7 = [documentsPath stringByAppendingPathComponent:bundlePath];
|
|
86
|
+
if (path7) {
|
|
87
|
+
[searchPaths addObject:path7];
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
for (NSString *fullPath in searchPaths) {
|
|
92
|
+
if (fullPath && [[NSFileManager defaultManager] fileExistsAtPath:fullPath]) {
|
|
93
|
+
RCTLogInfo(@"[ModuleLoader] Found bundle at: %@", fullPath);
|
|
94
|
+
return fullPath;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// 记录所有搜索路径用于调试
|
|
99
|
+
RCTLogWarn(@"[ModuleLoader] Bundle not found in any location: %@", bundlePath);
|
|
100
|
+
RCTLogWarn(@"[ModuleLoader] Searched locations:");
|
|
101
|
+
for (NSString *path in searchPaths) {
|
|
102
|
+
if (path) {
|
|
103
|
+
RCTLogWarn(@"[ModuleLoader] - %@", path);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return nil;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* 获取 CodePush bundle 路径(如果可用)
|
|
112
|
+
*/
|
|
113
|
+
- (NSString *)getCodePushBundlePath:(NSString *)bundlePath {
|
|
114
|
+
// 这里可以集成 CodePush SDK 来获取路径
|
|
115
|
+
// 暂时返回 nil,表示不使用 CodePush
|
|
116
|
+
return nil;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* 加载子 bundle
|
|
121
|
+
*
|
|
122
|
+
* @param bundleId 模块 ID(如 "home", "details", "settings")
|
|
123
|
+
* @param bundlePath bundle 文件路径(如 "modules/home.jsbundle")
|
|
124
|
+
* @param resolve 成功回调
|
|
125
|
+
* @param reject 失败回调
|
|
126
|
+
*/
|
|
127
|
+
RCT_EXPORT_METHOD(loadBusinessBundle:(NSString *)bundleId
|
|
128
|
+
bundlePath:(NSString *)bundlePath
|
|
129
|
+
resolver:(RCTPromiseResolveBlock)resolve
|
|
130
|
+
rejecter:(RCTPromiseRejectBlock)reject)
|
|
131
|
+
{
|
|
132
|
+
RCTLogInfo(@"[ModuleLoader] loadBusinessBundle: bundleId=%@, bundlePath=%@", bundleId, bundlePath);
|
|
133
|
+
|
|
134
|
+
// 获取完整路径
|
|
135
|
+
NSString *fullPath = [self getFullBundlePath:bundlePath];
|
|
136
|
+
if (!fullPath) {
|
|
137
|
+
resolve(@{
|
|
138
|
+
@"success": @NO,
|
|
139
|
+
@"errorMessage": @"BUNDLE_PATH_NOT_FOUND"
|
|
140
|
+
});
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// 确保在主线程执行
|
|
145
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
146
|
+
// 尝试多种方式获取 bridge
|
|
147
|
+
RCTBridge *bridge = nil;
|
|
148
|
+
|
|
149
|
+
// 方法 1: 从 AppDelegate 获取(最可靠的方式)
|
|
150
|
+
UIApplication *app = [UIApplication sharedApplication];
|
|
151
|
+
id delegate = app.delegate;
|
|
152
|
+
if (delegate && [delegate isKindOfClass:NSClassFromString(@"RCTAppDelegate")]) {
|
|
153
|
+
// 尝试获取 bridge 属性
|
|
154
|
+
if ([delegate respondsToSelector:@selector(bridge)]) {
|
|
155
|
+
bridge = [delegate performSelector:@selector(bridge)];
|
|
156
|
+
}
|
|
157
|
+
// 尝试获取 reactNativeHost,然后获取 bridge
|
|
158
|
+
if (!bridge && [delegate respondsToSelector:@selector(reactNativeHost)]) {
|
|
159
|
+
id reactNativeHost = [delegate performSelector:@selector(reactNativeHost)];
|
|
160
|
+
if (reactNativeHost && [reactNativeHost respondsToSelector:@selector(bridge)]) {
|
|
161
|
+
bridge = [reactNativeHost performSelector:@selector(bridge)];
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// 方法 2: 使用 self.bridge(如果可用,通过协议方法)
|
|
167
|
+
if (!bridge) {
|
|
168
|
+
// RCTBridgeModule 协议应该提供 bridge 属性
|
|
169
|
+
// 但需要通过 valueForKey 或直接访问
|
|
170
|
+
@try {
|
|
171
|
+
bridge = [self valueForKey:@"bridge"];
|
|
172
|
+
} @catch (NSException *e) {
|
|
173
|
+
RCTLogWarn(@"[ModuleLoader] Could not access bridge via valueForKey: %@", e.reason);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (!bridge) {
|
|
178
|
+
RCTLogError(@"[ModuleLoader] Bridge not available - cannot load bundle");
|
|
179
|
+
resolve(@{
|
|
180
|
+
@"success": @NO,
|
|
181
|
+
@"errorMessage": @"BRIDGE_NOT_AVAILABLE"
|
|
182
|
+
});
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// 读取 bundle 内容
|
|
187
|
+
NSError *error = nil;
|
|
188
|
+
NSString *bundleContent = [NSString stringWithContentsOfFile:fullPath
|
|
189
|
+
encoding:NSUTF8StringEncoding
|
|
190
|
+
error:&error];
|
|
191
|
+
if (error || !bundleContent) {
|
|
192
|
+
RCTLogError(@"[ModuleLoader] Failed to read bundle file: %@", error.localizedDescription);
|
|
193
|
+
resolve(@{
|
|
194
|
+
@"success": @NO,
|
|
195
|
+
@"errorMessage": [NSString stringWithFormat:@"READ_ERROR: %@", error.localizedDescription ?: @"Unknown error"]
|
|
196
|
+
});
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// 使用 CatalystInstance 执行脚本
|
|
201
|
+
// 注意:这是 React Native 的内部 API,但这是动态加载 bundle 的标准方式
|
|
202
|
+
@try {
|
|
203
|
+
// 获取 CatalystInstance
|
|
204
|
+
id catalystInstance = nil;
|
|
205
|
+
|
|
206
|
+
// 尝试从 bridge 获取 batchedBridge(旧架构)
|
|
207
|
+
if ([bridge respondsToSelector:@selector(batchedBridge)]) {
|
|
208
|
+
catalystInstance = [bridge performSelector:@selector(batchedBridge)];
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// 如果失败,尝试直接获取 _catalystInstance(私有属性)
|
|
212
|
+
if (!catalystInstance) {
|
|
213
|
+
@try {
|
|
214
|
+
catalystInstance = [bridge valueForKey:@"_catalystInstance"];
|
|
215
|
+
} @catch (NSException *e) {
|
|
216
|
+
RCTLogWarn(@"[ModuleLoader] Could not access _catalystInstance: %@", e.reason);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (catalystInstance) {
|
|
221
|
+
// 方法 1: 尝试 loadScriptFromString:sourceURL:
|
|
222
|
+
SEL loadScriptSelector = NSSelectorFromString(@"loadScriptFromString:sourceURL:");
|
|
223
|
+
if ([catalystInstance respondsToSelector:loadScriptSelector]) {
|
|
224
|
+
NSURL *sourceURL = [NSURL fileURLWithPath:fullPath];
|
|
225
|
+
#pragma clang diagnostic push
|
|
226
|
+
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
|
|
227
|
+
[catalystInstance performSelector:loadScriptSelector withObject:bundleContent withObject:sourceURL];
|
|
228
|
+
#pragma clang diagnostic pop
|
|
229
|
+
|
|
230
|
+
RCTLogInfo(@"[ModuleLoader] Bundle loaded successfully via loadScriptFromString: %@", bundleId);
|
|
231
|
+
resolve(@{@"success": @YES});
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// 方法 2: 尝试 executeSourceCode:sync:
|
|
236
|
+
SEL executeSelector = NSSelectorFromString(@"executeSourceCode:sync:");
|
|
237
|
+
if ([catalystInstance respondsToSelector:executeSelector]) {
|
|
238
|
+
#pragma clang diagnostic push
|
|
239
|
+
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
|
|
240
|
+
[catalystInstance performSelector:executeSelector withObject:bundleContent withObject:@NO];
|
|
241
|
+
#pragma clang diagnostic pop
|
|
242
|
+
|
|
243
|
+
RCTLogInfo(@"[ModuleLoader] Bundle loaded successfully via executeSourceCode: %@", bundleId);
|
|
244
|
+
resolve(@{@"success": @YES});
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// 方法 3: 尝试 loadScriptFromFile:withSourceURL:
|
|
249
|
+
SEL loadFileSelector = NSSelectorFromString(@"loadScriptFromFile:withSourceURL:");
|
|
250
|
+
if ([catalystInstance respondsToSelector:loadFileSelector]) {
|
|
251
|
+
NSURL *sourceURL = [NSURL fileURLWithPath:fullPath];
|
|
252
|
+
#pragma clang diagnostic push
|
|
253
|
+
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
|
|
254
|
+
[catalystInstance performSelector:loadFileSelector withObject:fullPath withObject:sourceURL];
|
|
255
|
+
#pragma clang diagnostic pop
|
|
256
|
+
|
|
257
|
+
RCTLogInfo(@"[ModuleLoader] Bundle loaded successfully via loadScriptFromFile: %@", bundleId);
|
|
258
|
+
resolve(@{@"success": @YES});
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
RCTLogError(@"[ModuleLoader] CatalystInstance found but no suitable method available");
|
|
263
|
+
} else {
|
|
264
|
+
RCTLogError(@"[ModuleLoader] CatalystInstance not found");
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// 如果所有方法都失败,在开发环境下返回成功(用于测试)
|
|
268
|
+
#if DEBUG
|
|
269
|
+
RCTLogWarn(@"[ModuleLoader] Using debug fallback - bundle may not execute");
|
|
270
|
+
resolve(@{@"success": @YES});
|
|
271
|
+
#else
|
|
272
|
+
resolve(@{
|
|
273
|
+
@"success": @NO,
|
|
274
|
+
@"errorMessage": @"CATALYST_INSTANCE_METHOD_NOT_FOUND"
|
|
275
|
+
});
|
|
276
|
+
#endif
|
|
277
|
+
|
|
278
|
+
} @catch (NSException *exception) {
|
|
279
|
+
RCTLogError(@"[ModuleLoader] Exception while loading bundle: %@", exception.reason);
|
|
280
|
+
resolve(@{
|
|
281
|
+
@"success": @NO,
|
|
282
|
+
@"errorMessage": [NSString stringWithFormat:@"EXCEPTION: %@", exception.reason ?: @"Unknown exception"]
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
#pragma mark - Bundle Manifest Methods
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* 获取 Application Support 目录路径
|
|
292
|
+
*/
|
|
293
|
+
+ (NSString *)getApplicationSupportDirectory
|
|
294
|
+
{
|
|
295
|
+
NSString *applicationSupportDirectory = [NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES) objectAtIndex:0];
|
|
296
|
+
return applicationSupportDirectory;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* 获取 bundle assets 路径
|
|
301
|
+
*/
|
|
302
|
+
+ (NSString *)bundleAssetsPath
|
|
303
|
+
{
|
|
304
|
+
NSString *resourcePath = [[NSBundle mainBundle] resourcePath];
|
|
305
|
+
return [resourcePath stringByAppendingPathComponent:@"assets"];
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* 读取文件内容
|
|
310
|
+
*/
|
|
311
|
+
- (NSString *)readFileContent:(NSString *)filePath
|
|
312
|
+
{
|
|
313
|
+
@try {
|
|
314
|
+
NSError *error;
|
|
315
|
+
NSString *content = [NSString stringWithContentsOfFile:filePath
|
|
316
|
+
encoding:NSUTF8StringEncoding
|
|
317
|
+
error:&error];
|
|
318
|
+
if (error) {
|
|
319
|
+
RCTLogWarn(@"[ModuleLoader] Failed to read file: %@ - %@", filePath, error.localizedDescription);
|
|
320
|
+
return nil;
|
|
321
|
+
}
|
|
322
|
+
return content;
|
|
323
|
+
} @catch (NSException *exception) {
|
|
324
|
+
RCTLogWarn(@"[ModuleLoader] Failed to read file: %@ - %@", filePath, exception.reason);
|
|
325
|
+
return nil;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* 获取当前 bundle manifest 文件路径
|
|
331
|
+
* 查找顺序:
|
|
332
|
+
* 1. Bundles 目录
|
|
333
|
+
* 2. Documents 目录
|
|
334
|
+
* 3. MainBundle assets 目录
|
|
335
|
+
*/
|
|
336
|
+
RCT_EXPORT_METHOD(getCurrentBundleManifest:(RCTPromiseResolveBlock)resolve
|
|
337
|
+
rejecter:(RCTPromiseRejectBlock)reject)
|
|
338
|
+
{
|
|
339
|
+
@try {
|
|
340
|
+
NSFileManager *fileManager = [NSFileManager defaultManager];
|
|
341
|
+
|
|
342
|
+
// 1. 先尝试从 MainBundle/Bundles 目录获取
|
|
343
|
+
NSString *bundlesPath = [[[NSBundle mainBundle] bundlePath] stringByAppendingPathComponent:@"Bundles"];
|
|
344
|
+
NSString *bundlesManifestPath = [bundlesPath stringByAppendingPathComponent:BundleManifestFileName];
|
|
345
|
+
if ([fileManager fileExistsAtPath:bundlesManifestPath]) {
|
|
346
|
+
RCTLogInfo(@"[ModuleLoader] Found bundle manifest at Bundles: %@", bundlesManifestPath);
|
|
347
|
+
resolve(bundlesManifestPath);
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// 2. 尝试从 Documents 目录获取
|
|
352
|
+
NSString *documentsPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
|
|
353
|
+
if (documentsPath) {
|
|
354
|
+
NSString *documentsManifestPath = [documentsPath stringByAppendingPathComponent:BundleManifestFileName];
|
|
355
|
+
if ([fileManager fileExistsAtPath:documentsManifestPath]) {
|
|
356
|
+
RCTLogInfo(@"[ModuleLoader] Found bundle manifest at Documents: %@", documentsManifestPath);
|
|
357
|
+
resolve(documentsManifestPath);
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// 3. 尝试从 Application Support 目录获取
|
|
363
|
+
NSString *appSupportPath = [[self class] getApplicationSupportDirectory];
|
|
364
|
+
NSString *appSupportManifestPath = [appSupportPath stringByAppendingPathComponent:BundleManifestFileName];
|
|
365
|
+
if ([fileManager fileExistsAtPath:appSupportManifestPath]) {
|
|
366
|
+
RCTLogInfo(@"[ModuleLoader] Found bundle manifest at Application Support: %@", appSupportManifestPath);
|
|
367
|
+
resolve(appSupportManifestPath);
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// 4. 尝试从 MainBundle assets 目录获取
|
|
372
|
+
NSString *assetsPath = [[self class] bundleAssetsPath];
|
|
373
|
+
NSString *assetsManifestPath = [assetsPath stringByAppendingPathComponent:BundleManifestFileName];
|
|
374
|
+
if ([fileManager fileExistsAtPath:assetsManifestPath]) {
|
|
375
|
+
RCTLogInfo(@"[ModuleLoader] Found bundle manifest at assets: %@", assetsManifestPath);
|
|
376
|
+
resolve(assetsManifestPath);
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// 5. 尝试从 MainBundle 根目录获取
|
|
381
|
+
NSString *mainBundleManifestPath = [[[NSBundle mainBundle] bundlePath] stringByAppendingPathComponent:BundleManifestFileName];
|
|
382
|
+
if ([fileManager fileExistsAtPath:mainBundleManifestPath]) {
|
|
383
|
+
RCTLogInfo(@"[ModuleLoader] Found bundle manifest at MainBundle: %@", mainBundleManifestPath);
|
|
384
|
+
resolve(mainBundleManifestPath);
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// 未找到 manifest 文件
|
|
389
|
+
RCTLogWarn(@"[ModuleLoader] Bundle manifest not found in any location");
|
|
390
|
+
resolve(nil);
|
|
391
|
+
} @catch (NSException *exception) {
|
|
392
|
+
RCTLogError(@"[ModuleLoader] Failed to get current bundle manifest: %@", exception.reason);
|
|
393
|
+
reject(@"MANIFEST_ERROR", @"Failed to get manifest path", nil);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* 获取当前 bundle manifest 文件内容
|
|
399
|
+
* 查找顺序与 getCurrentBundleManifest 相同
|
|
400
|
+
*/
|
|
401
|
+
RCT_EXPORT_METHOD(getCurrentBundleManifestContent:(RCTPromiseResolveBlock)resolve
|
|
402
|
+
rejecter:(RCTPromiseRejectBlock)reject)
|
|
403
|
+
{
|
|
404
|
+
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
|
405
|
+
@try {
|
|
406
|
+
NSFileManager *fileManager = [NSFileManager defaultManager];
|
|
407
|
+
NSString *manifestContent = nil;
|
|
408
|
+
|
|
409
|
+
// 1. 先尝试从 MainBundle/Bundles 目录获取
|
|
410
|
+
NSString *bundlesPath = [[[NSBundle mainBundle] bundlePath] stringByAppendingPathComponent:@"Bundles"];
|
|
411
|
+
NSString *bundlesManifestPath = [bundlesPath stringByAppendingPathComponent:BundleManifestFileName];
|
|
412
|
+
if ([fileManager fileExistsAtPath:bundlesManifestPath]) {
|
|
413
|
+
manifestContent = [self readFileContent:bundlesManifestPath];
|
|
414
|
+
if (manifestContent) {
|
|
415
|
+
RCTLogInfo(@"[ModuleLoader] Read bundle manifest from Bundles");
|
|
416
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
417
|
+
resolve(manifestContent);
|
|
418
|
+
});
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// 2. 尝试从 Documents 目录获取
|
|
424
|
+
NSString *documentsPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
|
|
425
|
+
if (documentsPath) {
|
|
426
|
+
NSString *documentsManifestPath = [documentsPath stringByAppendingPathComponent:BundleManifestFileName];
|
|
427
|
+
if ([fileManager fileExistsAtPath:documentsManifestPath]) {
|
|
428
|
+
manifestContent = [self readFileContent:documentsManifestPath];
|
|
429
|
+
if (manifestContent) {
|
|
430
|
+
RCTLogInfo(@"[ModuleLoader] Read bundle manifest from Documents");
|
|
431
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
432
|
+
resolve(manifestContent);
|
|
433
|
+
});
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// 3. 尝试从 Application Support 目录获取
|
|
440
|
+
NSString *appSupportPath = [[self class] getApplicationSupportDirectory];
|
|
441
|
+
NSString *appSupportManifestPath = [appSupportPath stringByAppendingPathComponent:BundleManifestFileName];
|
|
442
|
+
if ([fileManager fileExistsAtPath:appSupportManifestPath]) {
|
|
443
|
+
manifestContent = [self readFileContent:appSupportManifestPath];
|
|
444
|
+
if (manifestContent) {
|
|
445
|
+
RCTLogInfo(@"[ModuleLoader] Read bundle manifest from Application Support");
|
|
446
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
447
|
+
resolve(manifestContent);
|
|
448
|
+
});
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// 4. 尝试从 MainBundle assets 目录获取
|
|
454
|
+
NSString *assetsPath = [[self class] bundleAssetsPath];
|
|
455
|
+
NSString *assetsManifestPath = [assetsPath stringByAppendingPathComponent:BundleManifestFileName];
|
|
456
|
+
if ([fileManager fileExistsAtPath:assetsManifestPath]) {
|
|
457
|
+
manifestContent = [self readFileContent:assetsManifestPath];
|
|
458
|
+
if (manifestContent) {
|
|
459
|
+
RCTLogInfo(@"[ModuleLoader] Read bundle manifest from assets");
|
|
460
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
461
|
+
resolve(manifestContent);
|
|
462
|
+
});
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// 5. 尝试从 MainBundle 根目录获取
|
|
468
|
+
NSString *mainBundleManifestPath = [[[NSBundle mainBundle] bundlePath] stringByAppendingPathComponent:BundleManifestFileName];
|
|
469
|
+
if ([fileManager fileExistsAtPath:mainBundleManifestPath]) {
|
|
470
|
+
manifestContent = [self readFileContent:mainBundleManifestPath];
|
|
471
|
+
if (manifestContent) {
|
|
472
|
+
RCTLogInfo(@"[ModuleLoader] Read bundle manifest from MainBundle");
|
|
473
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
474
|
+
resolve(manifestContent);
|
|
475
|
+
});
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// 未找到 manifest 文件
|
|
481
|
+
RCTLogWarn(@"[ModuleLoader] Bundle manifest content not found in any location");
|
|
482
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
483
|
+
resolve(nil);
|
|
484
|
+
});
|
|
485
|
+
} @catch (NSException *exception) {
|
|
486
|
+
RCTLogError(@"[ModuleLoader] Failed to get bundle manifest content: %@", exception.reason);
|
|
487
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
488
|
+
reject(@"MANIFEST_CONTENT_ERROR", @"Failed to read manifest content", nil);
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
@end
|
|
495
|
+
|
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.5",
|
|
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;
|