【第 2 期】模块联邦简介

1,523 阅读8分钟

模块联邦是什么

从字面意思上理解,模块 对应的英文是 module。按照拆分粒度不同,可以是antd 组件这种原子级别的模块,也可以是阿里飞冰区块这种区块级别的模块,再往大还可以是一个单体应用。没错,就是你熟悉的那个 npm 包。联邦 对应的就是把这些 模块 组织在一起。

这么一看,好像和npm干的事情差不多,就是允许不同的应用间共享模块嘛。所以为啥还要整个 MF 呢,而且还有文章称之为 Game-changer in JavaScript architecture!很酷炫的名字,类似终结者一样的霸气称号!我们来结合实际场景的问题来说说。

假如我们有一个类似于阿里云的大型应用,里面会有很多的产品模块(云服务器、数据库、域名网站等等),作为这个大型应用的前端团队,我们对接了云服务器、数据库、域名网站等等多个后端团队。假设每个小产品模块和一个后端团队对接,在产品上也会拆分为多个小产品。

这个时候,按照传统方案,我们在前端架构的时候有两种选择:第一种是一个单体前端应用,包含所有产品,将不同产品维护在同一仓库的不同目录下,统一打包。这种方案在项目规模不是很大的时候,用起来会很舒服。

但是随着不断迭代,项目体积越来越大的时候,就会出现打包越来越慢的问题,同时多项目在一个仓库也增加了并行场景下的冲突风险。这个时候,我们就会选择第二种方案,将不同子产品拆成独立的项目,将公共模块抽成 npm 包,每个子产品单独仓库维护,单独发布。这样就解决了之前打包慢和并行冲突的问题。

可是!当我们的应用拆的越来越多,几十个、几百个的时候。我们又遇到了新的问题:公共模块升级了,子应用需要全部升级一遍然后发布。脑阔疼!虽然我们可以使用 lerna 等 monorepo 的方案将多个子应用管理在一个大仓库里,同时每个子应用单独打包,但是当你的产品到了更大量级的时候,像下图这样。

webpack5模块联邦解析-图1

显然单仓库不是一个很好的解决方案。

正所谓:分久必合,合久必分。于是诞生了微前端。

通过微前端架构,来帮助我们组织大型应用。以阿里的乾坤为例,它帮助我们解决单体应用在一个相对长的时间跨度下,由于参与的人员、团队的增多、变迁,从一个普通应用演变成一个巨石应用后,随之而来的应用不可维护的问题。从某种意义上来说,这种以 singleSPA 为代表的微前端方案能帮助我们解决大型应用的架构问题。

MF 的作者在这个 issue里提到:

I don't want to use less integrated/framework oriented solutions like SingleSPA, browser events, service workers - I want to use Webpack as the host container for foreign chunks and modules, not load other app entry points, but rather load other apps chunks and use those modules

也就是说,MF 可以帮助我们解决一个更深层次的问题,让多个子应用间的模块可以在运行时共享,不再需要因为依赖的 npm 包的一次升级,N 处发布。不同的子应用因为联邦可以任意共享模块,就像导入导出 JS 一样简单。作为 Webpack5 的重磅功能,MF 为我们提供了面向未来的微前端解决方案,基于 MF 可以实现真正意义上的让多个独立的子应用像一个真正的 SPA 一样运行,不需要在切换子应用的时候刷新页面

一个完整的基于 MF 应用的例子 🌰

这个例子展示了一个相对比较完整的类微前端架构的例子。

完整效果

webpack5模块联邦解析-图2

目录结构

.
├── shell // 应用的主容器
│   ├── package.json
│   ├── public
│   │   └── index.html
│   ├── src
│   │   └── 应用代码
│   └── webpack.config.js
├── dashboard // dashboard页面
│   ├── package.json
│   ├── public
│   │   └── index.html
│   ├── src
│   │   └── 应用代码
│   └── webpack.config.js
├── order //
│   ├── package.json
│   ├── public
│   │   └── index.html
│   ├── src
│   │   └── 应用代码
│   └── webpack.config.js
├── profile
│   ├── package.json
│   ├── public
│   │   └── index.html
│   ├── src
│   │   └── 应用代码
│   └── webpack.config.js
├── sales
│   ├── package.json
│   ├── public
│   │   └── index.html
│   ├── src
│   │   └── 应用代码
│   └── webpack.config.js
└── package.json

其中 shell 是应用的主外壳,提供外层容器,dashboardorderprofile目录对应 3 个页面,sales 是 dashboard 中引用的组件,同时 dashboard 还引用了 order 作为自己的组件。

