Appearance
Webpack 模块联邦
Webpack5 模块联邦让 Webpack
达到了线上 Runtime
的效果,让代码直接在项目间利用 CDN
直接共享,不再需要本地安装 Npm
包、构建再发布了!
我们知道 Webpack
可以通过 DLL
或者 Externals
做代码共享时 Common Chunk
,但不同应用和项目间这个任务就变得困难了,我们几乎无法在项目之间做到按需热插拔。
模块联邦是 Webpack5
新内置的一个重要功能,可以让跨应用间真正做到模块共享。为更大型的前端应用提供了开箱解决方案,并已经作为 Webpack5
官方模块内置,可以说是继 Externals
后最终的运行时代码复用解决方案。
概述
npm 方式共享模块
正常的共享模块方式就是 NPM
正常的代码共享需要将依赖作为 Lib 安装到项目,进行 Webpack 打包构建再上线。
在两个项目,需要共享一个模块时,最常见的办法就是将其抽成通用依赖并分别安装在各自项目中。
虽然 Monorepo 可以一定程度解决重复安装和修改困难的问题,但依然需要走本地编译。
UMD 方式共享模块
真正 Runtime 的方式可能是 UMD 方式共享代码模块,即将模块用 Webpack UMD 模式打包,并输出到其他项目中。这是非常普遍的模块共享方式
在两个项目中,直接利用 UMD 包复用一个模块。但这种技术方案问题也很明显,就是包体积无法达到本地编译时的优化效果,且库之间容易冲突。
微前端方式共享模块
微前端:micro-frontends
(MFE) 也是最近比较火的模块共享管理方式,微前端就是要解决多项目并存问题,多项目并存的最大问题就是模块共享,不能有冲突。
由于微前端还要考虑样式冲突、生命周期管理,所以本文只聚焦在资源加载方式上。微前端一般有两种打包方式:
- 子应用独立打包,模块更解耦,但无法抽取公共依赖等。
- 整体应用一起打包,很好解决上面的问题,但打包速度实在是太慢了,不具备水平扩展能力。
模块联邦方式
这个方案是直接将一个应用的包应用于另一个应用,同时具备整体应用一起打包的公共依赖抽取能力。比如有两个应用分别名为application
(宿主),appMenus
(远程),目的: application
远程获取到 appMenus
,并且实现 appMenus
升级, application
加载最新的 appMenus
。
让应用具备模块化输出能力,其实开辟了一种新的应用形态,即 “中心应用”,这个中心应用用于在线动态分发 Runtime
子模块,并不直接提供给用户使用:
对微前端而言,这张图就是一个完美的主应用,因为所有子应用都可以利用 Runtime 方式复用主应用的 Npm 包和模块,更好的集成到主应用中。
模块联邦的使用方式如下:
javascript
const HtmlWebpackPlugin = require("html-webpack-plugin");
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");
module.exports = {
// other webpack configs...
plugins: [
new ModuleFederationPlugin({
name: "app_one_remote",
remotes: {
app_two: "app_two_remote",
app_three: "app_three_remote"
},
exposes: {
AppContainer: "./src/App"
},
shared: ["react", "react-dom", "react-router-dom"]
}),
new HtmlWebpackPlugin({
template: "./public/index.html",
chunks: ["main"]
})
]
};
模块联邦本身是一个普通的 Webpack 插件 ModuleFederationPlugin
,插件有几个重要参数:
name
当前应用名称(别名),需要全局唯一。filename
入口文件名remotes
可以将其他项目的name
映射到当前项目中。 shared 要想生效,则 host 应用和 remote 应用的 shared 配置的依赖要一致。exposes
表示导出的模块,只有在此申明的模块才可以作为远程依赖被使用。shared
是非常重要的参数,指定了这个参数,可以让远程加载的模块对应依赖改为使用本地项目的 React 或 ReactDOM。
比如设置了 remotes: { app_two: "app_two_remote" }
,在代码中就可以直接利用以下方式直接从对方应用调用模块:
javascript
import { Search } from "app_two/Search";
这个 app_two/Search
来自于 app_two
的配置:
javascript
export default {
plugins: [
new ModuleFederationPlugin({
name: "app_two",
library: { type: "var", name: "app_two" },
filename: "remoteEntry.js",
exposes: {
Search: "./src/Search"
},
shared: ["react", "react-dom"]
})
]
};
正是因为 Search
在 exposes
被导出,我们因此可以使用 [name]/[exposes_name]
这个模块,这个模块对于被引用应用来说是一个本地模块。