@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.
Files changed (32) hide show
  1. package/INTEGRATION.md +371 -0
  2. package/LICENSE +21 -0
  3. package/README.md +240 -0
  4. package/package.json +86 -0
  5. package/scripts/build-multi-bundle.js +483 -0
  6. package/scripts/release-codepush-dev.sh +90 -0
  7. package/scripts/release-codepush.js +420 -0
  8. package/scripts/sync-bundles-to-assets.js +252 -0
  9. package/src/index.ts +40 -0
  10. package/src/multi-bundle/DevModeConfig.ts +23 -0
  11. package/src/multi-bundle/HMRClient.ts +157 -0
  12. package/src/multi-bundle/LocalBundleManager.ts +155 -0
  13. package/src/multi-bundle/ModuleErrorFallback.tsx +85 -0
  14. package/src/multi-bundle/ModuleLoaderMock.ts +169 -0
  15. package/src/multi-bundle/ModuleLoadingPlaceholder.tsx +34 -0
  16. package/src/multi-bundle/ModuleRegistry.ts +295 -0
  17. package/src/multi-bundle/README.md +343 -0
  18. package/src/multi-bundle/config.ts +141 -0
  19. package/src/multi-bundle/createModuleLoader.tsx +92 -0
  20. package/src/multi-bundle/createModuleRouteLoader.tsx +31 -0
  21. package/src/multi-bundle/devUtils.ts +48 -0
  22. package/src/multi-bundle/init.ts +131 -0
  23. package/src/multi-bundle/metro-config-helper.js +140 -0
  24. package/src/multi-bundle/preloadModule.ts +33 -0
  25. package/src/multi-bundle/routeRegistry.ts +118 -0
  26. package/src/types/global.d.ts +14 -0
  27. package/templates/metro.config.js.template +45 -0
  28. package/templates/multi-bundle.config.json.template +31 -0
  29. package/templates/native/android/ModuleLoaderModule.kt +227 -0
  30. package/templates/native/android/ModuleLoaderPackage.kt +26 -0
  31. package/templates/native/ios/ModuleLoaderModule.h +13 -0
  32. package/templates/native/ios/ModuleLoaderModule.m +60 -0
