webpack 5 模块联邦
官方是这么解释模块联邦的:多个独立的构建可以组成一个应用程序,这些独立的构建之间不应该存在依赖关系,因此可以单独开发和部署它们。通常称为微前端,但不仅于此。
🧐🧐🧐 或许打个比方,在此之前在多个项目之间我们是如何共享一个模块 或 组件的?
- 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-dom
npm i -D webpack webpack-cli webpack-dev-server html-webpack-plugin babel-loader @babel/preset-env @babel/preset-react @babel/core
⚠️ 注意:webpack 必须要是 5版本及以上
📋 配置
// app-a/src/app.jsx
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-a/src/app.js
// 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-a/src/components/Example.jsx
// app-b 一样,在 Example 和 Example2 随便写个组件就好了
import React from 'react'
export default function Example() {
return (
<h1>我是A应用的一个组件-example1</h1>
)
}
// app-a/src/index.jsx
// 为什么要设置这个文件呢?
// 大致的理由是 由于被共享的组件需要被先加载进来。但 webpack 在构建打包时,并不知道 remotes 里哪些组件是被使用到的。于是建议在入口里动态加载我们的程序。
// 详情可以查阅这篇 https://webpack.docschina.org/concepts/module-federation/#Uncaught-Error-Shared-module-is-not-available-for-eager-consumption
import('./src/app.js')
重点来了!!!
// app-a/webpack.config.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。