这里有两个神奇的地方:

  • 所有的子应用都有自己独立的打包项,完全独立解耦
  • 引入其他应用的组件就像引入自己内部组件一样,完全没有区别
// dashboard应用中引入其他组件
const RecentOrders = React.lazy(() => import('order/RecentOrdersWidget'));
const SalesDeposits = React.lazy(() => import('sales/DepositsWidget'));
const SalesToday = React.lazy(() => import('sales/TodayWidget'));

打开组件调试工具,你甚至完全感受不到子组件分布在其他应用里。

webpack5模块联邦解析-图3

如何构建一个模块联邦

使用模块联邦,只需要依赖 webpack5 的内置模块 ModuleFederationPlugin就可以。我们以构建一个由三个子应用构成的模块联邦为例。

// 应用一的 webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin');
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');
module.exports = {
  // 其他配置...
  plugins: [
    new ModuleFederationPlugin({
      name: 'app1',
      remotes: {
        app2: 'app2@http://localhost:3002/remoteEntry.js',
      },
      shared: ['react', 'react-dom'],
    }),
    new HtmlWebpackPlugin({
      template: './public/index.html',
    }),
  ],
};

// 应用二的 webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin');
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');

module.exports = {
  // 其他配置...
  plugins: [
    new ModuleFederationPlugin({
      name: 'app2',
      exposes: {
        './Button': './src/Button',
      },
      shared: ['react', 'react-dom'],
      filename: 'remoteEntry.js',
    }),
    new HtmlWebpackPlugin({
      template: './public/index.html',
    }),
  ],
};

上面这段代码就是最基本的模块联邦应用的配置例子。除此之外的所有配置都和以前的 webpack 应用没有任何区别。

这里有 3 个重要的字段,name 顾名思义,表示这个模块联邦应用的名称,是其他子应用引用时候的唯一标识,类似 npm 的包名的概念。remotes 类似我们 import 一个包,表示这个应用要去使用其他模块联邦里的哪些包,exposes 类似 export 一个包,导出的是当前应用下的模块供其他子应用使用。

除此之外,还有两个重要字段,一个是 filename, 一个是 sharedshared 里表示应用间共享的依赖,app1 引入了 app2 的 Button 组件,Button 组件依赖 react,但是因为在shared 中配置了共享这个依赖,所以 app1 可以直接使用自己的 react。如果 app1 和 app2 的 react 是不同版本的话,则会智能判断然后去自动引入 app2 依赖的 react 的 chunck 文件。

remotes will depend on host dependencies, if the host does not have a dependency, the remote will download its own. No code duplication, but built-in redundancy.

想体验这个功能可以看diffrent-react-versions这个 demo。 另外一个是filename字段,这个字段导出的是 MF 子应用的元信息,供模块联邦的子模块间互相依赖的时候去查询。不同于普通的应用 entry point,这个文件很小。

This connects you to other Webpack runtimes and provisions the orchestration layer at runtime. It's a specially designed Webpack runtime and entry point. It's not a normal application entry point and is only a few KB.

webpack5模块联邦解析-图4

本地开发状态未压缩的情况下,这个 remoteEntry.js 的文件只有 20 多 K。

通过上面的配置后,我们就可以在 app1 中使用 app2 导出的 Button 组件了。

const RemoteButton = React.lazy(() => import('app2/Button'));

const App = () => (
  <div>
    <h1>Basic Host-Remote</h1>
    <h2>App 1</h2>
    <React.Suspense fallback="Loading Button">
      <RemoteButton />
    </React.Suspense>
  </div>
);

import('app2/Button') 就对应 app1 中remotes里设置的 app2 这个"包"名,和 app2 中exposes导出的 Button 组件。

这样就可以很容易的构建一个模块联邦应用。当然,除此之外还有很多其他的配置项和配置方法,MF 提供了这样的一个组织应用的变革性的机制,剩下的就看大家的想象力了。

官方 demo里还有很多其他的例子,想深入了解的话建议看看。

总结

MF 的这种组织应用的模式带给我们很多想象空间,因为目前 webpack5 还没有发布正式版,所以还没有达到生产可用的状态。基于 MF 的微前端方案会是面向未来的一种方案,会让多个子应用间就像一个单页应用一样,同时也能让子应用间可以运行时动态共享模块。当然,运行时共享也是一把双刃剑,如何去做版本控制以及控制共享模块的影响是需要去考虑的问题。此外,这种方案也会对子应用的打包方案有一定的要求(webpack5 或者 rollup),所以对于已存在的项目会有一定改造成本。总之,MF 不是银弹,还是需要结合场景去选择。期待我们可以基于 MF 做出更多有意思的事情。

相关资料