@@ -0,0 +1,141 @@
1
+ /**
2
+ * MultiBundleConfig - 多 Bundle 配置系统
3
+ * 提供统一的配置接口,支持代码配置方式
4
+ */
5
+
6
+ import { BundleManifest } from './ModuleRegistry';
7
+ import { Platform } from 'react-native';
8
+
9
+ /**
10
+ * 开发服务器配置
11
+ */
12
+ export interface DevServerConfig {
13
+ host: string;
14
+ port: number;
15
+ protocol?: 'http' | 'https';
16
+ }
17
+
18
+ /**
19
+ * Manifest 提供者类型
20
+ * 支持函数(异步获取)或 URL 字符串
21
+ */
22
+ export type ManifestProvider = (() => Promise<BundleManifest>) | string;
23
+
24
+ /**
25
+ * ModuleLoader 接口
26
+ */
27
+ export interface ModuleLoader {
28
+ loadBusinessBundle(moduleId: string, bundlePath: string): Promise<{ success: boolean; errorMessage?: string }>;
29
+ }
30
+
31
+ /**
32
+ * 多 Bundle 配置接口
33
+ */
34
+ export interface MultiBundleConfig {
35
+ /**
36
+ * 模块路径规则(glob 模式)
37
+ * 用于 Metro 配置中识别模块 bundle
38
+ * 例如: ['src/modules/**']
39
+ */
40
+ modulePaths?: string[];
41
+
42
+ /**
43
+ * 共享依赖路径列表(glob 模式)
44
+ * 这些路径下的代码会被打包到主 bundle,模块 bundle 可以共享使用
45
+ * 例如: ['src/multi-bundle/**', 'src/navigation/**']
46
+ */
47
+ sharedDependencies?: string[];
48
+
49
+ /**
50
+ * Manifest 提供者
51
+ * - 函数:自定义获取逻辑
52
+ * - 字符串:manifest URL(开发环境从 HTTP 服务器获取)
53
+ */
54
+ manifestProvider?: ManifestProvider;
55
+
56
+ /**
57
+ * 自定义 ModuleLoader(可选)
58
+ * 如果不提供,将使用默认的 ModuleLoader(开发环境用 Mock,生产环境用 Native)
59
+ */
60
+ moduleLoader?: ModuleLoader;
61
+
62
+ /**
63
+ * 预加载模块列表
64
+ * 应用启动时自动预加载这些模块
65
+ */
66
+ preloadModules?: string[];
67
+
68
+ /**
69
+ * 开发服务器配置
70
+ * 用于开发环境从 HTTP 服务器获取 manifest 和 bundle
71
+ */
72
+ devServer?: DevServerConfig;
73
+
74
+ /**
75
+ * 错误处理回调
76
+ */
77
+ onError?: (error: Error) => void;
78
+ }
79
+
80
+ /**
81
+ * 默认配置
82
+ */
83
+ const DEFAULT_CONFIG: Required<Omit<MultiBundleConfig, 'moduleLoader' | 'manifestProvider' | 'onError'>> & {
84
+ moduleLoader?: ModuleLoader;
85
+ manifestProvider?: ManifestProvider;
86
+ onError?: (error: Error) => void;
87
+ } = {
88
+ modulePaths: ['src/modules/**'],
89
+ sharedDependencies: ['src/multi-bundle/**', 'src/navigation/**'],
90
+ preloadModules: [],
91
+ devServer: {
92
+ host: Platform.OS === 'android' ? '10.0.2.2' : 'localhost',
93
+ port: 8081,
94
+ protocol: 'http',
95
+ },
96
+ };
97
+
98
+ /**
99
+ * 合并配置
100
+ * 将用户配置与默认配置合并
101
+ */
102
+ export function mergeConfig(
103
+ userConfig: MultiBundleConfig
104
+ ): Required<Omit<MultiBundleConfig, 'moduleLoader' | 'manifestProvider' | 'onError'>> & {
105
+ moduleLoader?: ModuleLoader;
106
+ manifestProvider?: ManifestProvider;
107
+ onError?: (error: Error) => void;
108
+ } {
109
+ return {
110
+ modulePaths: userConfig.modulePaths ?? DEFAULT_CONFIG.modulePaths,
111
+ sharedDependencies: userConfig.sharedDependencies ?? DEFAULT_CONFIG.sharedDependencies,
112
+ preloadModules: userConfig.preloadModules ?? DEFAULT_CONFIG.preloadModules,
113
+ devServer: {
114
+ ...DEFAULT_CONFIG.devServer,
115
+ ...userConfig.devServer,
116
+ },
117
+ moduleLoader: userConfig.moduleLoader,
118
+ manifestProvider: userConfig.manifestProvider,
119
+ onError: userConfig.onError,
120
+ };
121
+ }
122
+
123
+ /**
124
+ * 全局配置实例(用于 Metro 配置等场景)
125
+ */
126
+ let globalConfig: ReturnType<typeof mergeConfig> | null = null;
127
+
128
+ /**
129
+ * 设置全局配置
130
+ */
131
+ export function setGlobalConfig(config: ReturnType<typeof mergeConfig>) {
132
+ globalConfig = config;
133
+ }
134
+
135
+ /**
136
+ * 获取全局配置
137
+ */
138
+ export function getGlobalConfig(): ReturnType<typeof mergeConfig> | null {
139
+ return globalConfig;
140
+ }
141
+
@@ -0,0 +1,92 @@
1
+ /**
2
+ * createModuleLoader - 创建模块加载器函数
3
+ * 用于 React Navigation 的 getComponent API,实现真正的懒加载
4
+ * 组件只在导航到该路由时才被创建和加载
5
+ */
6
+
7
+ import React, { useEffect, useState } from 'react';
8
+ import { ModuleRegistry } from './ModuleRegistry';
9
+ import { ModuleLoadingPlaceholder } from './ModuleLoadingPlaceholder';
10
+ import { ModuleErrorFallback } from './ModuleErrorFallback';
11
+
12
+ type ModuleState = 'idle' | 'loading' | 'success' | 'error';
13
+
14
+ interface CreateModuleLoaderOptions {
15
+ onError?: (error: Error) => void;
16
+ }
17
+
18
+ /**
19
+ * 创建模块加载器函数
20
+ * @param moduleId 模块 ID
21
+ * @param pickComponent 从模块 exports 中提取组件的函数
22
+ * @param options 可选配置
23
+ * @returns 返回一个函数,该函数返回 React 组件(用于 getComponent)
24
+ */
25
+ export function createModuleLoader(
26
+ moduleId: string,
27
+ pickComponent: (exports: any) => React.ComponentType<any>,
28
+ options: CreateModuleLoaderOptions = {}
29
+ ): () => React.ComponentType<any> {
30
+ return function getModuleComponent() {
31
+ function ModuleWrappedScreen(props: any) {
32
+ const [state, setState] = useState<ModuleState>('idle');
33
+ const [InnerComponent, setInnerComponent] =
34
+ useState<React.ComponentType<any> | null>(null);
35
+ const [error, setError] = useState<Error | null>(null);
36
+
37
+ useEffect(() => {
38
+ let isMounted = true;
39
+
40
+ async function load() {
41
+ try {
42
+ setState('loading');
43
+
44
+ await ModuleRegistry.loadModule(moduleId);
45
+ if (!isMounted) return;
46
+
47
+ const exports = ModuleRegistry.getModuleExports(moduleId);
48
+ const Comp = pickComponent(exports);
49
+
50
+ setInnerComponent(() => Comp);
51
+ setState('success');
52
+ } catch (e) {
53
+ if (!isMounted) return;
54
+ const err = e instanceof Error ? e : new Error(String(e));
55
+ setError(err);
56
+ setState('error');
57
+ options.onError?.(err);
58
+ }
59
+ }
60
+
61
+ load();
62
+
63
+ return () => {
64
+ isMounted = false;
65
+ };
66
+ }, [moduleId]);
67
+
68
+ if (state === 'error') {
69
+ return (
70
+ <ModuleErrorFallback
71
+ moduleId={moduleId}
72
+ error={error}
73
+ onRetry={() => {
74
+ setState('idle');
75
+ setError(null);
76
+ }}
77
+ />
78
+ );
79
+ }
80
+
81
+ if (state === 'loading' || !InnerComponent) {
82
+ return <ModuleLoadingPlaceholder moduleId={moduleId} />;
83
+ }
84
+
85
+ return <InnerComponent {...props} />;
86
+ }
87
+
88
+ (ModuleWrappedScreen as any).displayName = `ModuleLoader(${moduleId})`;
89
+
90
+ return ModuleWrappedScreen;
91
+ };
92
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * createModuleRouteLoader - 创建路由模块加载器函数
3
+ * 简化路由定义的辅助函数,自动从模块 exports.routes 中提取组件
4
+ * 用于 React Navigation 的 getComponent API
5
+ */
6
+
7
+ import React from 'react';
8
+ import { createModuleLoader } from './createModuleLoader';
9
+
10
+ /**
11
+ * 创建路由模块加载器函数
12
+ * @param moduleId 模块 ID
13
+ * @param routeKey 路由 key(对应模块 exports.routes[routeKey])
14
+ * @returns 返回一个函数,该函数返回 React 组件(用于 getComponent)
15
+ */
16
+ export function createModuleRouteLoader(
17
+ moduleId: string,
18
+ routeKey: string
19
+ ): () => React.ComponentType<any> {
20
+ return createModuleLoader(
21
+ moduleId,
22
+ (exports: any) => {
23
+ if (!exports || !exports.routes || !exports.routes[routeKey]) {
24
+ throw new Error(
25
+ `Route "${routeKey}" not found in module "${moduleId}"`
26
+ );
27
+ }
28
+ return exports.routes[routeKey];
29
+ }
30
+ );
31
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * devUtils - 开发工具函数
3
+ * 提供开发模式下的调试和工具函数
4
+ */
5
+
6
+ import { ModuleRegistry } from './ModuleRegistry';
7
+
8
+ /**
9
+ * 获取所有模块的状态信息
10
+ */
11
+ export function getAllModuleStates() {
12
+ // 这里需要访问 ModuleRegistry 的内部状态
13
+ // 为了不暴露内部实现,我们可以通过 ModuleRegistry 添加一个调试方法
14
+ // 当前简化实现
15
+ return {
16
+ // 可以通过 ModuleRegistry 暴露的方法获取
17
+ };
18
+ }
19
+
20
+ /**
21
+ * 开发模式下的日志
22
+ */
23
+ export function logModuleLoad(moduleId: string, event: string, data?: any) {
24
+ if (__DEV__) {
25
+ console.log(`[ModuleRegistry:${moduleId}] ${event}`, data || '');
26
+ }
27
+ }
28
+
29
+ /**
30
+ * 重置模块状态(仅开发环境)
31
+ */
32
+ export function resetModule(moduleId: string) {
33
+ if (__DEV__) {
34
+ console.warn(`[devUtils] resetModule is not implemented yet for: ${moduleId}`);
35
+ // 实际实现需要 ModuleRegistry 暴露 reset 方法
36
+ }
37
+ }
38
+
39
+ /**
40
+ * 重新加载模块(仅开发环境)
41
+ */
42
+ export function reloadModule(moduleId: string) {
43
+ if (__DEV__) {
44
+ console.warn(`[devUtils] reloadModule is not implemented yet for: ${moduleId}`);
45
+ // 实际实现需要 ModuleRegistry 暴露 reload 方法
46
+ }
47
+ }
48
+
@@ -0,0 +1,131 @@
1
+ /**
2
+ * initMultiBundle - 多 Bundle 统一初始化入口
3
+ * 封装所有初始化逻辑,业务代码只需调用一次即可完成所有初始化
4
+ */
5
+
6
+ import { ModuleRegistry, BundleManifest } from './ModuleRegistry';
7
+ import { preloadModule } from './preloadModule';
8
+ import { mergeConfig, setGlobalConfig, MultiBundleConfig } from './config';
9
+ import { LocalBundleManager } from './LocalBundleManager';
10
+ import { HMRClient } from './HMRClient';
11
+
12
+ /**
13
+ * 初始化结果
14
+ */
15
+ export interface InitResult {
16
+ success: boolean;
17
+ error?: Error;
18
+ manifest?: BundleManifest;
19
+ }
20
+
21
+ /**
22
+ * 从 manifestProvider 获取 manifest
23
+ */
24
+ async function getManifestFromProvider(
25
+ provider: MultiBundleConfig['manifestProvider']
26
+ ): Promise<BundleManifest> {
27
+ if (!provider) {
28
+ // 如果没有提供 provider,使用默认的 LocalBundleManager
29
+ return await LocalBundleManager.getCurrentBundleManifest();
30
+ }
31
+
32
+ if (typeof provider === 'string') {
33
+ // URL 模式:从 HTTP 服务器获取
34
+ try {
35
+ const response = await fetch(provider);
36
+ if (response.ok) {
37
+ return await response.json();
38
+ }
39
+ throw new Error(`Failed to fetch manifest from ${provider}: ${response.statusText}`);
40
+ } catch (error) {
41
+ const err = error instanceof Error ? error : new Error(String(error));
42
+ console.warn(`[initMultiBundle] Failed to fetch manifest from URL: ${err.message}`);
43
+ // 降级到默认方式
44
+ return await LocalBundleManager.getCurrentBundleManifest();
45
+ }
46
+ } else {
47
+ // 函数模式:调用自定义函数
48
+ return await provider();
49
+ }
50
+ }
51
+
52
+ /**
53
+ * 初始化多 Bundle 系统
54
+ *
55
+ * @param config 配置对象
56
+ * @returns 初始化结果
57
+ *
58
+ * @example
59
+ * ```typescript
60
+ * const result = await initMultiBundle({
61
+ * modulePaths: ['src/modules/**'],
62
+ * sharedDependencies: ['src/multi-bundle/**', 'src/navigation/**'],
63
+ * preloadModules: ['settings'],
64
+ * });
65
+ * ```
66
+ */
67
+ export async function initMultiBundle(config: MultiBundleConfig = {}): Promise<InitResult> {
68
+ try {
69
+ // 1. 合并配置
70
+ const mergedConfig = mergeConfig(config);
71
+
72
+ // 2. 设置全局配置(供 Metro 配置等使用)
73
+ setGlobalConfig(mergedConfig);
74
+
75
+ // 2.5 初始化 HMR Client (开发环境)
76
+ if (__DEV__ && mergedConfig.devServer) {
77
+ HMRClient.getInstance().setup(
78
+ mergedConfig.devServer.host,
79
+ mergedConfig.devServer.port
80
+ );
81
+ }
82
+
83
+ // 3. 获取 manifest
84
+ let manifest: BundleManifest;
85
+ try {
86
+ manifest = await getManifestFromProvider(mergedConfig.manifestProvider);
87
+ } catch (error) {
88
+ const err = error instanceof Error ? error : new Error(String(error));
89
+ console.error('[initMultiBundle] Failed to get manifest:', err);
90
+ config.onError?.(err);
91
+ return {
92
+ success: false,
93
+ error: err,
94
+ };
95
+ }
96
+
97
+ // 4. 设置自定义 ModuleLoader(如果提供)
98
+ if (mergedConfig.moduleLoader) {
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
+ }
116
+
117
+ return {
118
+ success: true,
119
+ manifest,
120
+ };
121
+ } catch (error) {
122
+ const err = error instanceof Error ? error : new Error(String(error));
123
+ console.error('[initMultiBundle] Initialization failed:', err);
124
+ config.onError?.(err);
125
+ return {
126
+ success: false,
127
+ error: err,
128
+ };
129
+ }
130
+ }
131
+
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Metro 配置辅助模块
3
+ * 用于在 Node.js 环境中读取多 Bundle 配置
4
+ * 支持从环境变量或默认值读取配置
5
+ */
6
+
7
+ const path = require('path');
8
+ const fs = require('fs');
9
+
10
+ /**
11
+ * 从环境变量读取配置
12
+ */
13
+ function getConfigFromEnv() {
14
+ const modulePaths = process.env.RN_MULTI_BUNDLE_MODULE_PATHS
15
+ ? JSON.parse(process.env.RN_MULTI_BUNDLE_MODULE_PATHS)
16
+ : ['src/modules/**'];
17
+
18
+ const sharedDependencies = process.env.RN_MULTI_BUNDLE_SHARED_DEPS
19
+ ? JSON.parse(process.env.RN_MULTI_BUNDLE_SHARED_DEPS)
20
+ : ['src/multi-bundle/**', 'src/navigation/**'];
21
+
22
+ return {
23
+ modulePaths,
24
+ sharedDependencies,
25
+ };
26
+ }
27
+
28
+ /**
29
+ * 检查路径是否匹配 glob 模式
30
+ * 简化版 glob 匹配(支持 ** 和 *)
31
+ */
32
+ function matchGlob(filePath, pattern) {
33
+ const normalizedPath = filePath.replace(/\\/g, '/');
34
+ const normalizedPattern = pattern.replace(/\\/g, '/');
35
+
36
+ // 将 glob 模式转换为正则表达式
37
+ const regexPattern = normalizedPattern
38
+ .replace(/\*\*/g, '___DOUBLE_STAR___')
39
+ .replace(/\*/g, '[^/]*')
40
+ .replace(/___DOUBLE_STAR___/g, '.*');
41
+
42
+ const regex = new RegExp(`^${regexPattern}$`);
43
+ return regex.test(normalizedPath);
44
+ }
45
+
46
+ /**
47
+ * 检查文件路径是否匹配模块路径规则
48
+ */
49
+ function isModulePath(filePath, modulePaths) {
50
+ return modulePaths.some((pattern) => matchGlob(filePath, pattern));
51
+ }
52
+
53
+ /**
54
+ * 检查文件路径是否是共享依赖
55
+ */
56
+ function isSharedDependencyPath(filePath, sharedDependencies) {
57
+ // 先检查 node_modules 中的共享依赖
58
+ if (filePath.includes('/node_modules/')) {
59
+ const sharedNodeModules = [
60
+ 'react',
61
+ 'react/jsx-runtime',
62
+ 'react-native',
63
+ '@react-navigation',
64
+ ];
65
+ return sharedNodeModules.some((dep) =>
66
+ filePath.includes(`/node_modules/${dep}`)
67
+ );
68
+ }
69
+
70
+ // 再检查本地共享依赖路径
71
+ return sharedDependencies.some((pattern) => matchGlob(filePath, pattern));
72
+ }
73
+
74
+ /**
75
+ * 从模块路径提取模块名(支持配置化的路径规则)
76
+ */
77
+ function extractModuleNameFromPath(modulePath, modulePaths) {
78
+ const normalizedPath = modulePath.replace(/\\/g, '/');
79
+
80
+ // 尝试从配置的模块路径中提取模块名
81
+ for (const pattern of modulePaths) {
82
+ // 将 glob 模式转换为匹配模式
83
+ // 例如: src/modules/** -> src/modules/([^/]+)
84
+ const matchPattern = pattern
85
+ .replace(/\*\*/g, '.*')
86
+ .replace(/\*/g, '[^/]*')
87
+ .replace(/\(\[/g, '(')
88
+ .replace(/\]\*\)/g, '+)');
89
+
90
+ // 尝试提取模块名
91
+ // 对于 src/modules/**,我们尝试匹配 src/modules/([^/]+)/
92
+ const regex = new RegExp(`src/modules/([^/]+)/`, 'i');
93
+ const match = normalizedPath.match(regex);
94
+ if (match) {
95
+ return match[1].toLowerCase();
96
+ }
97
+ }
98
+
99
+ // 降级到默认规则(向后兼容)
100
+ const defaultMatch = normalizedPath.match(/src\/modules\/([^\/]+)\//i);
101
+ if (defaultMatch) {
102
+ return defaultMatch[1].toLowerCase();
103
+ }
104
+
105
+ return null;
106
+ }
107
+
108
+ /**
109
+ * 从模块入口路径提取模块名(支持配置化的路径规则)
110
+ */
111
+ function extractModuleNameFromEntry(modulePath, modulePaths) {
112
+ const normalizedPath = modulePath.replace(/\\/g, '/');
113
+
114
+ // 尝试从配置的模块路径中提取模块名
115
+ for (const pattern of modulePaths) {
116
+ // 对于入口文件,通常格式为 src/modules/ModuleName/index.ts
117
+ const regex = new RegExp(`src/modules/([^/]+)/index\\.(ts|tsx|js|jsx)$`, 'i');
118
+ const match = normalizedPath.match(regex);
119
+ if (match) {
120
+ return match[1].toLowerCase();
121
+ }
122
+ }
123
+
124
+ // 降级到默认规则(向后兼容)
125
+ const defaultMatch = normalizedPath.match(/src\/modules\/([^\/]+)\/index\.(ts|tsx|js|jsx)$/i);
126
+ if (defaultMatch) {
127
+ return defaultMatch[1].toLowerCase();
128
+ }
129
+
130
+ return null;
131
+ }
132
+
133
+ module.exports = {
134
+ getConfigFromEnv,
135
+ isModulePath,
136
+ isSharedDependencyPath,
137
+ extractModuleNameFromPath,
138
+ extractModuleNameFromEntry,
139
+ };
140
+
@@ -0,0 +1,33 @@
1
+ /**
2
+ * preloadModule - 模块预加载函数
3
+ * 静默预加载模块,失败不打断主流程
4
+ */
5
+
6
+ import { ModuleRegistry } from './ModuleRegistry';
7
+
8
+ /**
9
+ * 预加载模块
10
+ * @param moduleId 模块 ID
11
+ */
12
+ export async function preloadModule(moduleId: string): Promise<void> {
13
+ const state = ModuleRegistry.getModuleState(moduleId);
14
+
15
+ // 已加载就不干活
16
+ if (state === 'loaded') {
17
+ return;
18
+ }
19
+
20
+ // loading 中也不重复发起
21
+ if (state === 'loading') {
22
+ return;
23
+ }
24
+
25
+ try {
26
+ await ModuleRegistry.loadModule(moduleId);
27
+ } catch (error) {
28
+ // 预加载失败对用户"无感知",但记录日志 / 埋点
29
+ console.warn(`[preloadModule] failed: ${moduleId}`, error);
30
+ // 可以在这里上报监控
31
+ }
32
+ }
33
+