Webpack中的模块联邦
1.简介
模块联邦可以真正实现跨应用间模块共享。它主要是去中心化的思想;npm组件库是中心化的思想。
模块联邦是Webpack5
新内置的一个重要功能,可以让跨应用间真正做到模块共享模块联邦本身是一个普通的Webpack
插件 ModuleFederationPlugin
。
2.什么是模块联邦
多个独立的构建可以组成一个应用程序,这些独立的构建之间不应该存在依赖关系,因此可以单独开发和部署它们。这通常被称作微前端,但并不仅限于此。
Webpack5 模块联邦让Webpack达到了线上Runtime的效果,让代码直接在项目间利用CDN直接共享,不再需要本地安装 Npm 包、构建再发布了!
我们知道 Webpack可以通过DLL或者Externals做代码共享时Common Chunk,但不同应用和项目间这个任务就变得困难了,我们几乎无法在项目之间做到按需热插拔。
-
NPM 方式共享模块
想象一下正常的共享模块方式,对,就是 NPM。如下图所示,正常的代码共享需要将依赖作为Lib 安装到项目,进行Webpack打包。构建再上线,如下图:
对于项目 Home 与 Search,需要共享一个模块时,最常见的办法就是将其抽成通用依赖并分别安装在各自项目中。虽然 Monorepo 可以一定程度解决重复安装和修改困难的问题,但依然需要走本地编译。 -
UMD 方式共享模块
真正 Runtime的方式可能是UMD方式共享代码模块,即将模块用Webpack UMD模式打包,并输出到其他项目中。这是非常普遍的模块共享方式:
对于项目Home与Search,直接利用UMD包复用一个模块。但这种技术方案问题。也很明显,就是包体积无法达到本地编译时的优化效果,且库之间容易冲突。 -
微前端方式共享模块
微前端:micro-frontends (MFE) 也是最近比较火的模块共享管理方式,微前端就是要解决多项目并存问题,多项目并存的最大问题就是模块共享,不能有冲突。
由于微前端还要考虑样式冲突、生命周期管理,所以本文只聚焦在资源加载方式上。微前端一般有两种打包方式:- 子应用独立打包,模块更解耦,但无法抽取公共依赖等。
- 整体应用一起打包,很好解决上面的问题,但打包速度实在是太慢了,不具备水平扩展能力。
-
模块联邦方式
终于提到本文的主角了,作为 Webpack5 内置核心特性之一的 Federated Module:
从图中可以看到,这个方案是直接将一个应用的包应用于另一个应用,同时具备整体应用一起打包的公共依赖抽取能力。
3.参数
name
当前应用名称,需要全局唯一。remotes
可以将其他项目的name
映射到当前项目中。exposes
表示导出的模块,只有在此申明的模块才可以作为远程依赖被使用。shared
是非常重要的参数,制定了这个参数,可以让远程加载的模块对应依赖改为使用本地项目的React
或ReactDOM
。
4.示例
本示例模拟三个应用,Nav
、Search
和Home
。每个应用都是独立的,又通过模块联邦联系在了一块。
三个应用都要安装一些插件
npm init -y
npm webpack webpack-cli webpack-dev-server html-webpack-plugin -D
三个应用的目录结构如下:
4.1 nav导航应用
src/Header.js:
//用原生模拟一个组件。这里没用框架,也可以用框架写一个组件
const Header = () => {
const header = document.createElement('h1')
header.textContent = '公共头部内容'
return header
}
export default Header
src/index.js:
//在index.js中测试一下
import Header from './Header'
const div = document.createElement('div')
div.appendChild(Header())
document.body.appendChild(div)
webpack.config.js
const HtmlWebpackPlugin = require("html-webpack-plugin");
const { ModuleFederationPlugin } = require("webpack").container;
module.exports = {
mode: "production",
entry: "./src/index.js",
plugins: [
new HtmlWebpackPlugin(),
new ModuleFederationPlugin({
// 模块联邦名字,其它项目引入时候要与此保持一致
name: "nav",
// 外部访问的资源名字,其它项目引入时候要与此保持一致
filename: "remoteEntry.js",
// 引用的外部资源列表
remotes: {},
// 暴露给外部资源列表
exposes: {
"./Header": "./src/Header.js",
},
// 共享模块,如lodash
shared: {},
}),
],
};
应用 webpack 运行服务:
4.2 home首页
src/HomeList.js:
const HomeList = (num) => {
let str = '<ul>'
for (let i = 0; i < num; i++) {
str += '<li>item ' + i + '</li>'
}
str += '</ul>'
return str
}
export default HomeList
src/index.js:
import HomeList from './HomeList'
//使用异步方式引入其它项目的组件
import('nav/Header').then((Header) => {
const body = document.createElement('div')
body.appendChild(Header.default())
document.body.appendChild(body)
document.body.innerHTML += HomeList(5)
})
webpack.config.js
const HtmlWebpackPlugin = require("html-webpack-plugin");
const { ModuleFederationPlugin } = require("webpack").container;
module.exports = {
mode: "production",
entry: "./src/index.js",
plugins: [
new HtmlWebpackPlugin(),
new ModuleFederationPlugin({
name: "home",
filename: "remoteEntry.js",
remotes: {
nav: "nav@http://localhost:3003/remoteEntry.js",
},
exposes: {
"./HomeList": "./src/HomeList.js",
},
shared: {},
}),
],
};
应用 webpack 运行服务:
4.3 search搜索
src/index.js:
Promise.all([import('nav/Header'), import('home/HomeList')])
.then(([{
default: Header
}, {
default: HomeList
}]) => {
document.body.appendChild(Header())
document.body.innerHTML += HomeList(4)
})
webpack.config.js
const HtmlWebpackPlugin = require("html-webpack-plugin");
const { ModuleFederationPlugin } = require("webpack").container;
module.exports = {
mode: "production",
entry: "./src/index.js",
plugins: [
new HtmlWebpackPlugin(),
new ModuleFederationPlugin({
name: "search",
filename: "remoteEntry.js",
remotes: {
nav: "nav@http://localhost:3003/remoteEntry.js",
home: "home@http://localhost:3001/remoteEntry.js"
},
exposes: {},
shared: {},
}),
],
};
应用 webpack 运行服务: