@bm-fe/react-native-multi-bundle 1.0.0-beta.0 → 1.0.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/INTEGRATION.md
CHANGED
|
@@ -326,6 +326,53 @@ export const createHomeScreen = createModuleRouteLoader('home', 'Home');
|
|
|
326
326
|
<Stack.Screen name="Home" getComponent={createHomeScreen} />
|
|
327
327
|
```
|
|
328
328
|
|
|
329
|
+
**路由 Props 传递说明:**
|
|
330
|
+
|
|
331
|
+
`createModuleRouteLoader` 会自动将 React Navigation 的路由 props 传递给模块组件,包括:
|
|
332
|
+
|
|
333
|
+
- `navigation`:导航对象,用于页面跳转
|
|
334
|
+
- `route`:路由对象,包含路由参数和配置
|
|
335
|
+
|
|
336
|
+
模块组件可以直接使用这些 props:
|
|
337
|
+
|
|
338
|
+
```typescript
|
|
339
|
+
// 在模块组件中(如 HomeScreen.tsx)
|
|
340
|
+
import React from 'react';
|
|
341
|
+
import { View, Text } from 'react-native';
|
|
342
|
+
import { NativeStackScreenProps } from '@react-navigation/native-stack';
|
|
343
|
+
|
|
344
|
+
type Props = NativeStackScreenProps<RootStackParamList, 'Home'>;
|
|
345
|
+
|
|
346
|
+
const HomeScreen = ({ navigation, route }: Props) => {
|
|
347
|
+
// 可以使用 navigation 进行页面跳转
|
|
348
|
+
const handleNavigate = () => {
|
|
349
|
+
navigation.navigate('Details', { id: '123' });
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
// 可以使用 route.params 获取路由参数
|
|
353
|
+
const params = route.params;
|
|
354
|
+
|
|
355
|
+
return (
|
|
356
|
+
<View>
|
|
357
|
+
<Text>Home Screen</Text>
|
|
358
|
+
{/* ... */}
|
|
359
|
+
</View>
|
|
360
|
+
);
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
export default HomeScreen;
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
如果需要传递额外的 props,可以通过 React Navigation 的 `initialParams` 或 `getInitialProps`:
|
|
367
|
+
|
|
368
|
+
```typescript
|
|
369
|
+
<Stack.Screen
|
|
370
|
+
name="Home"
|
|
371
|
+
getComponent={createHomeScreen}
|
|
372
|
+
initialParams={{ customProp: 'value' }}
|
|
373
|
+
/>
|
|
374
|
+
```
|
|
375
|
+
|
|
329
376
|
### 方式二:使用路由注册系统
|
|
330
377
|
|
|
331
378
|
```typescript
|
|
@@ -364,6 +411,18 @@ const HomeScreen = getModuleRoute('home', 'Home');
|
|
|
364
411
|
- 检查 `devServer` 配置是否正确
|
|
365
412
|
- 验证 manifest URL 是否可访问
|
|
366
413
|
|
|
414
|
+
### 5. 配置文件找不到
|
|
415
|
+
|
|
416
|
+
如果执行打包命令时提示 `Config file not found`:
|
|
417
|
+
|
|
418
|
+
- **确认配置文件位置**:`multi-bundle.config.json` 应该在项目根目录(与 `package.json` 同级)
|
|
419
|
+
- **确认执行目录**:打包命令应该在项目根目录执行,或者通过 npm scripts 执行
|
|
420
|
+
- **手动指定项目根目录**:如果仍有问题,可以通过环境变量指定:
|
|
421
|
+
```bash
|
|
422
|
+
PROJECT_ROOT=/path/to/your/project node node_modules/@bm-fe/react-native-multi-bundle/scripts/build-multi-bundle.js ios
|
|
423
|
+
```
|
|
424
|
+
- **检查文件路径**:确认错误信息中的路径是否正确指向你的项目根目录
|
|
425
|
+
|
|
367
426
|
## 更多信息
|
|
368
427
|
|
|
369
428
|
- [API 参考](./src/multi-bundle/README.md)
|
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.1",
|
|
4
4
|
"description": "React Native 多 Bundle 系统 - 支持模块按需加载和独立更新",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"types": "src/index.ts",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"version:patch": "standard-version --release-as patch",
|
|
35
35
|
"version:minor": "standard-version --release-as minor",
|
|
36
36
|
"version:major": "standard-version --release-as major",
|
|
37
|
-
"version:beta": "npm version prerelease --preid=beta --no-git-tag-version",
|
|
37
|
+
"version:beta": "npm version prerelease --preid=beta --no-git-tag-version --no-scripts",
|
|
38
38
|
"publish:beta": "npm publish --tag beta",
|
|
39
39
|
"release:beta": "npm run version:beta && npm run publish:beta",
|
|
40
40
|
"release": "npm run version && npm publish"
|
|
@@ -13,17 +13,41 @@
|
|
|
13
13
|
* - --env: 'development' | 'staging' | 'production' (默认: 'production')
|
|
14
14
|
*
|
|
15
15
|
* 环境变量:
|
|
16
|
-
* - PROJECT_ROOT:
|
|
16
|
+
* - PROJECT_ROOT: 项目根目录(可选,默认从当前工作目录向上查找包含 multi-bundle.config.json 的目录)
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
19
|
const fs = require('fs');
|
|
20
20
|
const path = require('path');
|
|
21
21
|
const { execSync } = require('child_process');
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
/**
|
|
24
|
+
* 查找项目根目录
|
|
25
|
+
* 从当前工作目录向上查找,直到找到包含 multi-bundle.config.json 的目录
|
|
26
|
+
* 如果找不到,则使用当前工作目录(通常打包命令在项目根目录执行)
|
|
27
|
+
*/
|
|
28
|
+
function findProjectRoot() {
|
|
29
|
+
// 如果通过环境变量指定,直接使用
|
|
30
|
+
if (process.env.PROJECT_ROOT) {
|
|
31
|
+
return process.env.PROJECT_ROOT;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// 从当前工作目录开始向上查找
|
|
35
|
+
let currentDir = process.cwd();
|
|
36
|
+
const root = path.parse(currentDir).root;
|
|
37
|
+
|
|
38
|
+
while (currentDir !== root) {
|
|
39
|
+
const configFile = path.join(currentDir, 'multi-bundle.config.json');
|
|
40
|
+
if (fs.existsSync(configFile)) {
|
|
41
|
+
return currentDir;
|
|
42
|
+
}
|
|
43
|
+
currentDir = path.dirname(currentDir);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// 如果找不到配置文件,使用当前工作目录(通常打包命令在项目根目录执行)
|
|
47
|
+
return process.cwd();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const PROJECT_ROOT = findProjectRoot();
|
|
27
51
|
const CONFIG_FILE = path.join(PROJECT_ROOT, 'multi-bundle.config.json');
|
|
28
52
|
const OUTPUT_DIR = path.join(PROJECT_ROOT, 'build/bundles');
|
|
29
53
|
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import { Platform } from 'react-native';
|
|
11
11
|
import { HMRClient } from './HMRClient';
|
|
12
|
+
import { getGlobalConfig } from './config';
|
|
12
13
|
|
|
13
14
|
interface LoadResult {
|
|
14
15
|
success: boolean;
|
|
@@ -22,29 +23,117 @@ const loadedBundles = new Set<string>();
|
|
|
22
23
|
// 正在加载中的 bundle(并发复用)
|
|
23
24
|
const inflightBundles = new Map<string, Promise<LoadResult>>();
|
|
24
25
|
|
|
26
|
+
// 缓存 multi-bundle.config.json(开发环境)
|
|
27
|
+
let cachedMultiBundleConfig: any = null;
|
|
28
|
+
let configLoadPromise: Promise<any> | null = null;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* 从开发服务器获取 multi-bundle.config.json
|
|
32
|
+
*/
|
|
33
|
+
async function getMultiBundleConfig(): Promise<any> {
|
|
34
|
+
// 如果已缓存,直接返回
|
|
35
|
+
if (cachedMultiBundleConfig) {
|
|
36
|
+
return cachedMultiBundleConfig;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// 如果正在加载,等待加载完成
|
|
40
|
+
if (configLoadPromise) {
|
|
41
|
+
return configLoadPromise;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// 开始加载配置
|
|
45
|
+
configLoadPromise = (async () => {
|
|
46
|
+
try {
|
|
47
|
+
const config = getGlobalConfig();
|
|
48
|
+
const devServer = config?.devServer;
|
|
49
|
+
const host = devServer?.host || (Platform.OS === 'android' ? '10.0.2.2' : 'localhost');
|
|
50
|
+
const port = devServer?.port || 8081;
|
|
51
|
+
const protocol = devServer?.protocol || 'http';
|
|
52
|
+
|
|
53
|
+
const configUrl = `${protocol}://${host}:${port}/multi-bundle.config.json`;
|
|
54
|
+
const response = await fetch(configUrl);
|
|
55
|
+
|
|
56
|
+
if (response.ok) {
|
|
57
|
+
const configData = await response.json();
|
|
58
|
+
cachedMultiBundleConfig = configData;
|
|
59
|
+
return configData;
|
|
60
|
+
} else {
|
|
61
|
+
console.warn(
|
|
62
|
+
`[ModuleLoaderMock] Failed to fetch multi-bundle.config.json: HTTP ${response.status}`
|
|
63
|
+
);
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
} catch (error) {
|
|
67
|
+
console.warn(
|
|
68
|
+
`[ModuleLoaderMock] Failed to fetch multi-bundle.config.json: ${error}`
|
|
69
|
+
);
|
|
70
|
+
return null;
|
|
71
|
+
} finally {
|
|
72
|
+
configLoadPromise = null;
|
|
73
|
+
}
|
|
74
|
+
})();
|
|
75
|
+
|
|
76
|
+
return configLoadPromise;
|
|
77
|
+
}
|
|
78
|
+
|
|
25
79
|
/**
|
|
26
|
-
* 根据
|
|
80
|
+
* 根据 bundlePath 构建 HTTP URL(开发环境 HTTP 模式)
|
|
27
81
|
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
82
|
+
* 优先从 multi-bundle.config.json 的 entry 字段获取模块路径
|
|
83
|
+
* 如果无法获取,则使用 bundlePath 进行简单推断
|
|
84
|
+
*
|
|
85
|
+
* @param bundleId 模块 ID
|
|
86
|
+
* @param bundlePath bundle 文件路径(从 manifest 中获取,开发环境可能不使用)
|
|
30
87
|
*/
|
|
31
|
-
function
|
|
32
|
-
const
|
|
88
|
+
async function buildHttpUrlFromBundlePath(bundleId: string, bundlePath: string): Promise<string> {
|
|
89
|
+
const config = getGlobalConfig();
|
|
90
|
+
const devServer = config?.devServer;
|
|
91
|
+
const host = devServer?.host || (Platform.OS === 'android' ? '10.0.2.2' : 'localhost');
|
|
92
|
+
const port = devServer?.port || 8081;
|
|
93
|
+
const protocol = devServer?.protocol || 'http';
|
|
33
94
|
|
|
34
|
-
//
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
95
|
+
// 优先从 multi-bundle.config.json 的 entry 获取模块路径
|
|
96
|
+
let sourcePath: string | null = null;
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
const multiBundleConfig = await getMultiBundleConfig();
|
|
100
|
+
if (multiBundleConfig?.modules) {
|
|
101
|
+
const moduleConfig = multiBundleConfig.modules.find((m: any) => m.id === bundleId);
|
|
102
|
+
if (moduleConfig?.entry) {
|
|
103
|
+
sourcePath = moduleConfig.entry;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
} catch (error) {
|
|
107
|
+
console.warn(
|
|
108
|
+
`[ModuleLoaderMock] Failed to get entry from multi-bundle.config.json for module ${bundleId}: ${error}`
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// 如果无法从配置文件获取,使用 bundlePath 进行简单推断
|
|
113
|
+
if (!sourcePath) {
|
|
114
|
+
// 如果 bundlePath 已经是源码路径,直接使用
|
|
115
|
+
if (bundlePath.startsWith('src/') || bundlePath.startsWith('./src/')) {
|
|
116
|
+
sourcePath = bundlePath;
|
|
117
|
+
} else {
|
|
118
|
+
// 生产环境路径(如 bundles/home/index.bundle),提取模块名
|
|
119
|
+
const match = bundlePath.match(/(?:bundles\/|modules\/)(?:[^/]+\/)?([^/]+)\//);
|
|
120
|
+
const moduleName = match?.[1] || bundleId.charAt(0).toUpperCase() + bundleId.slice(1);
|
|
121
|
+
sourcePath = `src/modules/${moduleName}/index.bundle`;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// 确保路径以 .bundle 结尾
|
|
126
|
+
if (!sourcePath.endsWith('.bundle')) {
|
|
127
|
+
sourcePath = sourcePath.replace(/\.(ts|tsx|js|jsx)$/, '.bundle') || sourcePath + '/index.bundle';
|
|
128
|
+
}
|
|
38
129
|
|
|
39
130
|
// 构建 Metro 请求 URL
|
|
40
|
-
|
|
41
|
-
// 注意:使用 .bundle 后缀,Metro 会识别并构建对应的 .ts/.tsx 文件
|
|
42
|
-
return `http://${host}:8081/src/modules/${moduleName}/index.bundle` +
|
|
131
|
+
return `${protocol}://${host}:${port}/${sourcePath}` +
|
|
43
132
|
`?platform=${Platform.OS}` +
|
|
44
133
|
`&dev=true` +
|
|
45
134
|
`&minify=false` +
|
|
46
|
-
`&modulesOnly=true` +
|
|
47
|
-
`&runModule=true`;
|
|
135
|
+
`&modulesOnly=true` +
|
|
136
|
+
`&runModule=true`;
|
|
48
137
|
}
|
|
49
138
|
|
|
50
139
|
/**
|
|
@@ -74,8 +163,8 @@ async function loadBusinessBundle(bundleId: string, bundlePath: string): Promise
|
|
|
74
163
|
};
|
|
75
164
|
}
|
|
76
165
|
|
|
77
|
-
// 开发环境:根据
|
|
78
|
-
const httpUrl =
|
|
166
|
+
// 开发环境:根据 bundlePath 动态构建 HTTP URL(优先从 multi-bundle.config.json 获取 entry)
|
|
167
|
+
const httpUrl = await buildHttpUrlFromBundlePath(bundleId, bundlePath);
|
|
79
168
|
|
|
80
169
|
// 已经加载过的 bundle,不再重复执行
|
|
81
170
|
if (loadedBundles.has(httpUrl)) {
|
|
@@ -166,4 +255,3 @@ export const ModuleLoader = {
|
|
|
166
255
|
};
|
|
167
256
|
|
|
168
257
|
export type { LoadResult };
|
|
169
|
-
|
package/src/multi-bundle/init.ts
CHANGED
|
@@ -18,6 +18,10 @@ export interface InitResult {
|
|
|
18
18
|
manifest?: BundleManifest;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
// 初始化状态标记
|
|
22
|
+
let isInitialized = false;
|
|
23
|
+
let initPromise: Promise<InitResult> | null = null;
|
|
24
|
+
|
|
21
25
|
/**
|
|
22
26
|
* 从 manifestProvider 获取 manifest
|
|
23
27
|
*/
|
|
@@ -53,6 +57,7 @@ async function getManifestFromProvider(
|
|
|
53
57
|
* 初始化多 Bundle 系统
|
|
54
58
|
*
|
|
55
59
|
* @param config 配置对象
|
|
60
|
+
* @param forceReinit 是否强制重新初始化(默认 false,已初始化时直接返回成功结果)
|
|
56
61
|
* @returns 初始化结果
|
|
57
62
|
*
|
|
58
63
|
* @example
|
|
@@ -64,68 +69,106 @@ async function getManifestFromProvider(
|
|
|
64
69
|
* });
|
|
65
70
|
* ```
|
|
66
71
|
*/
|
|
67
|
-
export async function initMultiBundle(
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
72
|
+
export async function initMultiBundle(
|
|
73
|
+
config: MultiBundleConfig = {},
|
|
74
|
+
forceReinit: boolean = false
|
|
75
|
+
): Promise<InitResult> {
|
|
76
|
+
// 如果已经初始化且不强制重新初始化,直接返回成功
|
|
77
|
+
if (isInitialized && !forceReinit) {
|
|
78
|
+
return {
|
|
79
|
+
success: true,
|
|
80
|
+
manifest: undefined, // 不返回 manifest,因为已经初始化过了
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// 如果正在初始化,等待正在进行的初始化完成
|
|
85
|
+
if (initPromise && !forceReinit) {
|
|
86
|
+
return initPromise;
|
|
87
|
+
}
|
|
82
88
|
|
|
83
|
-
|
|
84
|
-
|
|
89
|
+
// 开始新的初始化
|
|
90
|
+
initPromise = (async () => {
|
|
85
91
|
try {
|
|
86
|
-
|
|
92
|
+
// 1. 合并配置
|
|
93
|
+
const mergedConfig = mergeConfig(config);
|
|
94
|
+
|
|
95
|
+
// 2. 设置全局配置(供 Metro 配置等使用)
|
|
96
|
+
setGlobalConfig(mergedConfig);
|
|
97
|
+
|
|
98
|
+
// 2.5 初始化 HMR Client (开发环境)
|
|
99
|
+
if (__DEV__ && mergedConfig.devServer) {
|
|
100
|
+
HMRClient.getInstance().setup(
|
|
101
|
+
mergedConfig.devServer.host,
|
|
102
|
+
mergedConfig.devServer.port
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// 3. 获取 manifest
|
|
107
|
+
let manifest: BundleManifest;
|
|
108
|
+
try {
|
|
109
|
+
manifest = await getManifestFromProvider(mergedConfig.manifestProvider);
|
|
110
|
+
} catch (error) {
|
|
111
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
112
|
+
console.error('[initMultiBundle] Failed to get manifest:', err);
|
|
113
|
+
config.onError?.(err);
|
|
114
|
+
return {
|
|
115
|
+
success: false,
|
|
116
|
+
error: err,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// 4. 设置自定义 ModuleLoader(如果提供)
|
|
121
|
+
if (mergedConfig.moduleLoader) {
|
|
122
|
+
ModuleRegistry.setModuleLoader(mergedConfig.moduleLoader);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// 5. 初始化 ModuleRegistry
|
|
126
|
+
ModuleRegistry.init({ manifest });
|
|
127
|
+
|
|
128
|
+
// 6. 预加载模块
|
|
129
|
+
if (mergedConfig.preloadModules && mergedConfig.preloadModules.length > 0) {
|
|
130
|
+
const preloadPromises = mergedConfig.preloadModules.map((moduleId) =>
|
|
131
|
+
preloadModule(moduleId).catch((error) => {
|
|
132
|
+
// 预加载失败不影响初始化,只记录日志
|
|
133
|
+
console.warn(`[initMultiBundle] Preload module ${moduleId} failed:`, error);
|
|
134
|
+
config.onError?.(error instanceof Error ? error : new Error(String(error)));
|
|
135
|
+
})
|
|
136
|
+
);
|
|
137
|
+
await Promise.allSettled(preloadPromises);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
isInitialized = true;
|
|
141
|
+
return {
|
|
142
|
+
success: true,
|
|
143
|
+
manifest,
|
|
144
|
+
};
|
|
87
145
|
} catch (error) {
|
|
88
146
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
89
|
-
console.error('[initMultiBundle]
|
|
147
|
+
console.error('[initMultiBundle] Initialization failed:', err);
|
|
90
148
|
config.onError?.(err);
|
|
149
|
+
// 初始化失败时,允许下次重试
|
|
150
|
+
isInitialized = false;
|
|
151
|
+
initPromise = null;
|
|
91
152
|
return {
|
|
92
153
|
success: false,
|
|
93
154
|
error: err,
|
|
94
155
|
};
|
|
156
|
+
} finally {
|
|
157
|
+
// 如果强制重新初始化,清除 promise(允许下次调用)
|
|
158
|
+
if (forceReinit) {
|
|
159
|
+
initPromise = null;
|
|
160
|
+
}
|
|
95
161
|
}
|
|
162
|
+
})();
|
|
96
163
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
ModuleRegistry.setModuleLoader(mergedConfig.moduleLoader);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// 5. 初始化 ModuleRegistry
|
|
103
|
-
ModuleRegistry.init({ manifest });
|
|
104
|
-
|
|
105
|
-
// 6. 预加载模块
|
|
106
|
-
if (mergedConfig.preloadModules && mergedConfig.preloadModules.length > 0) {
|
|
107
|
-
const preloadPromises = mergedConfig.preloadModules.map((moduleId) =>
|
|
108
|
-
preloadModule(moduleId).catch((error) => {
|
|
109
|
-
// 预加载失败不影响初始化,只记录日志
|
|
110
|
-
console.warn(`[initMultiBundle] Preload module ${moduleId} failed:`, error);
|
|
111
|
-
config.onError?.(error instanceof Error ? error : new Error(String(error)));
|
|
112
|
-
})
|
|
113
|
-
);
|
|
114
|
-
await Promise.allSettled(preloadPromises);
|
|
115
|
-
}
|
|
164
|
+
return initPromise;
|
|
165
|
+
}
|
|
116
166
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
console.error('[initMultiBundle] Initialization failed:', err);
|
|
124
|
-
config.onError?.(err);
|
|
125
|
-
return {
|
|
126
|
-
success: false,
|
|
127
|
-
error: err,
|
|
128
|
-
};
|
|
129
|
-
}
|
|
167
|
+
/**
|
|
168
|
+
* 重置初始化状态(用于测试或特殊场景)
|
|
169
|
+
*/
|
|
170
|
+
export function resetInitState(): void {
|
|
171
|
+
isInitialized = false;
|
|
172
|
+
initPromise = null;
|
|
130
173
|
}
|
|
131
174
|
|
|
@@ -37,9 +37,161 @@ const sharedDependencies = multiBundleConfig.sharedDependencies || [
|
|
|
37
37
|
'src/navigation/**'
|
|
38
38
|
];
|
|
39
39
|
|
|
40
|
-
//
|
|
41
|
-
|
|
40
|
+
// ⚠️ 注意:这是"请求级别"的标记,由 middleware 动态设置
|
|
41
|
+
let isDevModuleRequest = false;
|
|
42
42
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
43
|
+
/**
|
|
44
|
+
* 加载 multi-bundle.config.json 配置
|
|
45
|
+
*/
|
|
46
|
+
function loadMultiBundleConfig() {
|
|
47
|
+
try {
|
|
48
|
+
if (fs.existsSync(MULTI_BUNDLE_CONFIG_FILE)) {
|
|
49
|
+
const configContent = fs.readFileSync(MULTI_BUNDLE_CONFIG_FILE, 'utf-8');
|
|
50
|
+
return JSON.parse(configContent);
|
|
51
|
+
}
|
|
52
|
+
} catch (error) {
|
|
53
|
+
console.warn('[Metro] Failed to load multi-bundle.config.json:', error);
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* 获取主 bundle 文件名(根据平台)
|
|
60
|
+
*/
|
|
61
|
+
function getMainBundleFileName(platform) {
|
|
62
|
+
if (platform === 'ios') {
|
|
63
|
+
return 'main.jsbundle';
|
|
64
|
+
} else if (platform === 'android') {
|
|
65
|
+
return 'index.android.bundle';
|
|
66
|
+
}
|
|
67
|
+
throw new Error(`Unsupported platform: ${platform}`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* 获取模块 bundle 文件名(根据平台和模块 ID)
|
|
72
|
+
*/
|
|
73
|
+
function getModuleBundleFileName(moduleId, platform) {
|
|
74
|
+
if (platform === 'ios') {
|
|
75
|
+
return `${moduleId}.jsbundle`;
|
|
76
|
+
} else if (platform === 'android') {
|
|
77
|
+
return `${moduleId}.bundle`;
|
|
78
|
+
}
|
|
79
|
+
throw new Error(`Unsupported platform: ${platform}`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* 从配置生成 manifest
|
|
84
|
+
*/
|
|
85
|
+
function generateManifestFromConfig(config, platform) {
|
|
86
|
+
if (!config || !config.modules) return null;
|
|
87
|
+
|
|
88
|
+
const mainFileName = getMainBundleFileName(platform);
|
|
89
|
+
const manifest = {
|
|
90
|
+
formatVersion: config.formatVersion || 1,
|
|
91
|
+
main: {
|
|
92
|
+
file: mainFileName,
|
|
93
|
+
version: config.main?.version || '1.0.0',
|
|
94
|
+
},
|
|
95
|
+
modules: (config.modules || []).map((module) => ({
|
|
96
|
+
id: module.id,
|
|
97
|
+
file: `modules/${getModuleBundleFileName(module.id, platform)}`,
|
|
98
|
+
version: module.version || '1.0.0',
|
|
99
|
+
dependencies: module.dependencies || [],
|
|
100
|
+
lazy: module.lazy !== false,
|
|
101
|
+
})),
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
return manifest;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const config = {
|
|
108
|
+
projectRoot: path.resolve(__dirname),
|
|
109
|
+
|
|
110
|
+
watcher: {
|
|
111
|
+
healthCheck: { enabled: true },
|
|
112
|
+
},
|
|
113
|
+
|
|
114
|
+
server: {
|
|
115
|
+
enhanceMiddleware: (middleware) => {
|
|
116
|
+
return (req, res, next) => {
|
|
117
|
+
// ⭐ 处理 bundle-manifest.json 请求
|
|
118
|
+
// 基于 multi-bundle.config.json 动态生成 manifest
|
|
119
|
+
if (req.url && req.url.startsWith('/bundle-manifest.json')) {
|
|
120
|
+
// 从查询参数中提取 platform,默认为 android
|
|
121
|
+
const urlParts = req.url.split('?');
|
|
122
|
+
let platform = 'android';
|
|
123
|
+
if (urlParts.length > 1) {
|
|
124
|
+
const params = new URLSearchParams(urlParts[1]);
|
|
125
|
+
platform = params.get('platform') || 'android';
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
// 优先尝试读取已构建的 manifest 文件
|
|
130
|
+
const manifestPath = path.join(projectRoot, 'build', 'bundles', platform, 'bundle-manifest.json');
|
|
131
|
+
if (fs.existsSync(manifestPath)) {
|
|
132
|
+
const manifestContent = fs.readFileSync(manifestPath, 'utf-8');
|
|
133
|
+
res.setHeader('Content-Type', 'application/json');
|
|
134
|
+
res.statusCode = 200;
|
|
135
|
+
res.end(manifestContent);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// 如果 manifest 文件不存在,基于 multi-bundle.config.json 动态生成
|
|
140
|
+
const config = loadMultiBundleConfig();
|
|
141
|
+
if (config) {
|
|
142
|
+
const manifest = generateManifestFromConfig(config, platform);
|
|
143
|
+
if (manifest) {
|
|
144
|
+
res.setHeader('Content-Type', 'application/json');
|
|
145
|
+
res.statusCode = 200;
|
|
146
|
+
res.end(JSON.stringify(manifest, null, 2));
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// 如果配置不存在或生成失败,返回错误
|
|
152
|
+
res.statusCode = 404;
|
|
153
|
+
res.setHeader('Content-Type', 'application/json');
|
|
154
|
+
res.end(JSON.stringify({
|
|
155
|
+
error: 'Manifest not found',
|
|
156
|
+
message: 'bundle-manifest.json not found and multi-bundle.config.json is missing or invalid'
|
|
157
|
+
}));
|
|
158
|
+
return;
|
|
159
|
+
} catch (error) {
|
|
160
|
+
res.statusCode = 500;
|
|
161
|
+
res.setHeader('Content-Type', 'application/json');
|
|
162
|
+
res.end(JSON.stringify({ error: 'Failed to generate manifest', message: error.message }));
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// 使用配置化的模块路径规则判断是否是模块 bundle 请求
|
|
168
|
+
if (req.url) {
|
|
169
|
+
// 去掉查询参数和 URL 前缀,只保留路径部分
|
|
170
|
+
let pathToCheck = req.url.split('?')[0];
|
|
171
|
+
// 去掉前导斜杠(如果存在)
|
|
172
|
+
if (pathToCheck.startsWith('/')) {
|
|
173
|
+
pathToCheck = pathToCheck.substring(1);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// 检查路径是否匹配模块路径规则
|
|
177
|
+
if (isModulePathHelper(pathToCheck, modulePaths)) {
|
|
178
|
+
isDevModuleRequest = true;
|
|
179
|
+
} else {
|
|
180
|
+
isDevModuleRequest = false;
|
|
181
|
+
}
|
|
182
|
+
} else {
|
|
183
|
+
isDevModuleRequest = false;
|
|
184
|
+
}
|
|
185
|
+
return middleware(req, res, next);
|
|
186
|
+
};
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
|
|
190
|
+
// ... 其余配置代码(serializer、transformer 等)请参考完整实现
|
|
191
|
+
// 完整的 Metro 配置包含:
|
|
192
|
+
// - serializer: 自定义模块 ID 工厂、processModuleFilter、customSerializer
|
|
193
|
+
// - transformer: getTransformOptions
|
|
194
|
+
// 这些配置较为复杂,建议参考 @bm-fe/react-native-multi-bundle 的完整实现
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
module.exports = mergeConfig(defaultConfig, config);
|