/** * ModuleLoaderMock - Native ModuleLoader 的 Mock 实现 * 用于开发环境,模拟 Native 模块加载能力 * * 注意:生产环境需要真实的 Native 实现 * * 在开发环境中,我们通过 Metro bundler 的 HTTP 服务器加载 bundle */ import { Platform } from 'react-native'; import { HMRClient } from './HMRClient'; import { getGlobalConfig } from './config'; interface LoadResult { success: boolean; errorMessage?: string; } // 已完成的 bundle const loadedBundles = new Set(); // 正在加载中的 bundle(并发复用) const inflightBundles = new Map>(); // 缓存 multi-bundle.config.json(开发环境) let cachedMultiBundleConfig: any = null; let configLoadPromise: Promise | null = null; /** * 从开发服务器获取 multi-bundle.config.json */ async function getMultiBundleConfig(): Promise { // 如果已缓存,直接返回 if (cachedMultiBundleConfig) { return cachedMultiBundleConfig; } // 如果正在加载,等待加载完成 if (configLoadPromise) { return configLoadPromise; } // 开始加载配置 configLoadPromise = (async () => { try { const config = getGlobalConfig(); const devServer = config?.devServer; const host = devServer?.host || (Platform.OS === 'android' ? '10.0.2.2' : 'localhost'); const port = devServer?.port || 8081; const protocol = devServer?.protocol || 'http'; const configUrl = `${protocol}://${host}:${port}/multi-bundle.config.json`; const response = await fetch(configUrl); if (response.ok) { const configData = await response.json(); cachedMultiBundleConfig = configData; return configData; } else { console.warn( `[ModuleLoaderMock] Failed to fetch multi-bundle.config.json: HTTP ${response.status}` ); return null; } } catch (error) { console.warn( `[ModuleLoaderMock] Failed to fetch multi-bundle.config.json: ${error}` ); return null; } finally { configLoadPromise = null; } })(); return configLoadPromise; } /** * 根据 bundlePath 构建 HTTP URL(开发环境 HTTP 模式) * * 优先从 multi-bundle.config.json 的 entry 字段获取模块路径 * 如果无法获取,则使用 bundlePath 进行简单推断 * * @param bundleId 模块 ID * @param bundlePath bundle 文件路径(从 manifest 中获取,开发环境可能不使用) */ async function buildHttpUrlFromBundlePath(bundleId: string, bundlePath: string): Promise { const config = getGlobalConfig(); const devServer = config?.devServer; const host = devServer?.host || (Platform.OS === 'android' ? '10.0.2.2' : 'localhost'); const port = devServer?.port || 8081; const protocol = devServer?.protocol || 'http'; // 优先从 multi-bundle.config.json 的 entry 获取模块路径 let sourcePath: string | null = null; try { const multiBundleConfig = await getMultiBundleConfig(); if (multiBundleConfig?.modules) { const moduleConfig = multiBundleConfig.modules.find((m: any) => m.id === bundleId); if (moduleConfig?.entry) { sourcePath = moduleConfig.entry; } } } catch (error) { console.warn( `[ModuleLoaderMock] Failed to get entry from multi-bundle.config.json for module ${bundleId}: ${error}` ); } // 如果无法从配置文件获取,使用 bundlePath 进行简单推断 if (!sourcePath) { // 如果 bundlePath 已经是源码路径,直接使用 if (bundlePath.startsWith('src/') || bundlePath.startsWith('./src/')) { sourcePath = bundlePath; } else { // 生产环境路径(如 bundles/home/index.bundle),提取模块名 const match = bundlePath.match(/(?:bundles\/|modules\/)(?:[^/]+\/)?([^/]+)\//); const moduleName = match?.[1] || bundleId.charAt(0).toUpperCase() + bundleId.slice(1); sourcePath = `src/modules/${moduleName}/index.bundle`; } } // 确保路径以 .bundle 结尾 if (!sourcePath.endsWith('.bundle')) { sourcePath = sourcePath.replace(/\.(ts|tsx|js|jsx)$/, '.bundle') || sourcePath + '/index.bundle'; } // 构建 Metro 请求 URL return `${protocol}://${host}:${port}/${sourcePath}` + `?platform=${Platform.OS}` + `&dev=true` + `&minify=false` + `&modulesOnly=true` + `&runModule=true`; } /** * 加载 bundle 文件(Mock 实现) * * 在开发环境中,我们通过 HTTP 服务器加载 bundle 并执行 * 实际生产环境应该调用 Native CodePush.loadBusinessBundle * * @param bundleId 模块 ID * @param bundlePath bundle 文件路径(开发环境可能不使用,但为保持接口一致性保留) */ async function loadBusinessBundle(bundleId: string, bundlePath: string): Promise { try { // 1. 基本校验 if (!bundleId || bundleId.trim() === '') { return { success: false, errorMessage: 'EMPTY_BUNDLE_ID', }; } if (!__DEV__) { return { success: false, errorMessage: 'ModuleLoaderMock should not be used in production. Please implement Native CodePush.loadBusinessBundle.', }; } // 开发环境:根据 bundlePath 动态构建 HTTP URL(优先从 multi-bundle.config.json 获取 entry) const httpUrl = await buildHttpUrlFromBundlePath(bundleId, bundlePath); // 已经加载过的 bundle,不再重复执行 if (loadedBundles.has(httpUrl)) { // 确保即使加载过也注册 HMR,处理重新连接或初始化时机问题 if (__DEV__) { HMRClient.getInstance().registerBundle(httpUrl); } return { success: true }; } // 正在加载中,复用同一个 Promise const existing = inflightBundles.get(httpUrl); if (existing) { return existing; } const task: Promise = (async () => { // 从 HTTP 服务器获取 bundle const response = await fetch(httpUrl); if (!response.ok) { // 如果 HTTP 加载失败 console.warn( `[ModuleLoaderMock] HTTP load failed (${response.status})` ); return { success: false, errorMessage: `HTTP_ERROR_${response.status}` }; } const bundleCode = await response.text(); // 在 React Native 中执行 bundle 代码 try { // 帮 stack trace 标一下来源(部分引擎支持) const wrappedCode = bundleCode.includes('//# sourceURL=') ? bundleCode : `${bundleCode}\n//# sourceURL=${httpUrl}`; // 使用 Function 构造函数执行代码(更安全) const executeCode = new Function(wrappedCode); executeCode(); loadedBundles.add(httpUrl); if (__DEV__) { HMRClient.getInstance().registerBundle(httpUrl); } return { success: true }; } catch (evalError) { // 如果 Function 构造函数失败,尝试直接 eval(不推荐,但作为后备) console.warn( '[ModuleLoaderMock] Function constructor failed, trying eval' ); try { // eslint-disable-next-line no-eval eval(bundleCode); loadedBundles.add(httpUrl); if (__DEV__) { HMRClient.getInstance().registerBundle(httpUrl); } return { success: true }; } catch (e) { const msg = e instanceof Error ? e.message : String(e); console.error(`[ModuleLoaderMock] Eval failed: ${msg}`); return { success: false, errorMessage: msg }; } } })(); inflightBundles.set(httpUrl, task); const result = await task; inflightBundles.delete(httpUrl); return result; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); console.error(`[ModuleLoaderMock] Load failed: ${errorMessage}`); return { success: false, errorMessage: `EXCEPTION: ${errorMessage}`, }; } } export const ModuleLoader = { loadBusinessBundle, // 为了向后兼容,保留 loadBundleFile 别名 loadBundleFile: loadBusinessBundle, }; export type { LoadResult };