模块联合(Module federation)允许 JavaScript 应用在客户端和服务器上动态运行来自另一个包或版本的代码。

这是 JavaScript 捆绑器,等效于在 Apollo 中使用 GraphQL。

*从没有哪一种在独立的应用程序之间共享代码的可伸缩解决方案能够如此便捷,而且在成规模时几乎是不可能的做到的。我们所拥有的最接近的东西是 externals 或 DLLPlugin,不过这造成了对外部文件的集中式依赖。共享代码很麻烦,各个应用程序并不是真正独立的,并且通常只能共享有限数量的依赖项。此外,在单独捆绑的应用程序之间共享实际的功能代码或组件是不可行的、无效的并且是无益的。*

对于那些想要更通俗版本的人,Jack Herrington 录了一个视频!

油管视频:https://youtu.be/D3XYAx30CNc

Webpack 5 Module Federation: A game-changer in JavaScript architecture


我们需要一个可扩展的解决方案来共享 node 模块和功能与应用程序代码。它需要在运行时发生,以便具有适应性和动态性。 Externals 并不能有效或灵活地完成工作;Import maps 无法解决规模问题。我并不是要单独下载代码并共享依赖项,而是需要一个业务编配层,该层能够在运行时动态地共享模块,并有后备功能。

image-20200508143303964

什么是模块联合(Module Federation)?

Module Federation 是我发明并原型化的一种 JavaScript 体系结构。然后,在我的联合创始人和 Webpack 创始人的帮助下— —它变成了 Webpack 5 核心中最令人兴奋的功能之一(里面有一些很棒的东西,新的 API 确实功能强大且简洁)。

*我很自豪地向你介绍,JavaScript 应用架构中期待已久的飞跃。我们对开源社区的贡献:Module Federation*

模块联合(Module Federation) 允许 JavaScript 应用动态地从另一个应用中加载代码,然后在过程中共享依赖项。如果使用模块联合的应用程序不具有联合代码所需的依赖项,则 Webpack 将从该联合的生成源中下载缺少的依赖项。

可以共享代码,但是每种情况都存在后备方案。联合代码始终可以加载其依赖关系,但在下载更多有效负载之前将尝试使用使用者的依赖关系。这意味着像单片 Webpack 构建一样,更少的代码重复和依赖关系共享。虽然我发明了这个系统,但它是 Marais Rossouw 和我(Zack Jackson)共同编写的,并得到了 Tobias Koppers 的大量指导和帮助。这些工程师在重写和稳定 Webpack 5 核心中的模块联合部分发挥了关键作用。感谢他们一直以来的合作与支持。

术语

  • Module federation(模块联合):与 Apollo GraphQL 联合有着相同的思想——但适用于 JavaScript 模块,可用在浏览器和 node.js 中——通用模块联合
  • host(主机):一种 Webpack 构建,该构建在页面加载期间首先初始化(触发 onLoad 事件时)
  • remote(远程主机):另一个 Webpack 构建,其中一部分被 “host” 所用
  • Bidirectional-hosts(双向主机):当 bundle 或 Webpack 构建时可以作为主机或作为远程主机使用。可在运行时使用其他应用程序或着被其他人使用

img

*请注意,该系统的设计宗旨是使每个完全独立的构建或应用都可以位于自己的存储库中,可以独立部署,并能够作为自己的独立 SPA 运行。*

这些应用都是**双向主机(bi-directional hosts)。 首先加载的任何应用都将会成为主机。当你修改路由并在应用程序中移动时,它将会以和动态导入相同的方式加载联合模块。但是如果你要刷新页面,则首先在该负载上启动的任何应用程序都将会成为主机。

image-20200508143238487

*假设网站的每个页面都是独立部署和编译的。我需要这种 micro-frontend 样式的体系结构,但是我们不希望在修改路由时重新加载页面。我还希望在它们之间动态共享代码和服务以使其高效,就好像它是一个大型的 Webpack 构建并进行了代码拆分一样。*

登陆主页应用程序将使 “主页” 页面成为“主机”。如果浏览到 “about” 页面,则主机(主页 spa)实际上是从另一个独立的应用程序( about 页面 spa)动态导入模块,它不会加载主入口点和整个应用程序:仅仅几千字节的代码。如果我在 “about” 页面上并刷新浏览器,“about” 页面会成为“主机”,而再次浏览回到主页将是 “about” 页面 “主机” 的一种情况,即从 “远程” 页面(即主页)中获取运行时的一部分。

所有应用程序都是远程和主机,被调用者以及系统中任何其他联合模块的使用者。

你可以在 GitHub 上阅读更多有关技术方面的信息:

