@bm-fe/react-native-multi-bundle 1.0.0-beta.0
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 +371 -0
- package/LICENSE +21 -0
- package/README.md +240 -0
- package/package.json +86 -0
- package/scripts/build-multi-bundle.js +483 -0
- package/scripts/release-codepush-dev.sh +90 -0
- package/scripts/release-codepush.js +420 -0
- package/scripts/sync-bundles-to-assets.js +252 -0
- package/src/index.ts +40 -0
- package/src/multi-bundle/DevModeConfig.ts +23 -0
- package/src/multi-bundle/HMRClient.ts +157 -0
- package/src/multi-bundle/LocalBundleManager.ts +155 -0
- package/src/multi-bundle/ModuleErrorFallback.tsx +85 -0
- package/src/multi-bundle/ModuleLoaderMock.ts +169 -0
- package/src/multi-bundle/ModuleLoadingPlaceholder.tsx +34 -0
- package/src/multi-bundle/ModuleRegistry.ts +295 -0
- package/src/multi-bundle/README.md +343 -0
- package/src/multi-bundle/config.ts +141 -0
- package/src/multi-bundle/createModuleLoader.tsx +92 -0
- package/src/multi-bundle/createModuleRouteLoader.tsx +31 -0
- package/src/multi-bundle/devUtils.ts +48 -0
- package/src/multi-bundle/init.ts +131 -0
- package/src/multi-bundle/metro-config-helper.js +140 -0
- package/src/multi-bundle/preloadModule.ts +33 -0
- package/src/multi-bundle/routeRegistry.ts +118 -0
- package/src/types/global.d.ts +14 -0
- package/templates/metro.config.js.template +45 -0
- package/templates/multi-bundle.config.json.template +31 -0
- package/templates/native/android/ModuleLoaderModule.kt +227 -0
- package/templates/native/android/ModuleLoaderPackage.kt +26 -0
- package/templates/native/ios/ModuleLoaderModule.h +13 -0
- package/templates/native/ios/ModuleLoaderModule.m +60 -0
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ModuleRegistry - 模块生命周期管理器
|
|
3
|
+
* 负责管理模块的加载、状态、依赖关系
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { NativeModules } from 'react-native';
|
|
7
|
+
import { ModuleLoader as ModuleLoaderMock } from './ModuleLoaderMock';
|
|
8
|
+
|
|
9
|
+
type ModuleState = 'idle' | 'loading' | 'loaded' | 'failed';
|
|
10
|
+
|
|
11
|
+
interface ModuleMeta {
|
|
12
|
+
file: string;
|
|
13
|
+
version: string;
|
|
14
|
+
dependencies: string[];
|
|
15
|
+
lazy: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface ModuleLoadPromise {
|
|
19
|
+
promise: Promise<void>;
|
|
20
|
+
resolveRegister: () => void;
|
|
21
|
+
rejectRegister: (error: Error) => void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface BundleManifest {
|
|
25
|
+
formatVersion: number;
|
|
26
|
+
main: {
|
|
27
|
+
file: string;
|
|
28
|
+
version: string;
|
|
29
|
+
};
|
|
30
|
+
modules: Array<{
|
|
31
|
+
id: string;
|
|
32
|
+
file: string;
|
|
33
|
+
version: string;
|
|
34
|
+
dependencies?: string[];
|
|
35
|
+
lazy?: boolean;
|
|
36
|
+
}>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const REGISTER_TIMEOUT_MS = 5000;
|
|
40
|
+
|
|
41
|
+
// 内部闭包数据(不暴露给业务方)
|
|
42
|
+
// 允许外部注入 ModuleLoader(主要用于测试)
|
|
43
|
+
let injectedModuleLoader: any = null;
|
|
44
|
+
|
|
45
|
+
const moduleMeta: Record<string, ModuleMeta> = {};
|
|
46
|
+
const moduleState: Record<string, ModuleState> = {};
|
|
47
|
+
const moduleExports: Record<string, any> = {};
|
|
48
|
+
const moduleLoadPromises: Record<string, ModuleLoadPromise> = {};
|
|
49
|
+
|
|
50
|
+
// 循环依赖检测
|
|
51
|
+
function checkCircularDependency(
|
|
52
|
+
moduleId: string,
|
|
53
|
+
visited: Set<string>,
|
|
54
|
+
path: string[]
|
|
55
|
+
): void {
|
|
56
|
+
if (visited.has(moduleId)) {
|
|
57
|
+
const cycle = [...path, moduleId].join(' -> ');
|
|
58
|
+
throw new Error(`Circular dependency detected: ${cycle}`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
visited.add(moduleId);
|
|
62
|
+
path.push(moduleId);
|
|
63
|
+
|
|
64
|
+
const deps = moduleMeta[moduleId]?.dependencies || [];
|
|
65
|
+
deps.forEach((depId) => {
|
|
66
|
+
if (moduleMeta[depId]) {
|
|
67
|
+
checkCircularDependency(depId, new Set(visited), [...path]);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* 设置 ModuleLoader(仅供测试使用)
|
|
74
|
+
* 注意:实际接口是 CodePush.loadBusinessBundle,但为了兼容性保留此方法名
|
|
75
|
+
*/
|
|
76
|
+
function setModuleLoader(loader: any) {
|
|
77
|
+
injectedModuleLoader = loader;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function resetInternalState() {
|
|
81
|
+
injectedModuleLoader = injectedModuleLoader; // 保留注入的 loader
|
|
82
|
+
|
|
83
|
+
for (const key of Object.keys(moduleMeta)) delete moduleMeta[key];
|
|
84
|
+
for (const key of Object.keys(moduleState)) delete moduleState[key];
|
|
85
|
+
for (const key of Object.keys(moduleExports)) delete moduleExports[key];
|
|
86
|
+
for (const key of Object.keys(moduleLoadPromises))
|
|
87
|
+
delete moduleLoadPromises[key];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* 初始化 ModuleRegistry
|
|
92
|
+
*
|
|
93
|
+
* @param manifest - Bundle manifest,包含所有模块的元数据
|
|
94
|
+
*
|
|
95
|
+
* 注意:
|
|
96
|
+
* - rootPath 不需要传入,Native 层会自己处理路径查找
|
|
97
|
+
* - manifest 只在初始化时使用,初始化后会被丢弃(信息已提取到 moduleMeta)
|
|
98
|
+
*/
|
|
99
|
+
function init(options: { manifest: BundleManifest }): void {
|
|
100
|
+
// 每次 init 先 reset,避免残留状态
|
|
101
|
+
resetInternalState();
|
|
102
|
+
|
|
103
|
+
const { manifest } = options;
|
|
104
|
+
|
|
105
|
+
// 校验
|
|
106
|
+
if (!manifest) {
|
|
107
|
+
throw new Error('ModuleRegistry init failed: missing manifest');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// 解析 meta
|
|
111
|
+
manifest.modules.forEach((m) => {
|
|
112
|
+
const id = m.id;
|
|
113
|
+
moduleMeta[id] = {
|
|
114
|
+
file: m.file,
|
|
115
|
+
version: m.version,
|
|
116
|
+
dependencies: m.dependencies || [],
|
|
117
|
+
lazy: !!m.lazy,
|
|
118
|
+
};
|
|
119
|
+
moduleState[id] = 'idle';
|
|
120
|
+
moduleExports[id] = undefined;
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// 检测循环依赖
|
|
124
|
+
manifest.modules.forEach((m) => {
|
|
125
|
+
checkCircularDependency(m.id, new Set(), []);
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const map = {};
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* 注册模块(供模块 bundle 调用)
|
|
133
|
+
*/
|
|
134
|
+
function registerModule(moduleId: string, exportsObj: any): void {
|
|
135
|
+
if (!moduleMeta[moduleId]) {
|
|
136
|
+
console.warn(`registerModule: unknown moduleId=${moduleId}`);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
map[moduleId] = 1;
|
|
141
|
+
|
|
142
|
+
moduleExports[moduleId] = exportsObj;
|
|
143
|
+
moduleState[moduleId] = 'loaded';
|
|
144
|
+
|
|
145
|
+
const pending = moduleLoadPromises[moduleId];
|
|
146
|
+
if (pending?.resolveRegister) {
|
|
147
|
+
pending.resolveRegister();
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* 获取模块状态
|
|
153
|
+
*/
|
|
154
|
+
function getModuleState(moduleId: string): ModuleState {
|
|
155
|
+
return moduleState[moduleId] || 'idle';
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* 获取模块导出对象
|
|
160
|
+
*/
|
|
161
|
+
function getModuleExports(moduleId: string): any {
|
|
162
|
+
if (moduleState[moduleId] !== 'loaded') {
|
|
163
|
+
throw new Error(`Module ${moduleId} not loaded yet`);
|
|
164
|
+
}
|
|
165
|
+
return moduleExports[moduleId];
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* 加载模块(核心逻辑)
|
|
170
|
+
*/
|
|
171
|
+
async function loadModule(moduleId: string): Promise<void> {
|
|
172
|
+
// 1. 非法模块
|
|
173
|
+
if (!moduleMeta[moduleId]) {
|
|
174
|
+
throw new Error(`Unknown module: ${moduleId}`);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// 2. 已加载完成
|
|
178
|
+
if (moduleState[moduleId] === 'loaded') {
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// 3. 并发加载处理
|
|
183
|
+
if (moduleLoadPromises[moduleId]) {
|
|
184
|
+
return moduleLoadPromises[moduleId].promise;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// 4. 创建一个加载 Promise(缓存)
|
|
188
|
+
let resolveRegister!: () => void;
|
|
189
|
+
let rejectRegister!: (error: Error) => void;
|
|
190
|
+
|
|
191
|
+
const registerPromise = new Promise<void>((resolve, reject) => {
|
|
192
|
+
resolveRegister = resolve;
|
|
193
|
+
rejectRegister = reject;
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
const loadPromise = (async () => {
|
|
197
|
+
try {
|
|
198
|
+
// Step 1:递归加载依赖
|
|
199
|
+
const deps = moduleMeta[moduleId].dependencies;
|
|
200
|
+
for (const depId of deps) {
|
|
201
|
+
await loadModule(depId);
|
|
202
|
+
if (moduleState[depId] !== 'loaded') {
|
|
203
|
+
throw new Error(
|
|
204
|
+
`Dependency ${depId} failed for module ${moduleId}. State: ${moduleState[depId]}`
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Step 2:设置加载状态
|
|
210
|
+
moduleState[moduleId] = 'loading';
|
|
211
|
+
|
|
212
|
+
// Step 3:选择 ModuleLoader
|
|
213
|
+
let loader: any = injectedModuleLoader;
|
|
214
|
+
|
|
215
|
+
if (!loader) {
|
|
216
|
+
// 开发环境:优先使用 ModuleLoaderMock(HTTP 模式)
|
|
217
|
+
if (__DEV__) {
|
|
218
|
+
loader = ModuleLoaderMock;
|
|
219
|
+
} else {
|
|
220
|
+
// 生产环境:使用 Native CodePush 模块
|
|
221
|
+
if (NativeModules?.ModuleLoader.loadBusinessBundle) {
|
|
222
|
+
loader = NativeModules.ModuleLoader;
|
|
223
|
+
} else {
|
|
224
|
+
throw new Error(
|
|
225
|
+
'ModuleLoader not available: Native CodePush module not found in production'
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// 传入 bundleId(即 moduleId)和 bundlePath(从 manifest 中获取)
|
|
232
|
+
const bundlePath = moduleMeta[moduleId].file;
|
|
233
|
+
const result = await loader.loadBusinessBundle(moduleId, bundlePath);
|
|
234
|
+
if (!result?.success) {
|
|
235
|
+
moduleState[moduleId] = 'failed';
|
|
236
|
+
const err = new Error(result?.errorMessage || 'LOAD_FAILED');
|
|
237
|
+
rejectRegister(err); // 提前结束 registerPromise
|
|
238
|
+
throw err;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Step 4:等待 registerModule 执行
|
|
242
|
+
await Promise.race([
|
|
243
|
+
registerPromise,
|
|
244
|
+
new Promise<void>((_, reject) =>
|
|
245
|
+
setTimeout(
|
|
246
|
+
() => reject(new Error('REGISTER_TIMEOUT')),
|
|
247
|
+
REGISTER_TIMEOUT_MS
|
|
248
|
+
)
|
|
249
|
+
),
|
|
250
|
+
]);
|
|
251
|
+
|
|
252
|
+
// 验证模块确实已注册
|
|
253
|
+
// 注意:TypeScript 可能无法正确推断状态,但运行时检查是必要的
|
|
254
|
+
const finalState = moduleState[moduleId] as ModuleState;
|
|
255
|
+
if (finalState !== 'loaded') {
|
|
256
|
+
throw new Error(`registerModule not called: ${moduleId}, state: ${finalState}`);
|
|
257
|
+
}
|
|
258
|
+
} catch (err) {
|
|
259
|
+
moduleState[moduleId] = 'failed';
|
|
260
|
+
const error =
|
|
261
|
+
err instanceof Error ? err : new Error(String(err));
|
|
262
|
+
console.error(`[ModuleRegistry] load failed: ${moduleId}`, error);
|
|
263
|
+
throw error;
|
|
264
|
+
} finally {
|
|
265
|
+
delete moduleLoadPromises[moduleId];
|
|
266
|
+
}
|
|
267
|
+
})();
|
|
268
|
+
|
|
269
|
+
moduleLoadPromises[moduleId] = {
|
|
270
|
+
promise: loadPromise,
|
|
271
|
+
resolveRegister,
|
|
272
|
+
rejectRegister,
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
return loadPromise;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export const ModuleRegistry = {
|
|
279
|
+
init,
|
|
280
|
+
loadModule,
|
|
281
|
+
registerModule,
|
|
282
|
+
getModuleState,
|
|
283
|
+
getModuleExports,
|
|
284
|
+
setModuleLoader,
|
|
285
|
+
map,
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
// 将 ModuleRegistry 挂载到全局作用域,以便模块 bundle 可以访问
|
|
289
|
+
// 这在开发环境中特别重要,因为模块 bundle 和主 bundle 共享同一个 JS 上下文
|
|
290
|
+
if (typeof globalThis !== 'undefined') {
|
|
291
|
+
globalThis.__ModuleRegistry = ModuleRegistry;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
export type { BundleManifest, ModuleState };
|
|
295
|
+
|
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
# React Native 多 Bundle 系统
|
|
2
|
+
|
|
3
|
+
多 Bundle 系统提供了模块化的代码分割方案,支持将 React Native 应用拆分为多个独立的 bundle,实现按需加载和独立更新。
|
|
4
|
+
|
|
5
|
+
## 快速接入
|
|
6
|
+
|
|
7
|
+
### 1. 初始化多 Bundle 系统
|
|
8
|
+
|
|
9
|
+
在应用入口(通常是 `App.tsx`)中调用 `initMultiBundle`:
|
|
10
|
+
|
|
11
|
+
```typescript
|
|
12
|
+
import { initMultiBundle } from './src/multi-bundle/init';
|
|
13
|
+
import { LocalBundleManager } from './src/multi-bundle/LocalBundleManager';
|
|
14
|
+
import { Platform } from 'react-native';
|
|
15
|
+
|
|
16
|
+
function App() {
|
|
17
|
+
const [ready, setReady] = useState(false);
|
|
18
|
+
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
async function bootstrap() {
|
|
21
|
+
const result = await initMultiBundle({
|
|
22
|
+
// 模块路径规则(glob 模式)
|
|
23
|
+
modulePaths: ['src/modules/**'],
|
|
24
|
+
|
|
25
|
+
// 共享依赖路径(这些代码会被打包到主 bundle)
|
|
26
|
+
sharedDependencies: ['src/multi-bundle/**', 'src/navigation/**'],
|
|
27
|
+
|
|
28
|
+
// Manifest 提供者(可选,默认使用 LocalBundleManager)
|
|
29
|
+
manifestProvider: async () => {
|
|
30
|
+
return await LocalBundleManager.getCurrentBundleManifest();
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
// 预加载模块列表(可选)
|
|
34
|
+
preloadModules: ['settings'],
|
|
35
|
+
|
|
36
|
+
// 开发服务器配置(可选)
|
|
37
|
+
devServer: {
|
|
38
|
+
host: Platform.OS === 'android' ? '10.0.2.2' : 'localhost',
|
|
39
|
+
port: 8081,
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
// 错误处理回调(可选)
|
|
43
|
+
onError: (error) => {
|
|
44
|
+
console.error('MultiBundle error:', error);
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
if (result.success) {
|
|
49
|
+
setReady(true);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
bootstrap();
|
|
54
|
+
}, []);
|
|
55
|
+
|
|
56
|
+
if (!ready) {
|
|
57
|
+
return <LoadingScreen />;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ... 其余代码
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### 2. 使用模块路由
|
|
65
|
+
|
|
66
|
+
#### 方式一:使用 `createModuleRouteLoader` + `getComponent`(推荐,React Navigation v7+)
|
|
67
|
+
|
|
68
|
+
这是推荐的方式,支持真正的懒加载,组件只在导航到该路由时才被创建:
|
|
69
|
+
|
|
70
|
+
```typescript
|
|
71
|
+
import { createModuleRouteLoader } from './src/multi-bundle/createModuleRouteLoader';
|
|
72
|
+
|
|
73
|
+
// 创建路由加载器函数
|
|
74
|
+
export const createHomeScreen = createModuleRouteLoader('home', 'Home');
|
|
75
|
+
export const createDetailScreen = createModuleRouteLoader('details', 'Details');
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
在 React Navigation 中使用:
|
|
79
|
+
|
|
80
|
+
```tsx
|
|
81
|
+
import { createNativeStackNavigator } from '@react-navigation/native-stack';
|
|
82
|
+
import { createHomeScreen, createDetailScreen } from './src/navigation/moduleRoutes';
|
|
83
|
+
|
|
84
|
+
const Stack = createNativeStackNavigator();
|
|
85
|
+
|
|
86
|
+
function App() {
|
|
87
|
+
return (
|
|
88
|
+
<Stack.Navigator>
|
|
89
|
+
<Stack.Screen name="Home" getComponent={createHomeScreen} />
|
|
90
|
+
<Stack.Screen name="Details" getComponent={createDetailScreen} />
|
|
91
|
+
</Stack.Navigator>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
**优势**:
|
|
97
|
+
- 真正的懒加载:组件只在导航到该路由时才创建
|
|
98
|
+
- 减少初始内存占用
|
|
99
|
+
- 符合 React Navigation 最佳实践
|
|
100
|
+
|
|
101
|
+
#### 方式二:使用路由注册系统
|
|
102
|
+
|
|
103
|
+
```typescript
|
|
104
|
+
import { registerModuleRoute, getModuleRoute } from './src/multi-bundle/routeRegistry';
|
|
105
|
+
|
|
106
|
+
// 注册路由
|
|
107
|
+
registerModuleRoute('home', 'Home');
|
|
108
|
+
registerModuleRoute('details', 'Details');
|
|
109
|
+
|
|
110
|
+
// 获取路由组件
|
|
111
|
+
const HomeScreen = getModuleRoute('home', 'Home');
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### 3. 配置 Metro
|
|
115
|
+
|
|
116
|
+
确保 `metro.config.js` 已正确配置。系统会自动从环境变量读取配置,或使用默认值。
|
|
117
|
+
|
|
118
|
+
如果需要自定义配置,可以通过环境变量传递:
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
RN_MULTI_BUNDLE_MODULE_PATHS='["src/modules/**"]' \
|
|
122
|
+
RN_MULTI_BUNDLE_SHARED_DEPS='["src/multi-bundle/**","src/navigation/**"]' \
|
|
123
|
+
npx react-native start
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## 配置选项
|
|
127
|
+
|
|
128
|
+
### MultiBundleConfig
|
|
129
|
+
|
|
130
|
+
| 选项 | 类型 | 必填 | 默认值 | 说明 |
|
|
131
|
+
|------|------|------|--------|------|
|
|
132
|
+
| `modulePaths` | `string[]` | 否 | `['src/modules/**']` | 模块路径规则(glob 模式) |
|
|
133
|
+
| `sharedDependencies` | `string[]` | 否 | `['src/multi-bundle/**', 'src/navigation/**']` | 共享依赖路径 |
|
|
134
|
+
| `manifestProvider` | `ManifestProvider` | 否 | 使用 `LocalBundleManager` | Manifest 获取方式 |
|
|
135
|
+
| `moduleLoader` | `ModuleLoader` | 否 | 自动选择 | 自定义 ModuleLoader |
|
|
136
|
+
| `preloadModules` | `string[]` | 否 | `[]` | 预加载模块列表 |
|
|
137
|
+
| `devServer` | `DevServerConfig` | 否 | 根据平台自动选择 | 开发服务器配置 |
|
|
138
|
+
| `onError` | `(error: Error) => void` | 否 | - | 错误处理回调 |
|
|
139
|
+
|
|
140
|
+
### ManifestProvider
|
|
141
|
+
|
|
142
|
+
支持两种方式提供 manifest:
|
|
143
|
+
|
|
144
|
+
1. **函数方式**(推荐):
|
|
145
|
+
```typescript
|
|
146
|
+
manifestProvider: async () => {
|
|
147
|
+
// 自定义获取逻辑
|
|
148
|
+
return await fetchManifestFromCustomSource();
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
2. **URL 方式**:
|
|
153
|
+
```typescript
|
|
154
|
+
manifestProvider: 'http://localhost:8081/bundle-manifest.json'
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### DevServerConfig
|
|
158
|
+
|
|
159
|
+
```typescript
|
|
160
|
+
{
|
|
161
|
+
host: string; // 服务器地址
|
|
162
|
+
port: number; // 端口号
|
|
163
|
+
protocol?: 'http' | 'https'; // 协议(默认 'http')
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
## 模块开发规范
|
|
168
|
+
|
|
169
|
+
### 模块入口文件
|
|
170
|
+
|
|
171
|
+
每个模块需要在入口文件中注册:
|
|
172
|
+
|
|
173
|
+
```typescript
|
|
174
|
+
// src/modules/Home/index.ts
|
|
175
|
+
import HomeScreen from './HomeScreen';
|
|
176
|
+
|
|
177
|
+
const exports = {
|
|
178
|
+
routes: {
|
|
179
|
+
Home: HomeScreen,
|
|
180
|
+
},
|
|
181
|
+
components: {
|
|
182
|
+
HomeScreen,
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
// 注册模块
|
|
187
|
+
const ModuleRegistry = globalThis.__ModuleRegistry;
|
|
188
|
+
if (ModuleRegistry) {
|
|
189
|
+
ModuleRegistry.registerModule('home', exports);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export default exports;
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### 模块导出结构
|
|
196
|
+
|
|
197
|
+
建议使用统一的导出结构:
|
|
198
|
+
|
|
199
|
+
```typescript
|
|
200
|
+
{
|
|
201
|
+
routes: {
|
|
202
|
+
[routeKey]: React.ComponentType;
|
|
203
|
+
};
|
|
204
|
+
components: {
|
|
205
|
+
[componentName]: React.ComponentType;
|
|
206
|
+
};
|
|
207
|
+
services?: {
|
|
208
|
+
[serviceName]: any;
|
|
209
|
+
};
|
|
210
|
+
constants?: {
|
|
211
|
+
[constantName]: any;
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
## API 参考
|
|
217
|
+
|
|
218
|
+
### initMultiBundle
|
|
219
|
+
|
|
220
|
+
初始化多 Bundle 系统。
|
|
221
|
+
|
|
222
|
+
```typescript
|
|
223
|
+
function initMultiBundle(config?: MultiBundleConfig): Promise<InitResult>
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
**返回值**:
|
|
227
|
+
```typescript
|
|
228
|
+
{
|
|
229
|
+
success: boolean;
|
|
230
|
+
error?: Error;
|
|
231
|
+
manifest?: BundleManifest;
|
|
232
|
+
}
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
### createModuleRouteLoader
|
|
236
|
+
|
|
237
|
+
创建模块路由加载器函数(推荐,用于 React Navigation `getComponent` API)。
|
|
238
|
+
|
|
239
|
+
```typescript
|
|
240
|
+
function createModuleRouteLoader(
|
|
241
|
+
moduleId: string,
|
|
242
|
+
routeKey: string
|
|
243
|
+
): () => React.ComponentType<any>
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
**使用示例**:
|
|
247
|
+
```typescript
|
|
248
|
+
const createHomeScreen = createModuleRouteLoader('home', 'Home');
|
|
249
|
+
|
|
250
|
+
// 在 React Navigation 中使用
|
|
251
|
+
<Stack.Screen name="Home" getComponent={createHomeScreen} />
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
### createModuleLoader
|
|
255
|
+
|
|
256
|
+
创建通用的模块加载器函数(用于自定义组件提取逻辑)。
|
|
257
|
+
|
|
258
|
+
```typescript
|
|
259
|
+
function createModuleLoader(
|
|
260
|
+
moduleId: string,
|
|
261
|
+
pickComponent: (exports: any) => React.ComponentType<any>,
|
|
262
|
+
options?: CreateModuleLoaderOptions
|
|
263
|
+
): () => React.ComponentType<any>
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
**使用示例**:
|
|
267
|
+
```typescript
|
|
268
|
+
const createCustomScreen = createModuleLoader(
|
|
269
|
+
'my-module',
|
|
270
|
+
(exports) => exports.components.MyComponent
|
|
271
|
+
);
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
### registerModuleRoute
|
|
276
|
+
|
|
277
|
+
注册模块路由。
|
|
278
|
+
|
|
279
|
+
```typescript
|
|
280
|
+
function registerModuleRoute(
|
|
281
|
+
moduleId: string,
|
|
282
|
+
routeKey: string,
|
|
283
|
+
component?: React.ComponentType<any>
|
|
284
|
+
): void
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
### getModuleRoute
|
|
288
|
+
|
|
289
|
+
获取已注册的路由组件。
|
|
290
|
+
|
|
291
|
+
```typescript
|
|
292
|
+
function getModuleRoute(
|
|
293
|
+
moduleId: string,
|
|
294
|
+
routeKey: string
|
|
295
|
+
): React.ComponentType<any> | undefined
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
### preloadModule
|
|
299
|
+
|
|
300
|
+
预加载模块。
|
|
301
|
+
|
|
302
|
+
```typescript
|
|
303
|
+
function preloadModule(moduleId: string): Promise<void>
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
## 最佳实践
|
|
307
|
+
|
|
308
|
+
1. **统一初始化**:在应用入口统一调用 `initMultiBundle`,避免分散的初始化逻辑。
|
|
309
|
+
|
|
310
|
+
2. **配置集中管理**:将多 Bundle 配置集中管理,便于维护和修改。
|
|
311
|
+
|
|
312
|
+
3. **错误处理**:始终提供 `onError` 回调,妥善处理初始化失败的情况。
|
|
313
|
+
|
|
314
|
+
4. **预加载策略**:只预加载关键模块,避免影响启动性能。
|
|
315
|
+
|
|
316
|
+
5. **模块设计**:保持模块独立性,避免循环依赖。
|
|
317
|
+
|
|
318
|
+
## 故障排查
|
|
319
|
+
|
|
320
|
+
### 模块加载失败
|
|
321
|
+
|
|
322
|
+
- 检查 manifest 是否正确配置
|
|
323
|
+
- 确认模块 ID 与 manifest 中的 ID 一致
|
|
324
|
+
- 查看控制台错误信息
|
|
325
|
+
|
|
326
|
+
### Metro 构建问题
|
|
327
|
+
|
|
328
|
+
- 确认 `metro.config.js` 配置正确
|
|
329
|
+
- 检查模块路径规则是否匹配实际路径
|
|
330
|
+
- 清除 Metro 缓存:`npx react-native start --reset-cache`
|
|
331
|
+
|
|
332
|
+
### 开发环境问题
|
|
333
|
+
|
|
334
|
+
- 确认开发服务器正在运行
|
|
335
|
+
- 检查 `devServer` 配置是否正确
|
|
336
|
+
- 验证 manifest URL 是否可访问
|
|
337
|
+
|
|
338
|
+
## 更多信息
|
|
339
|
+
|
|
340
|
+
- [模块 Bundle 开发规范](../docs/模块%20Bundle%20开发规范.md)
|
|
341
|
+
- [使用指南](../docs/使用指南.md)
|
|
342
|
+
- [多 Bundle 架构设计](../docs/React%20Native%20多%20bundle%20技术方案.md)
|
|
343
|
+
|