什么是模块联邦

多个独立的构建可以组成一个应用程序,这些独立的构建之间不应该存在依赖关系,因此可以单独开发和部署它们。
这通常被称作微前端,但并不仅限于此。
Webpack5 模块联邦让 Webpack 达到了线上 Runtime 的效果,让代码直接在项目间利用 CDN直接共享,不再需要本地安装 Npm 包、构建再发布了
我们知道 Webpack 可以通过 DLL 或者 Externals 做代码共享时 Common Chunk,但不同应用和项目间这个任务就变得困难了,我们几乎无法在项目之间做到按需热插拔。


NPM 方式共享模块

想象一下正常的共享模块方式,对,就是 NPM
如下图所示,正常的代码共享需要将依赖作为 Lib安装到项目,进行 Webpack 打包构建再上线,如下图:
image.png
对于项目 Home 与 Search,需要共享一个模块时,最常见的办法就是将其抽成通用依赖并分别安装在各自项目中。
虽然 Monorepo 可以一定程度解决重复安装和修改困难的问题,但依然需要走本地编译。

UMD 方式共享模块

真正 Runtime 的方式可能是 UMD方式共享代码模块,即将模块用 Webpack UMD模式打包,并输出到其他项目中。这是非常普遍的模块共享方式:
image.png
对于项目 Home 与 Search,直接利用 UMD包复用一个模块。但这种技术方案问题也很明显,就是包体积无法达到本地编译时的优化效果,且库之间容易冲突


微前端方式共享模块

微前端:micro-frontends (MFE)也是最近比较火的模块共享管理方式,微前端就是要解决多项目并存问题,多项目并存的最大问题就是模块共享,不能有冲突。
image.png
由于微前端还要考虑样式冲突、生命周期管理,所以本文只聚焦在资源加载方式上
微前端一般有两种打包方式:

  1. 子应用独立打包,模块更解耦,但无法抽取公共依赖等。
  2. 整体应用一起打包,很好解决上面的问题,但打包速度实在是太慢了,不具备水平扩展能力。

模块联邦方式

终于提到本文的主角了,作为 Webpack5 内置核心特性之一的 Federated Module
image.png
从图中可以看到,这个方案是直接将一个应用的包应用于另一个应用,同时具备整体应用一起打包的公共依赖抽取能力。

应用案例

本案例模拟三个应用: Nav 、 Search 及 Home 。每个应用都是独立的,又通过模块 联邦联系到了一起。

Nav 导航

src/header.js

  1. const Header = () => {
  2. const header = document.createElement('h1')
  3. header.textContent = '公共头部内容'
  4. return header
  5. }
  6. export default Header

webpack.config.js

  1. const HtmlWebpackPlugin = require('html-webpack-plugin')
  2. const { ModuleFederationPlugin } = require('webpack').container
  3. module.exports = {
  4. mode: 'production',
  5. entry: './src/index.js',
  6. plugins: [
  7. new HtmlWebpackPlugin(),
  8. new ModuleFederationPlugin({
  9. //模块联邦名字
  10. name: 'nav',
  11. //外部访问的资源名字
  12. filename: 'remoteEntry.js',
  13. //引用的外部资源列表
  14. remotes: {},
  15. //暴露给外部资源列表
  16. exposes: {
  17. './Header': './src/Header.js'
  18. },
  19. //共享模块,如lodash
  20. shared: {}
  21. })
  22. ]
  23. }

npx webpack serve --port 3003

Home 导航

src/header.js

  1. const HomeList = (num) => {
  2. let str = '<ul>'
  3. for (let i = 0; i < num; i++) {
  4. str += '<li>item ' + i + '</li>'
  5. }
  6. str += '</ul>'
  7. return str
  8. }
  9. export default HomeList

webpack.config.js

  1. const HtmlWebpackPlugin = require('html-webpack-plugin')
  2. const { ModuleFederationPlugin } = require('webpack').container
  3. module.exports = {
  4. mode: 'production',
  5. entry: './src/index.js',
  6. plugins: [
  7. new HtmlWebpackPlugin(),
  8. new ModuleFederationPlugin({
  9. name: 'home',
  10. filename: 'remoteEntry.js',
  11. remotes: {},
  12. exposes: {
  13. './HomeList': './src/HomeList.js'
  14. },
  15. shared: {}
  16. })
  17. ]
  18. }

npx webpack serve --port 3001

Search 导航

src/header.js

  1. //nav: 引用的外部资源列表 remotes:{'nav@http://localhost:3003/remoteEntry.js'}
  2. //Header: 外部资源nav暴露给外部资源列表 exposes: {'./Header': './src/Header.js'}
  3. Promise.all([import('nav/Header'), import('home/HomeList')])
  4. .then(([ //数组解析
  5. {
  6. default: Header
  7. },
  8. {
  9. default: HomeList
  10. }
  11. ]) => {
  12. document.body.appendChild(Header())
  13. document.body.innerHTML += HomeList(3)
  14. })

webpack.config.js

  1. const HtmlWebpackPlugin = require('html-webpack-plugin')
  2. const { ModuleFederationPlugin } = require('webpack').container
  3. module.exports = {
  4. mode: 'production',
  5. entry: './src/index.js',
  6. plugins: [
  7. new HtmlWebpackPlugin(),
  8. new ModuleFederationPlugin({
  9. name: 'search',
  10. //外部访问的资源名字
  11. filename: 'remoteEntry.js',
  12. //引用的外部资源列表
  13. //nav模块联邦名字
  14. //http://localhost:3003 外部资源服务器地址
  15. //remoteEntry.js 外部访问的资源名字
  16. remotes: {
  17. nav: 'nav@http://localhost:3003/remoteEntry.js',
  18. home: 'home@http://localhost:3001/remoteEntry.js'
  19. }
  20. })
  21. ]
  22. }

npx webpack serve --port 3002
image.png