https://github.com/webpack/webpack/issues/10352

怎样构建联合应用程序

让我们从三个独立的应用程序开始。

App 1

配置:

我将使用 App 1 中的应用容器 App。其他应用程序将会使用它。为此我将其 App 公开为 AppContainer

App 1 还将使用来自另外两个联合应用的组件。为此,我指定了remotes 配置项:

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 运行时,并在运行时预配业务编配层。这是专门设计的 Webpack 运行时和入口点。 **它不是普通的应用程序入口点,只有几个 KB **。

要注意这些是特殊的入口点 —— 它们只有几KB的大小。包含可以与主机交互的特殊 Webpack 运行时,它不是标准入口点

<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>

从远程主机使用代码

App1 的页面使用了来自App 2 的对话框组件。

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 2

配置:

App 2 将公开对话框,使 App 1 能够使用它。App 2 也会使用 App 1 的 App,因此我们指定 app_one 为远端-展示双向主机:

const HtmlWebpackPlugin = require("html-webpack-plugin");
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");
module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      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 3

不出所料,App 3 看上去类似。但是它不会使用 App 1 中的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 标签。该代码将在三个不同的服务器之间进行联合:三个不同的 bundle。通常情况下,除非你用了 SSR 或渐进式加载,否则不要联合整个应用程序容器。但是这个概念非常强大。

img

image-20200508143204907

查看推文中的视频:https://twitter.com/ScriptedAlchemy/status/1234383702433468416

代码重复

几乎没有依赖项重复。通过 shared 选项 —— 远程将依赖于主机依赖关系,如果主机没有依赖关系,则 remote 将下载自己的依赖关系。没有代码重复,但是内置冗余。

image-20200508143118794

手动将供应商或其他模块添加到 shared 并不理想。可以用自定义编写的函数或补充性的 Webpack 插件轻松地将其自动化。我们确实打算发布 AutomaticModuleFederationPlugin 并从 Webpack 核心外部对其进行维护。既然我们已经在 Webpack 中内置了一流的代码联合支持,那么扩展其功能就变得微不足道了。

现在有一个大问题 —— SSR 可以胜任这项工作吗?

服务器端渲染

我们将其设计为通用的。模块联合可在任何环境中使用。在服务器端渲染联合代码是完全可能的。只需让服务器构建使用 commonjs 库目标即可。有多种实现联合 SSR 的方法:S3流、ESI、自动执行 npm 发布以使用服务器变体。我计划用公共共享文件卷或异步 S3 流在整个文件系统中流式传输文件,使服务器能够像在浏览器中一样请求联合代码,并用 fs 而不是 http 来加载联合代码。

module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: "container",
      library: { type: "commonjs-module" },
      filename: "container.js",
      remotes: {
        containerB: "../1-container-full/container.js"
      },
      shared: ["react"]
    })
  ]
};

“模块联合也可以与 target:"node" 一起使用。作为代替指向其他微前端的 URL,在这里用指向其他微前端的文件路径。这样你可以使用相同的代码库和不同的 webpack 配置进行 SSR,以构建 node.js。对于 node.js 中的 Module Federation,相同的属性仍然适用:e.g. 单独构建,单独部署” —— Tobias Koppers

在 Webpack 5 上联合 Next.js

联合需要 Webpack 5 —— Next 尚未正式支持。但是,我确实设法 fork 并升级了 Next.js 以使其与 Webpack 5 兼容!这项工作仍在进行中。一些开发模式的中间件需要完成。生产模式目前可以工作,一些其他加载器仍需要重新测试。

image-20200508143038785

在Twitter上查看

谈话,播客或反馈

我希望有机会分享更多有关这项技术的信息。如果你想使用 Module Federation 或 Federated 体系结构,我们很想听听你对当前体系结构的经验和改进。我们也希望有机会在播客、聚会或公司中谈论它。通过 Twitter 与我联系https://twitter.com/ScriptedAlchemy

你也可以成为我的共同创作者。请关注我们,并获取有关模块联合、FOSA(独立应用程序联盟)体系结构以及我们正在创建的其他工具的最新更新,这些工具被用于联合应用程序

模块联合的示例

社区对此反应热烈!我的共同创作者以及我自己的时间都花费在编写到 Webpack 5 中。我们希望最终完成其余功能并编写一些文档的同时,一些代码示例会对你有所帮助:https://twitter.com/codervandal

Webpack 5 and Module Federation - A Microfrontend Revolution

由于有足够的带宽,我们将会创建 SSR 示例和更全面的演示。如果有人想构建可用作演示的东西,我们将很乐意接受将请求并 pull 到 webpack-external-import 中。