# Webpack 5 Module Federation: JavaScript 架构的变革者
Module Federation [ˌfedəˈreɪʃn] 使 JavaScript 应用得以在客户端或服务器上动态运行另一个 bundle 或者 build 的代码。
原文:Webpack 5 Module Federation: A game-changer in JavaScript architecture
Module Federation 之于 JavaScript Bundler,如同 Apollo 之于 GraphQL。
在独立的应用之间共享代码的可伸缩解决方案从未如此方便,而且几乎不可能大规模实现。此前我们拥有的最成熟方案是 externals 或 DLLPlugin,它们强制把依赖集中于一个外部文件中。共享代码真的很麻烦,因为很多应用看似独立,却不是真正意义上的独立,可共享的依赖非常有限。此外,在单独打包的 Web 应用之间共享实际的特性代码往往并不可行、无效、且毫无益处。
# 那么什么是 Module Federation?
这是我发明并且赋予它最初形态的 JavaScript 架构,在我的联合创作者兼 Webpack 创始人的帮助下,它成为了 Webpack 5 Core 中最令人兴奋的功能之一(Webpack 5 有一堆很酷的东西,新的 API 强大又干净)。
我非常荣幸地向大家介绍 JavaScript 应用架构中的一个期待已久的飞跃,我们对于开源社区的贡献:Module Federation。
Module Federation 使 JavaScript 应用得以从另一个 JavaScript 应用中动态地加载代码 —— 同时共享依赖。如果某应用所消费的 federated module 没有 federated code 中所需的依赖,Webpack 将会从 federated 构建源中下载缺少的依赖项。
代码是可以共享的,但每种情况都有降级方案。federated code 可以总是加载自己的依赖,但在下载前会去尝试使用消费者的依赖。更少的代码冗余,依赖共享就像一个单一的 Webpack 构建。虽然我已经实现了这个最初的系统——他是由我自己(Zack Jackson)和 Marais Rossouw 在 Tobias Koppers 的悉心帮助和结对编程下完成的。这些开发者们在 Webpack 5 的核心重写和稳定化上起着关键性的作用,谢谢你们一直以来的合作和支持。
# 术语

