## 1 引言

先说结论：Webpack5 模块联邦让 Webpack 达到了线上 Runtime 的效果，让代码直接在项目间利用 CDN 直接共享，不再需要本地安装 Npm 包、构建再发布了！

我们知道 Webpack 可以通过 DLL 或者 Externals 做代码共享时 Common Chunk，但不同应用和项目间这个任务就变得困难了，我们几乎无法在项目之间做到按需热插拔。

模块联邦是 Webpack5 新内置的一个重要功能，可以让跨应用间真正做到模块共享，所以这周让我们通过 [webpack-5-module-federation-a-game-changer-in-javascript-architecture](https://indepth.dev/webpack-5-module-federation-a-game-changer-in-javascript-architecture/#its-important-to-note-these-are-special-entry-points-they-are-only-a-few-kb-in-size-containing-a-special-webpack-runtime-that-can-interface-with-the-host-it-is-not-a-standard-entry-point--7/) 这篇文章了解什么是 “模块联邦” 功能。

## 2 概述 & 精读

### NPM 方式共享模块

想象一下正常的共享模块方式，对，就是 NPM。

如下图所示，正常的代码共享需要将依赖作为 Lib 安装到项目，进行 Webpack 打包构建再上线，如下图：

<img width=500 src="https://img.alicdn.com/tfs/TB1MoLPy.z1gK0jSZLeXXb9kVXa-2494-1478.png">

对于项目 Home 与 Search，需要共享一个模块时，最常见的办法就是将其抽成通用依赖并分别安装在各自项目中。

虽然 Monorepo 可以一定程度解决重复安装和修改困难的问题，但依然需要走本地编译。

### UMD 方式共享模块

真正 Runtime 的方式可能是 UMD 方式共享代码模块，即将模块用 Webpack UMD 模式打包，并输出到其他项目中。这是非常普遍的模块共享方式：

<img width=500 src="https://img.alicdn.com/tfs/TB1rQnSy4n1gK0jSZKPXXXvUXXa-2404-1484.png">

对于项目 Home 与 Search，直接利用 UMD 包复用一个模块。但这种技术方案问题也很明显，就是包体积无法达到本地编译时的优化效果，且库之间容易冲突。

### 微前端方式共享模块

微前端：micro-frontends (MFE) 也是最近比较火的模块共享管理方式，微前端就是要解决多项目并存问题，多项目并存的最大问题就是模块共享，不能有冲突。

<img width=500 src="https://img.alicdn.com/tfs/TB1vqvTy1T2gK0jSZFvXXXnFXXa-2410-1520.png">

由于微前端还要考虑样式冲突、生命周期管理，所以本文只聚焦在资源加载方式上。微前端一般有两种打包方式：

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

### 模块联邦方式

终于提到本文的主角了，作为 Webpack5 内置核心特性之一的 Federated Module：

<img width=500 src="https://img.alicdn.com/tfs/TB1qLz1yYj1gK0jSZFuXXcrHpXa-2414-1474.png">

从图中可以看到，这个方案是直接将一个应用的包应用于另一个应用，同时具备整体应用一起打包的公共依赖抽取能力。

让应用具备模块化输出能力，其实开辟了一种新的应用形态，即 “中心应用”，这个中心应用用于在线动态分发 Runtime 子模块，并不直接提供给用户使用：

<img width=300 src="https://img.alicdn.com/tfs/TB1ymbWy7Y2gK0jSZFgXXc5OFXa-1346-1442.png">

对微前端而言，这张图就是一个完美的主应用，因为所有子应用都可以利用 Runtime 方式复用主应用的 Npm 包和模块，更好的集成到主应用中。

模块联邦的使用方式如下：

```js
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`，插件有几个重要参数：

1. `name` 当前应用名称，需要全局唯一。
2. `remotes` 可以将其他项目的 `name` 映射到当前项目中。
3. `exposes` 表示导出的模块，只有在此申明的模块才可以作为远程依赖被使用。
4. `shared` 是非常重要的参数，制定了这个参数，可以让远程加载的模块对应依赖改为使用本地项目的 React 或 ReactDOM。

比如设置了 `remotes: { app_two: "app_two_remote" }`，在代码中就可以直接利用以下方式直接从对方应用调用模块：

```js
import { Search } from "app_two/Search";
```

这个 `app_two/Search` 来自于 `app_two` 的配置：

```js
// app_two 的 webpack 配置
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]` 这个模块，这个模块对于被引用应用来说是一个本地模块。

## 3 总结

模块联邦为更大型的前端应用提供了开箱解决方案，并已经作为 Webpack5 官方模块内置，可以说是继 Externals 后最终的运行时代码复用解决方案。

另外 Webpack5 还内置了大量编译时缓存功能，可以看到，无论是性能还是多项目组织，Webpack5 都在尝试给出自己的最佳思路，期待 Webpack5 正式发布，前端工程化会迈向一个新的阶段。

> 讨论地址是：[精读《Webpack5 新特性 - 模块联邦》 · Issue #239 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/239)

**如果你想参与讨论，请 [点击这里](https://github.com/dt-fe/weekly)，每周都有新的主题，周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**

> 关注 **前端精读微信公众号**

<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">

> 版权声明：自由转载-非商用-非衍生-保持署名（[创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh)）
