ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Umi(@umijs/max)Module Federation 插件详解:mf 配置、运行时 API 与源码实现

Umi(@umijs/max)Module Federation 插件详解:mf 配置、运行时 API 与源码实现 Umiumijs/maxModule Federation 插件详解mf 配置、运行时 API 与源码实现【免费下载链接】umiA framework in react community ✨项目地址: https://gitcode.com/GitHub_Trending/um/umi本文基于 umi 官方文档与仓库源码完整讲解在 Umi / umijs/max 项目中使用 Module Federation 的两种方式通过mf配置消费与导出远程模块以及通过运行时 APIsafeMfImport、rawMfImport、registerMfRemote等在运行时动态加载。读完本文你将掌握mf配置项的完整字段与约束、src/exposes导出约定、MFSU 自动适配原理并能在真实示例examples/mf-host/examples/mf-remote中跑通一个跨应用的远程组件加载。前置条件与浏览器兼容性::: 警告 Module Federation 能力依赖浏览器的Top Level AwaitTLA支持。在生产环境使用前务必确认目标浏览器对 TLA 的支持情况。umi 的 MF 产物如remote.xxx.js是包含顶层await的 ES Module不支持 TLA 的浏览器如旧版 Safari将无法加载远程模块。 :::这一限制在源码中有直接体现当需要消费 remotes 时插件会把 Umi 入口替换为一个使用await import(...)的异步入口见 mf.ts 中的changeUmiEntry与onGenerateFiles里生成的asyncEntry.ts因此 TLA 是该能力不可回避的运行前提。插件实现位于 packages/plugins/src/mf.ts运行时 API 由模板 packages/plugins/templates/mf/runtime.ts.tpl 生成。配置消费远程模块mf.remotesumijs/max项目已内置该插件只需启用mf配置即可// .umirc.ts import { defineConfig } from umijs/max; const shared { react: { singleton: true, eager: true, }, react-dom: { singleton: true, eager: true, }, }; export default defineConfig({ // Module Federation plugin is already built-in, just enable the configuration mf: { remotes: [ { // 可选不配置时默认使用 remotes[].name 字段 aliasName: mfNameAlias, name: theMfName, entry: https://to.the.remote.com/remote.js, // 可选配置后会改写 mf 入口文件的 publicPath 为 window[mf_${name}EntryPath] runtimeEntryPath: {}, }, ], // 配置 MF shared 模块 shared, }, });普通 Umi 项目非 max需要显式引入插件// .umirc.ts import { defineConfig } from umi; const shared { react: { singleton: true, eager: true }, react-dom: { singleton: true, eager: true }, }; export default defineConfig({ plugins: [umijs/plugins/dist/mf], // 引入插件 mf: { remotes: [ { aliasName: mfNameAlias, name: theMfName, entry: https://to.the.remote.com/remote.js, }, ], shared, }, });配置完成后即可用import XXX from mfNameAlias/XXXX的方式消费远程模块。这里aliasName缺省时回退为name就是你在 import 语句中使用的模块前缀。从源码结构看插件的api.modifyWebpackConfig会把每个 remote 格式化后交给 webpack 的ModuleFederationPlugin静态entry直接拼成nameentry的形式mf.ts 中formatRemote返回${remote.name}${remote.entry}该行为有对应单测验证见 mf.test.ts 中remotes: { bar: foohttp://a.b/c.js }的用例。若remotes与exposes均为空插件会输出告警ModuleFederation exposes and remotes are empty, plugin will not work并不注入ModuleFederationPlugin。运行时决定远程模块地址entries keyResolver如果远程模块的地址需要在运行时按运行环境动态决定可以改用entrieskeyResolver// .umirc.ts defineConfig({ mf: { remotes: [ { name: theMfName, keyResolver: (function(){ try { return window.injectInfo.env || PROD } catch(e) { return PROD} })(), entries: { PRE: http://pre.mf.com/remote.js, PROD: http://produ.mf.com/remote.js, TEST: http://test.dev.mf.com/remote.js, DEV: http://127.0.0.1:8000/remote.js, }, }, ], shared, }, });使用注意事项与文档一致使用运行时加载逻辑时不要配置remotes[]#entry插件优先识别entries/keyResolver组合keyResolver决定运行时取entries的哪个键推荐使用立即执行函数表达式IIFE可以写更复杂的取值逻辑不支持异步函数keyResolver也可以直接使用静态值如keyResolver: PROD。源码视角keyResolver 是如何生效的在 mf.ts 的formatRemote中entries keyResolver会被展开为一段 webpack 的promise远程表达式把entries序列化为 JSON 内联进产物在浏览器里执行const key ${keyResolver}再取entries[key]作为script.src注入页面脚本加载完成后返回一个get/init代理对象从而把运行时决定地址这一动作推迟到了浏览器执行期。对应单测可参考 mf.test.ts 中mf plugin add remotes with entries for different env用例其中keyResolver: key1被展开为const key key1;。配置中若既没有entry也没有entries keyResolver插件会直接抛出Wrong MF#remotes config错误同样有单测覆盖。另外配置了runtimeEntryPath: {}后静态entry与动态entries两种形态都会改为从window[mf_${name}EntryPath]读取实际地址用于入口文件 publicPath 在运行时注入的场景该写法生成的是 webpack 官方推荐的 promise-based 远程容器注入脚本script.onload后返回get/initproxy。导出远程模块mf.name 与 src/exposes 约定让当前项目对外提供远程模块核心是mf.name// .umirc.ts // 抽成变量是为了与 MFSU 配置保持一致 const remoteMFName remoteMFName; defineConfig({ mf: { name: remoteMFName, // 可选远程模块的 library 类型。若模块需要在 Qiankun 子应用中消费 // 建议采用下面的取值注意 name 必须与最终 MF 模块名一致 // library: { type: window, name: exportMFName }, }, });::: 提示 配置的模块名必须是合法的 JavaScript 变量名 :::这一点在源码中是硬性校验mf.ts 的isValidIdentifyName用 Unicode 标识符正则\p{ID_Start}/\p{ID_Continue}加一份保留字黑名单做检查不合法时直接抛出module federation name xxx is not valid javascript identifier未配置name时会告警并回退为unNamedMF。exposes 目录约定导出的模块遵循目录约定以src/exposes下的目录名作为导出项目录下的 index 文件为导出文件。例如src/exposes/ ├── Button │ └── index.jsx ├── Head │ └── index.ts └── Form └── index.tsx插件会自动生成如下的 Module Federationexposesmf.ts 中constructExposes会遍历src/exposes目录非目录文件会被忽略并告警{ ./Button: src/exposes/Button/index.jsx, ./Head : src/exposes/Head/index.ts, ./Form : src/exposes/Form/index.tsx, }仓库内的 examples/mf-remote 就是最小的导出方示例其 .umirc.ts 配置了mf.name: remoteCounter、shared与library: { type: window, name: moduleFederationName }而导出内容只有一个 Counter 组件。remoteHash关闭 MF 产物的文件名哈希默认在用户开启hash: true时MF 产物入口会自动带哈希例如remote.123abc.js。可以设置remoteHash: false关闭产物为remote.js此时需要相应调整 nginx/CDN/网关对remote.js的缓存响应头否则新构建不会生效。无哈希带来的缓存风险与推荐实践可参考 umi 社区的 issue #11711 讨论。mf: { remoteHash: false, }对应实现见 mf.tsconst useHash typeof api.config.mf.remoteHash boolean ? api.config.mf.remoteHash : api.config.hash api.env ! development; const mfConfig { name, remotes, filename: useHash ? remote.[contenthash:8].js : remote.js, // ... };即remoteHash显式布尔值优先未显式配置时回退为开启hash且非 development 环境才加哈希。运行时 API什么时候使用运行时 APIimport(remote/Module)已经能简单使用 Module Federation但在以下场景建议使用运行时 API远程模块加载失败时页面需要展示fallback 兜底组件远程模块地址无法由同步函数决定需要异步调用获取远程模块的地址与模块名需要在运行时才确定。这五个 API 都由模板 runtime.ts.tpl 生成在插件onGenerateFiles阶段写入临时目录随构建产物一起打包。safeMfImport带兜底的远程模块加载函数safeMfImport(moduleSpecifier: string, fallback: any): Promiseany结合React.lazy可以实现远程组件懒加载import { safeMfImport } from umijs/max; import React, { Suspense } from react; const RemoteCounter React.lazy(() { return safeMfImport(remoteCounter/Counter, { default: () Fallback }); }); export default function Page() { return ( Suspense fallbackloading RemoteCounter / /Suspense ); };注意fallback组件需要包在对象default字段中以模拟一个模块remoteCounter/Counter前缀必须与配置中的aliasName或name对应。从源码看safeMfImport会把moduleSpecifier按第一个/拆分为aliasName与module查不到对应 remote 或加载抛错时都会返回fallback见 runtime.ts.tpl 第 27~59 行。示例代码见 examples/mf-host/src/pages/safe-import.tsx。safeRemoteComponent封装safeMfImport的高阶组件safeRemoteComponentT extends React.ComponentTypeany(opts: { moduleSpecifier: string; // 远程模块地址aliasName/模块名 fallbackComponent: React.ComponentTypeany; // 加载失败时的兜底组件 loadingElement: React.ReactNode; // 加载中的展示 }): T示例const RemoteCounter safeRemoteComponentReact.FC{ init?: number }({ moduleSpecifier: remoteCounter/Counter, fallbackComponent: () fallbacked, loadingElement: Loading, }); export default function Page() { return ( div RemoteCounter init{808} / /div ); };实现上它就是React.lazy(() safeMfImport(moduleSpecifier, { default: fallbackComponent }))外包一层Suspense fallback{loadingElement}见 runtime.ts.tpl 第 122~127 行。示例代码见 examples/mf-host/src/pages/safe-remote-component.tsx。rawMfImport不依赖构建期配置、完全在运行时加载远程模块rawMfImport(opts: { entry: string; // 远程入口地址 remoteName: string; // 远程模块名 moduleName: string; // 要加载的模块名 }): Promiseanyconst RemoteCounter React.lazy(() { return rawMfImport({ entry: http://localhost:8001/remote.js, moduleName: Counter, remoteName: remoteCounter, }); });示例代码见 examples/mf-host/src/pages/raw-mf-import.tsx。safeRemoteComponentWithMfConfig封装rawMfImport的高阶组件加载失败自动回退type RawRemoteComponentOpts { mfConfig: { entry: string; remoteName: string; moduleName: string; }; fallbackComponent: ComponentTypeany; loadingElement: ReactNode; } safeRemoteComponentWithMfConfigT extends ComponentTypeany(opts: RawRemoteComponentOpts): Tconst RemoteCounter safeRemoteComponentWithMfConfigReact.FC{ init?: number }({ mfConfig: { entry: http://localhost:8001/remote.js, moduleName: Counter, remoteName: remoteCounter, }, fallbackComponent: () raw Fallback, loadingElement: raw Loading, }); export default function Page() { return RemoteCounter /; };示例代码见 examples/mf-host/src/pages/raw-mf-component.tsx。registerMfRemote运行时动态注册远程模块配置type MFModuleRegisterRequest { entry: string; remoteName: string; aliasName?: string; } registerMfRemote(opts: MFModuleRegisterRequest): void使用safeMfImport/safeRemoteComponent时moduleSpecifier必须是构建期配置过的远程模块而rawMfImport又略显繁琐。registerMfRemote正是折中方案先注册再使用简洁的safeMfImport/safeRemoteComponent。registerMfRemote({ aliasName: registered, remoteName: remoteCounter, entry: http://127.0.0.1:8001/remote.js, }); const RemoteCounter React.lazy(() { return safeMfImport(registered/Counter, { default: null }); });从源码看registerMfRemote只是把{ entry, remoteName, aliasName }挂入模板内的remotes表重复注册会告警并忽略因此注册后所有基于aliasName的 API 都立即可用。示例代码见 examples/mf-host/src/pages/register-then-import.tsx并有 Cypress 用例验证注册后可正常加载远程 Counter见 safe-import.cy.ts。所有远程脚本的加载都经过loadRemoteScriptWithCache同一remoteName的脚本只注入一次加载中的 Promise 被缓存复用失败后清除缓存以便重试。与 MFSU 的配合Module Federation 插件会基于自身配置自动改写 MFSU 的默认配置让两个特性在开发阶段可以并存。以如下mf配置为例// .umirc.ts export default defineConfig({ mf: { name: remoteMFName, remotes: [ { name: remote1, entry: https://to.the.remote.com/remote.js }, { aliasName: aliasRemote, name: remote2, entry: https://to.the.remote.com/remote2.js }, ], shared: { react: { singleton: true, eager: true }, react-dom: { singleton: true, eager: true }, }, }, });最终生效的等效配置为{ mfsu: { // mf 插件自动填入以下兼容 MFSU 的默认配置 // MFSU 也可以在 DEV 阶段调试 MF 模块 remoteName: remoteMFName, remoteAliases: [remote1, aliasRemote], shared: { react: { singleton: true, eager: true }, react-dom: { singleton: true, eager: true }, }, }, mf: { /* 同上保持不变 */ }, }实现位于 mf.ts 的api.modifyDefaultConfig当存在exposes时写入memo.mfsu.remoteName mfName()并额外生成memo.mfsu.mfName mf_ remoteName源码注释说明这是为了避免与宿主默认模块名冲突再把formatRemotes()的键集aliasName 优先写入memo.mfsu.remoteAliases、把shared原样透传。另一个关键配合点在开发环境当api.env development api.config.mfsu时插件跳过把umi.ts入口替换为asyncEntry.ts的逻辑onGenerateFiles中 stage 10001 的早退分支因为 MFSU 已经通过动态 import 处理了入口避免双重改造。生产环境或关闭 MFSU 时才会执行changeUmiEntry把entry.umi指向生成的plugin-mf/asyncEntry.ts——这也是产物需要 TLA 的直接原因。完整示例mf-host 与 mf-remote仓库内置了一对可直接运行的示例工程覆盖了本文全部 API消费方 examples/mf-host.umirc.ts 同时演示了entries keyResolver的动态地址keyResolver: \(() DEV)()指向http://127.0.0.1:9000/remote.js与一个故意指向不存在地址的utopia 远程用于验证 fallback 行为提供方 examples/mf-remote.umirc.ts 配置了mf.name: remoteCounter、library: { type: window, name: ... }与mfsu: { strategy: eager }导出 Counter 组件页面级用法可逐一对应查看 examples/mf-host/src/pages 下的dynamic-import.tsximport(remoteCounter/Counter)、safe-import.tsx、safe-remote-component.tsx、raw-mf-import.tsx、raw-mf-component.tsx、register-then-import.tsxE2E 验证见 examples/mf-host/cypress/e2e/safe-import.cy.ts不仅验证远程组件渲染与 hooks 正常工作远程计数器 10→11、宿主计数器 42→43说明宿主与远程共享了同一份 React还覆盖动态注册远程、以及safe-remote-component传入init{808}的 props 透传插件自身的行为契约空配置不生效、错误配置报错、entry/entries/runtimeEntryPath三种展开形态则由 mf.test.ts 与 mfUtils.test.ts 覆盖。小结mf.remotesname/aliasName/entry或entrieskeyResolver可选runtimeEntryPath决定消费哪些远程模块import aliasName/Module是静态用法mf.namesrc/exposes目录约定决定导出什么模块mf.library用于 Qiankun 等场景模块名必须是合法 JS 标识符源码强校验remoteHash: false可关闭remote.[contenthash:8].js命名但需自行处理 CDN/网关缓存头需要兜底、异步定址或运行时注册时使用safeMfImport/safeRemoteComponent/rawMfImport/safeRemoteComponentWithMfConfig/registerMfRemote五件套它们全部由 runtime.ts.tpl 生成插件会自动向 MFSU 注入remoteName/remoteAliases/shared默认配置开发期两特性可平滑共存产物依赖 Top Level Await选型时把浏览器兼容性纳入评估。【免费下载链接】umiA framework in react community ✨项目地址: https://gitcode.com/GitHub_Trending/um/umi创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表