Module federation: 与 Apollo GraphQL federation 的想法相同 —— 但适用于在浏览器或者 Node.js 中运行的 JavaScript 模块。
host:在页面加载过程中(当
onLoad事件被触发)最先被初始化的 Webpack 构建;remote:部分被 “host” 消费的另一个 Webpack 构建;
Bidirectional(双向的) hosts:当一个 bundle 或者 webpack build 作为一个
host或remote运行时,它要么消费其他应用,要么被其他应用消费——均发生在运行时(runtime)。
如果你想更好地理解这篇文章,欢迎阅读Jack Herrington 写的文章!
值得注意的是,该系统的设计使每个完全独立的构建/应用都可以存放在自己的仓库中,独立部署,并作为独立的 SPA 运行。
这些应用都是 bidirectional(双向的) hosts. 所有在你修改路由或者移动应用时率先加载的应用都将成为 host, 它会像你实现 dynamic imports 一样加载 federated modules. 不过,如果你要刷新页面,则任何在之后的 load 中率先启动的应用,都将成为一个 host。
假设一个网站的每个页面都是独立部署和编译的,我想要这种微前端风格的架构,但不希望页面随着我更改路由而重新加载。我还希望在页面之间动态地共享代码和 vendors, 这样它就像支持 code splitting 的大型 Webpack 构建般高效了。
访问应用的 home 也将会使这个 “home” 页成为 “host”,如果你切换到 “about” 页,那么这个 host(home 页的 spa)实际上是从另一个独立应用(about 页的 spa)中动态加载一个模块,它并不会加载应用主入口以及另一个完整的应用,而只会加载几千字节的代码。如果我在 “about” 页刷新浏览器,“about” 页将会成为 “host”,此时我回到 “home” 页,“about” 页(“host”)将会从 “remote” 获取运行时的一些片段——这个 “remote” 就是 “home” 页。在这个系统中,所有的应用都既是 “remote” 又是 “host”,与其它 federated module 互为消费者与被消费者。
你可以在 Github 上找到更多的技术细节:
Merge Proposal: Module federation and code sharing between bundles. Many builds act as one
# 构建一个 federated application
让我们从三个独立的应用入手。
# App One
# 配置
我将使用 App One 中的应用容器 <App>,它将会被另一个应用消费,为此,我把 App expose 为 AppContainer,App One 将会同时消费来自另外两个 federated applications 中的组件,为此,我指定了 remotes scope:
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"]
})
]
}
# 设定构建编排流程
在应用的头部,我加载了 app_one_remote.js —— 来让当前的应用连接到其他 Webpack runtimes,并在运行时提供一个编排层(orchestration layer),这是一个专门设计的 Webpack runtime 和 entry point,但它不是一个普通的应用 entry point,并且只有几 KB。
要注意,这些特殊的 entry points 大小只有几 KB——它们包含一个可以与 host 连接的特殊的 Webpack runtime,它们并非标准的 entry point。
<head>
<script src="http://localhost:3002/app_one_remote.js"></script>
<script src="http://localhost:3003/app_two_remote.js"></script>
</head>
<body>
<div id="root"></div>
</body>
# 从 remote 消费代码
App One 中有个 Page1 页面消费了 App Two 的 dialog 组件:
const Dialog = React.lazy(() => import("app_two_remote/Dialog"));
const Page1 = () => {
return (
<div>
<h1>Page 1</h1>
<React.Suspense fallback="Loading Material UI Dialog...">
<Dialog />
</React.Suspense>
</div>
);
}
export default Page1;
路由看起来很标准:
import { Route, Switch } from "react-router-dom";
import Page1 from "./pages/page1";
import Page2 from "./pages/page2";
import React from "react";
const Routes = () => (
<Switch>
<Route path="/page1">
<Page1 />
</Route>
<Route path="/page2">
<Page2 />
</Route>
</Switch>
);
export default Routes;
# App Two
# 配置
App Two 将会导出 Dialog,并让 App One 来消费它,App Two 将会同时消费 App One 的 <App> —— 因此我们把 app_one 页设置为 “remote”。演示一下 bi-directional hosts.
const HtmlWebpackPlugin = require("html-webpack-plugin");
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: "app_two_remote",
library: { type: "var", name: "app_two_remote" },
filename: "remoteEntry.js",
exposes: {
Dialog: "./src/Dialog"
},
remotes: {
app_one: "app_one_remote",
},
shared: ["react", "react-dom","react-router-dom"]
}),
new HtmlWebpackPlugin({
template: "./public/index.html",
chunks: ["main"]
})
]
};
# 消费
这个是根 App:
import React from "react";
import Routes from './Routes'
const AppContainer = React.lazy(() => import("app_one_remote/AppContainer"));
const App = () => {
return (
<div>
<React.Suspense fallback="Loading App Container from Host">
<AppContainer routes={Routes}/>
</React.Suspense>
</div>
);
}
export default App;
这个是使用了 Dialog 的默认页面:
import React from 'react'
import {ThemeProvider} from "@material-ui/core";
import {theme} from "./theme";
import Dialog from "./Dialog";
function MainPage() {
return (
<ThemeProvider theme={theme}>
<div>
<h1>Material UI App</h1>
<Dialog />
</div>
</ThemeProvider>
);
}
export default MainPage
# App Three
正如所料,App Three 看起来(和App Two)很相似。但是,它不消费来自 App one 的独立自运行的组件 <App> (没有导航栏或侧边栏),因此,它并未指定任何 “remote”:
new ModuleFederationPlugin({
name: "app_three_remote",
library: { type: "var", name: "app_three_remote" },
filename: "remoteEntry.js",
exposes: {
Button: "./src/Button"
},
shared: ["react", "react-dom"]
}),
# 浏览器中的最终结果(不同于第一个视频)
请重点关注 network 标签页,来自三个不同服务器的代码正在被整合(federated),三个不同的 bundles,我们一般建议不要 federate 整个应用的容器,除非你正享受 SSR 或者渐进式加载带来的好处。不管怎么说,这个概念简直太强大了。
# 代码冗余
这里几乎没有任何依赖冗余,通过 shared 选项 —— remotes 将会首先依赖来自 host 的依赖,如果 host 没有依赖,它将会下载自己的依赖。没有代码层面的冗余,而只有内置的冗余。
然而手动地增加 vendors 或其他用于共享的模块并不利于拓展,你可以自定义一个函数或者相应的 Webpack 插件来实现自动化。我们的确计划发布 AutomaticModuleFederationPlugin,并且在 Webpack Core 之外去维护它。既然我们在 Webpack 中已经有了相当好的 code federation 支持,拓展它的功能就不值一提了。
那么大问题来了… 上面说的这些支持 SSR 吗??
# 服务端渲染
我们将其设计成了可以在任何环境下工作的 “通用模块 Federation”,服务端渲染 federated code 是完全可行的,只要让服务器构建使用一个 commonjs 的 library target。实现 federated SSR 有很多种办法,S3 Streaming, ESI, 自动化一个 npm 发布去消费服务器的变化内容,我计划用一个常用的共享文件的 volume 或者移步的 S3 streaming 来跨文件系统传输文件。让服务器能够像在浏览器中一样去 require federated code,使用 fs 而不是 http 来加载 federated code。
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: "container",
library: { type: "commonjs-module" },
filename: "container.js",
remotes: {
containerB: "../1-container-full/container.js"
},
shared: ["react"]
})
]
};
“Module Federation 当然也适用于
target: "node",这里使用指向其他微前端应用的文件路径,而不是 URLs。这样的话你就可以用同样的代码,外加不一样的 Webpack 配置来在 Node.js 中实现 SSR. Module Federation 的特性在 Node.js 中保持不变,如独立构建、独立部署。—— Tobias Koppers
# Federated Next.js on Webpack 5
Federation 依赖 Webpack 5 —— Next 并未官方支持。不过我已经 fork Next.js,并设法升级它以支持 Webpack 5 了!这项工作仍在进行中,还有一些开发模式下的中间件需要完成。生产模式已经搞定了,但某些附加的 loaders 仍然需要重新测试。
feat: Upgrade Next.js to Webpack 5
# Talks, podcasts, or feedback
我非常想有和大家分享这项技术的机会,如果你想要使用 Module Federation 或者 Federated architecture,我们想要听到你的使用经历以及对现有的架构的调整。我们也非常期望能够有机会在 Podcasts、meetups 或者 corporations 中讨论它。你可以通过 Twitter 联系到我。
你还可以联系我的联合创作者,来了解 Module Federation 的最新变化、FOSA (Federation of Standalone Applications) 架构,以及我们正在做的其他工具 —— 用于和 Federated Applications 共用的工具。
# 使用 Module Federation 的例子
社区对 Module Federation 的反馈非常热烈!我和我的创作者们的大部分时间,都集中在将这项特性写到 Webpack 5 中,当我们忙于完成剩余特性,以及书写文档时,希望这些代码示例能对你有帮助。
因为我们有带宽,所以我们将创建一些 SSR 示例和更全面的 demo,如果有人想建设可以作为 demo 使用的东西 —— 我们很乐意接受提交给 webpack-external-import 的 pull requests。
感谢 Tobias Koppers.













