资讯详情

资讯详情

webpack Module Federation 官方示例拆解:app 宿主与 mfe-b、mfe-c 两个远端容器如何协作共享模块

webpack Module Federation 官方示例拆解app 宿主与 mfe-b、mfe-c 两个远端容器如何协作共享模块【免费下载链接】webpackA bundler for javascript and friends. Packs many modules into a few bundled assets. Code Splitting allows for loading parts of the application on demand. Through loaders, modules can be CommonJs, AMD, ES6 modules, CSS, Images, JSON, Coffeescript, LESS, ... and your custom stuff.项目地址: https://gitcode.com/GitHub_Trending/web/webpack本篇围绕当前 webpack 仓库中的官方示例 examples/module-federation 展开用一个webpack.config.js同时构建宿主 app 远端容器 mfe-b 远端容器 mfe-c三个互无编译期依赖的 bundle完整演示ModuleFederationPlugin的remotes、exposes、shared三大配置以及 React、date-fns、lodash 在容器之间的共享与按需加载。读完本文你将掌握一套可直接复制的多容器配置写法、import()异步边界的使用原理以及基于构建产物的运行时协作细节。示例概览一个目录、三份构建、零编译期耦合本示例的完整源码位于 examples/module-federation目录结构如下examples/module-federation/ ├── webpack.config.js # 一份配置导出三个编译目标app / mfe-b / mfe-c ├── index.html # 承载页面加载 app 与两个 remote 容器脚本 ├── src/ # app宿主代码index.js、bootstrap.js、App.js ├── src-b/ # 容器 B 的源码Component.js ├── src-c/ # 容器 C 的源码Component.js、LazyComponent.js ├── README.md # 示例说明含归档的构建产物逐行解读 └── template.md # 生成 README 的模板三个构建的关系如下构建名角色容器名关键职责app宿主host/consumer无不需要name通过remotes引用远端运行时消费共享模块mfe-b远端容器 BmfeBBB通过exposes暴露./Component共享 react、date-fnsmfe-c远端容器 CmfeCCC暴露./Component与懒加载的./Component2共享 lodash、date-fns、react配置代码里有一句非常关键的注释见 webpack.config.jsFor Module Federation there is not compile-time dependency between the builds. Each one can have different config options.也就是说Module Federation 的各个构建之间在编译期零耦合它们彼此只是运行时的伙伴。这也是为什么官方会把三个配置写进同一个文件只是为了示例方便——注释明确提示真实项目中完全可以拆成独立配置文件甚至各自独立的仓库。一份配置导出三份构建多配置的组织方式webpack.config.js 不是导出单个配置对象而是导出一个接收环境参数env的函数返回配置数组并带有 JSDoc 类型标注/** type {(env: development | production) import(webpack).Configuration[]} */ const config (env development) [ { name: app, ... }, { name: mfe-b, ... }, { name: mfe-c, ... } ]; module.exports config;各构建共享若干公共片段实际写法见源文件可整体复制运行mode: env跟随命令行传入的development/productionoutput.filename: [name].js、output.path分别落到dist/aaa、dist/bbb、dist/ccc且publicPath都使用相对路径如dist/bbb/保证任意静态目录下脚本路径自洽optimization.chunkIds: named让产物里出现可读的 chunk 文件名生产模式也保留仅示例用optimization.nodeEnv: production让 React 始终使用生产版本仅示例用module.rules对path.resolve(__dirname, src)下的.js使用babel-loaderpresets 为[babel/react]用于解析 JSXstats只关心 chunk 维度chunks、chunkModules、chunkOrigins把模块细节关掉便于观察产物结构。值得注意的一个细节示例中的rules只include了src目录而src-b、src-c的组件也写成了 JSX——这说明该示例依赖 Babel 转换链路真实复用 JSX 远端组件时每个容器都要配置好自己源码的转译规则。uniqueName避免运行时命名冲突三个配置都显式设置了不同的output.uniqueNamemodule-federation-aaa/module-federation-bbb/module-federation-ccc。配置注释解释得很清楚webpack.config.jsEach build needs a unique name to avoid runtime collisions. The default uses name from package.json.uniqueName会渗入产物运行时 JSONP 全局数组名、data-webpack属性前缀都以它为标识。例如归档产物中可见const dataWebpackPrefix module-federation-aaa:;与self[webpackChunkmodule_federation_aaa]见 README.md 归档代码。多个容器同页共存时一旦重名就会互相覆盖 chunk 加载状态因此保持唯一是硬性要求。宿主 app如何声明 remotes 与 shared宿主构建只负责消费不需要name插件配置如下webpack.config.jsplugins: [ new ModuleFederationPlugin({ // List of remotes with URLs remotes: { mfe-b: mfeBBB/dist/bbb/mfeBBB.js, mfe-c: mfeCCC/dist/ccc/mfeCCC.js }, // list of shared modules with optional options shared: { react: { singleton: true // make sure only a single react module is used } } }) ]这里有两个核心语法需要吃透remote 的 URL 声明格式是容器名地址键mfe-b是你在业务代码里的模块请求前缀例如import ... from mfe-b/ComponentmfeBBB是远端容器暴露的全局变量名后面是该容器入口脚本的加载地址。地址可以是完整 URL也可以像示例一样使用相对路径由页面当前路径拼接解析。shared声明的是可共享模块把某个模块请求名如react放进shared构建时该容器就会同时扮演两种角色——提供者provide把自己package.json里的版本注册进共享作用域供其他容器取用消费者consume按自己package.json的dependencies或dev/peer/optionalDependencies中声明的版本区间去共享作用域里挑选版本。注释原文webpack.config.js指出它会在所有满足版本要求的候选里取最高可用版本同时把自己的版本提供给其他人。这正是 Module Federation 单一实例、版本协商的核心语义。react: { singleton: true }表示 React 必须全局只存在一份实例React 双实例会导致 hooks 状态错乱这是把 React 设为singleton的经典原因。运行时若发现宿主要求的版本与共享作用域中已被加载的版本不匹配会给出Unsatisfied version ...之类告警归档运行时中有对应实现见 README.md。远端容器 mfe-b 与 mfe-cexposes 与 shared 的差异打法容器 B只暴露一个组件但多共享一个 date-fnsnew ModuleFederationPlugin({ name: mfeBBB, // 容器在全局的变量名 exposes: { ./Component: ./src-b/Component }, // 对外暴露的模块 shared: [ date-fns, // 与另一个 remote 共享宿主根本不知道这回事 { react: { singleton: true } } // singleton 必须在每个配置里各自声明 ] })name决定容器产物挂在哪个全局变量下产物末尾会出现mfeBBB __webpack_exports__;。exposes的键./Component即业务代码里mfe-b/Component的后半段请求路径值指向容器内部真实的本地模块。容器 C暴露两个模块react 用 import: false 完全仰仗共享new ModuleFederationPlugin({ name: mfeCCC, exposes: { ./Component: ./src-c/Component, ./Component2: ./src-c/LazyComponent }, shared: [ lodash/, // lodash 下所有被用到的子请求都参与共享 date-fns, { react: { import: false, // 不打包自己的 react 副本 singleton: true } } ] })shared里的三个条目展示了三种形态date-fns字符串简写提供方按 package.json 版本注册消费方按依赖区间选取lodash/带尾斜杠的目录前缀表示lodash下所有实际被使用到的请求如lodash/random都会被纳入共享源码见 src-c/LazyComponent.js 中的import random from lodash/randomreact: { import: false, singleton: true }这是示例里最值得品味的一个配置。import: false意味着容器 C 完全不编译、不携带自己的 React 副本运行时必须从共享作用域拿到一个合法的 React。注释webpack.config.js给出了明确的利弊好处是构建更快无需编译该依赖代价是放弃了本地兜底与运行时自动升级的可能——一旦运行时共享作用域里没有可满足的版本C 中的 React 代码将无法工作。共享声明不必在宿主与每个容器间对称——例如 mfe-b 与 mfe-c 共享 date-fns宿主app完全不参与、也不知情但它们之间依然能在共享作用域里相遇并复用同一份模块。为什么入口要用 import()async boundary异步边界宿主入口 src/index.js 的注释是理解 Module Federation 运行模型的关键// Sharing modules requires that all remotes are initialized // and can provide shared modules to the common scope // As this is an async operation we need an async boundary (import()) // Using modules from remotes is also an async operation // as chunks need to be loaded for the code of the remote module // This also requires an async boundary (import()) // At this point shared modules initialized and remote modules are loaded import(./bootstrap);即存在两类必定是异步的操作共享作用域的初始化要消费共享模块必须先让所有远端容器初始化并把它们能提供的共享模块注册进公共 scope这是一个跨脚本的异步流程远端模块的获取import远端组件背后要按需拉取容器脚本或对应 chunk同样是异步流程。因此所有依赖共享模块或远端模块的代码必须放进一个由import()切出来的异步边界之后执行入口文件里、边界之外只能放那些不触碰共享/远端模块的纯引导逻辑注释明确说It cant use any of the shared modules or remote modules。这正是示例采用index.js → import(./bootstrap) → bootstrap.js三级结构的原因src/bootstrap.js 在此边界内部像平常一样使用共享的 React 并挂载应用import ReactDom from react-dom; import React from react; // - this is a shared module, but used as usual import App from ./App; const el document.createElement(main); ReactDom.render(App /, el); document.body.appendChild(el); // remove spinner document.body.removeChild(document.getElementsByClassName(spinner)[0]);从归档产物README.md可以看到这一结构最终被编译为入口模块调用__webpack_require__.e(src_bootstrap_js)加载 bootstrap chunk然后再真正执行 bootstrap 模块——异步边界在产物层面由显式的 chunk 加载体现。业务代码零感知远端模块当普通包用src/App.js 展示了消费侧体验——远端组件与共享模块在源码层面没有任何特殊写法只是普通的 importimport React from react; import ComponentB from mfe-b/Component; // - these are remote modules, import ComponentC from mfe-c/Component; // - but they are used as usual packages import { de } from date-fns/locale; // remote modules can also be used with import() which lazy loads them as usual const ComponentD React.lazy(() import(mfe-c/Component2)); const App () ( article headerh1Hello World/h1/header pThis component is from a remote container:/p ComponentB locale{de} / pAnd this component is from another remote container:/p ComponentC locale{de} / React.Suspense fallback{pLazy loading component.../p} pAnd this component is from this remote container too, but lazy loaded:/p ComponentD / /React.Suspense /article ); export default App;要点有三个mfe-b/Component、mfe-c/Component的命名空间正是宿主remotes的键 容器exposes的键拼接而成远端模块同样支持import()动态加载这里用React.lazySuspense实现了对mfe-c/Component2的运行时懒加载产物中对应独立 chunk 名webpack_container_remote_mfe-c_Component2见 README.mdde德语 locale来自date-fns/localedate-fns 属于共享模块但使用它的宿主代码无需关心解析发生在本地还是远端。容器内部源码src-b/Component.js的注释进一步点明一个互补特性暴露模块本身就是一个异步边界——容器里写被 exposes 的组件不需要像宿主入口那样额外套import()因为模块在get时已被异步获取同时若组件用到 date-fns 这类共享模块webpack 会把 date-fns 拆到独立文件与组件代码并行加载从而缩短远端组件可用的等待时间。页面组装index.html 的脚本编排宿主页面 index.html 只做最小编排其body结构为!-- A spinner -- div classspinner/div !-- This script only contains bootstrapping logic -- !-- It will load all other scripts if necessary -- script src/dist/aaa/app.js async/script !-- These script tags are optional -- !-- They improve loading performance -- !-- Omitting them will add an additional round trip -- script src/dist/bbb/mfeBBB.js async/script script src/dist/ccc/mfeCCC.js async/script完整的 spinner CSS 动画样式在页面head中见 index.html此处从略。页面设计传递了三条工程经验先放一个 CSS 转圈动画应用就绪后由 bootstrap.js 移除它——给异步加载过程一个明确的就绪信号唯一必需的脚本只有app.js它只含引导逻辑host 运行时会按需自行加载 remote 脚本与 chunk两个 remote 的script标签只是可选的性能优化预先加载它们能省掉后续的发现并拉取往返an additional round trip。注释还指出这些脚本都很小约 5KB追求极致时可以内联。源码侧验证ModuleFederationPlugin 是三层插件的门面看完示例用法再看它在 webpack 里的实现就一目了然。lib/container/ModuleFederationPlugin.js 本体非常薄apply()主要做两件事在compiler.hooks.validate上注册 schema 校验校验规则来自 schemas/plugins/container/ModuleFederationPlugin.json对应生成的.check文件在 schemas/plugins 下在compiler.hooks.afterPlugins中把配置按字段拆分转发给三个底层插件源码第 69-104 行if (options.exposes ...length 0) { new ContainerPlugin({ name, library, filename, runtime, shareScope, exposes }).apply(compiler); } if (options.remotes ...length 0) { new ContainerReferencePlugin({ remoteType, shareScope, remotes }).apply(compiler); } if (options.shared) { new SharePlugin({ shared, shareScope }).apply(compiler); } new HoistContainerReferences().apply(compiler);对应关系如下ModuleFederationPlugin 配置项底层落地插件作用exposesname/library/filename/runtimelib/container/ContainerPlugin.js把当前构建变成可被引用的容器生成容器入口模块remoteslib/container/ContainerReferencePlugin.js把remotes声明的远端解析为外部引用并在 chunk 需要时触发加载sharedlib/sharing 下的SharePlugin生成提供共享模块 消费共享模块两套运行时逻辑—恒生效lib/container/HoistContainerReferencesPlugin.js将容器引用提升避免不必要的重复加载/初始化未显式配置时library默认取{ type: var, name: options.name }见 ModuleFederationPlugin.js这与容器名即全局变量名的现象完全一致。目录 lib/container 下还有 ContainerEntryModule.js、RemoteRuntimeModule.js 等模块分别负责容器入口的编译与远端加载运行时函数的生成有兴趣可按名字继续深挖。从归档产物读懂运行时协作示例 README 顺带归档了三个 bundle 的完整源码并逐段注解见 README.md 的# dist/aaa/app.js、# dist/bbb/mfeBBB.js、# dist/ccc/mfeCCC.js三节它们是最直观的运行时教材。挑三处最关键的现象说明1. remote 在宿主侧被编译为脚本外部模块 动态解析宿主产物中每个 remote 对应一个以external mfeBBB/dist/bbb/mfeBBB.js为标题的模块README.md。其逻辑是若全局mfeBBB已存在则直接 resolve否则用__webpack_require__.l注入script加载该地址加载失败则抛出带ScriptExternalLoadError语义的错误对象。这正是页面可省略 remote 标签、由运行时补拉的代码来源。2. 运行时通过映射表按 chunk 懒加载远端模块宿主产物中__webpack_require__.f.remotesREADME.md维护两张映射chunkMapping哪个业务 chunk 依赖哪些远端模块 id如src_bootstrap_js需要 id 8 与 10webpack_container_remote_mfe-c_Component2需要 id 26idToExternalAndNameMapping把模块 id 映射到[default, ./Component, 外部模块id]三元组即共享作用域名 default 远端暴露名 ./Component 对应 external。获取流程为require(external) → container.init(共享scope) → container.get(暴露名, getScope) → 返回工厂函数。getScope通过__webpack_require__.R在 get 调用期间暂存用于让容器在执行get时能把自己的共享条目挂进正确的作用域产物第 1009-1020 行可见moduleMap与get/init的标准实现。3. 共享模块按版本 加载策略注册与消费三个 bundle 的共享运行时都会调用register(name, version, factory, eager)向共享作用域default写入自己提供的版本。归档产物里可见README.mdregister(react, 19.2.8, () (__webpack_require__.e(vendors-node_modules_react_index_js).then(() () (__webpack_require__(/*! react */ 24)))));宿主随后调用initExternal(id)对每个 remote 执行module.init(共享scope)让远端把各自的date-fns4.4.0、react19.2.8注册进来——这就是共享作用域必须异步初始化的产物形态。消费侧生成的moduleToHandlerMapping如loadSingletonVersion(default,react,false,[1,19,2,8], fallback)见 README.md会按不同策略取版本产物中的版本解析、rangeToString、satisfy等函数均内联自 lib/util/semver.js失败/告警文案No satisfying version ... found in shared scope等也直接体现在运行时代码里README.md。若某个共享版本最终不可用useFallback逻辑会回退到消费方自带的本地副本——也就是shared中import默认开启所提供的兜底能力而示例里容器 C 的import: false恰恰主动放弃了这个兜底。shared 配置要点速查与常见误区结合示例用法与运行时策略整理成下表均以本示例实际呈现的语义为准写法/参数语义示例中的呈现date-fns字符串简写提供自己的 package.json 版本并按依赖区间消费共享版本mfe-b、mfe-c 均共享 date-fnslodash/目录前缀该包下所有被用到的子请求整体共享lodash/random见 LazyComponent.js{ react: { singleton: true } }强制全局单实例须在每个相关容器各自配置代码注释明确强调app、mfe-b、mfe-c 都写了import: false不自带/不编译该依赖副本运行时必须有合法共享版本mfe-c 的 react默认import消费失败可回退到本地副本app、mfe-b 的 reactshareScope默认default共享作用域名字产物中以default命名空间注册产物switch(name){ case default: ...}name/library容器挂载的全局变量名默认{type:var,name}mfeBBB、mfeCCCuniqueName产物运行时命名空间避免多容器冲突module-federation-aaa/bbb/ccc几个高频坑也都在注释与产物中得到印证忘写 singletonReact 类库一旦在页面存在两份会产生难以排查的运行时问题所以singleton: true必须写进每个参与方入口直接 import 共享/远端模块初始 chunk 里同步引用共享或远端模块会破坏异步边界必须退一层用import()引导对应 src/index.js 的注释被exposes的模块自带异步边界容器侧无需再套import()对应 src-b/Component.js 的注释uniqueName 重复多个容器产物会互相覆盖 chunk 全局状态导致加载静默失败。如何在本地构建与运行本示例该示例属于 webpack 官方 examples 测试体系的一部分。按 examples/README.md 中 Building an Example 一节第 170-177 行的通用流程在仓库根目录先yarn安装依赖再执行yarn setup完成测试环境的初始化如需使用 CLI可在根目录安装webpack-cli进入具体示例目录执行构建一次构建全部示例可运行npm run build:examples。需要注意本示例并非开箱即跑的独立脚手架它依赖babel-loaderbabel/react处理 JSX并在源码中引用了react、react-dom、date-fns、lodash等真实包见 webpack.config.js 与 examples/module-federation 下的业务源码运行前需确保这些依赖就绪。另外test.filter.js 显示本组用例在测试框架Jest vm、Deno、Bun下对运行时环境有明确要求它加载 ESM-only 的babel/core8需要 Node^22.18 || 24.11环境、且在 Deno/Bun 上会被跳过——实际使用前请先核对本机 Node 版本。构建完成后可用任意静态服务器托管dist目录并打开页面根路径的index.html产物中publicPath为相对路径如dist/aaa/从根路径访问即可正确解析。【免费下载链接】webpackA bundler for javascript and friends. Packs many modules into a few bundled assets. Code Splitting allows for loading parts of the application on demand. Through loaders, modules can be CommonJs, AMD, ES6 modules, CSS, Images, JSON, Coffeescript, LESS, ... and your custom stuff.项目地址: https://gitcode.com/GitHub_Trending/web/webpack创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →