webpack5 模块联邦
官方是这么解释模块联邦的:多个独立的构建可以组成一个应用程序,这些独立的构建之间不应该存在依赖关系,因此可以单独开发和部署它们。通常称为微前端,但不仅于此。
🧐🧐🧐 或许打个比方,在此之前在多个项目之间我们是如何共享一个模块 或 组件的?
- copy,缺点:每次修改都需要cv到依赖项目中。
- 将公共模块 打包成 npm 包,缺点:如果包更新,依赖的项目需要更新依赖包且重新构建上线。
- 以 UMD 方式共享,即直接使用线上的 cdn 链接,缺点:可能会存在库之间的冲突,无法最大优化编译打包。
因此这个东西诞生了🥳🥳🥳

所有子应用都可以利用 Runtime 方式复用主应用的 Npm 包和模块,让应用具备模块化输出能力,开辟了一种新的应用形态,即 “中心应用”,这个中心应用用于在线动态分发 Runtime 子模块,并不直接提供给用户使用。
案例演示
📁 首先新建两个应用,分别是 app-a
和 app-b
,目录如下👇
├─ app-*│ ├─ src│ │ ├─ components│ │ │ └─ Example.jsx│ │ ││ │ ├─ app.js│ │ └─ app.jsx| ││ ├─ index.js│ ├─ package.json│ ├─ webpack.config.js
🔃 安装依赖
npm i -S react react-domnpm i -D webpack webpack-cli webpack-dev-server html-webpack-plugin babel-loader @babel/preset-env @babel/preset-react @babel/core
⚠️ 注意:webpack 必须要是 5版本及以上
📋 配置
import React from 'react'
// 引用 app_b 导出的 Example 组件和 Example2 组件const Example1 = React.lazy(() => import('app_b/Example'))const Example2 = React.lazy(() => import('app_b/Example2'))
const App = () => { return ( <div> <p>this is application</p> <Example1 /> <Example2 /> </div> )}
export default App
// app-b 一样import React from 'react'import { createRoot } from 'react-dom/client'import App from './app.jsx'const root = createRoot(document.getElementById('app'))root.render(<App />)
// app-b 一样,在 Example 和 Example2 随便写个组件就好了import React from 'react'
export default function Example() { return ( <h1>我是A应用的一个组件-example1</h1> )}
// 为什么要设置这个文件呢?// 大致的理由是 由于被共享的组件需要被先加载进来。但 webpack 在构建打包时,并不知道 remotes 里哪些组件是被使用到的。于是建议在入口里动态加载我们的程序。// 详情可以查阅这篇 https://webpack.docschina.org/concepts/module-federation/#Uncaught-Error-Shared-module-is-not-available-for-eager-consumptionimport('./src/app.js')
重点来了!!!
const path = require('path')const webpack = require('webpack')const HtmlWebpackPlugin = require('html-webpack-plugin')
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin')const { dependencies } = require('./package.json')
module.exports = { mode: 'development', entry: './index.js', output: { path: path.resolve(__dirname, 'dist'), filename: '[name].bundle.js', }, resolve: { extensions: ['.js', '.jsx', 'json'], }, module: { rules: [ { test: /\.(js|jsx)$/, loader: 'babel-loader', options: { presets: ['@babel/env', '@babel/preset-react'], }, }, ], }, plugins: [ // 使用 模块联邦插件 new ModuleFederationPlugin({ name: 'app_a', // 当前的应用名,唯一 // 将 模块以 UMD 方式打包,name 为 使用的模块名,类似 jquery 的 $ // library: { // type: 'var', // name: 'app_a', // }, // 入口文件名称,用于给对外模块使用时的入口文件名 filename: 'remoteEntry.js', // 选择导出的组件,供外部使用 exposes: { './Example': './src/components/Example', }, // 依赖的远程模块 remotes: { app_b: 'app_b@http://localhost:8082/remoteEntry.js', }, // 共享的第三方库,可以让远程加载的模块对应依赖改为使用本地项目的 React 或 ReactDOM shared: { ...dependencies, react: { singleton: true, // 只允许用单个版本 requiredVersion: dependencies['react'], // 版本 }, 'react-dom': { singleton: true, requiredVersion: dependencies['react-dom'], }, }, }), new HtmlWebpackPlugin({ template: './public/index.html', }), // 热加载 new webpack.HotModuleReplacementPlugin(), ], devServer: { hot: true, },}
app-b
的 webpack.config.js
差不多,只是修改一下 exposes
和 remotes
即可。
现在就完成了 app-a
共享 Example
组件,同时使用远程模块 app-b
的 Example
和 Example2
组件。

🖥️ 运行一下看看

可以清楚看到 remoteEntry.js
在 src_app_js.bundle.js
前加载。
其中 src_components_Example.bundle.js
和 src_components_Example2.bundle.js
是来自 远程模块 app_b
exposes 的。
vendors-node_modules_react_index_js.bundle.js
和 vendors-node_modules_react-dom_index_js.bundle.js
是来自 远程模块 app_b
shared 的。
📦 完整代码
至此,一个简单的 模块联邦 小demo完成,实现了 共享组件功能。
后记
如果有使用过一些 微前端 的解决方案,比如 阿里的 qiankun,你会发现与 webpack 的模块联邦给出的微前端解决方案很类似。有兴趣的可以使用 它 来实现一个简单版的 qiankun。