likes
comments
collection
share

Webpack 5 之 模块联合(Module Federation)

作者站长头像
站长
· 阅读数 92

微前端

在了解 webpack 中 模块联合的之前,我们先了解一下微前端,如果你对微前端不了解可以查阅这里两篇文章《微前端(一)- 理念篇》《微前端(二)- 实现篇》来熟悉它的基本概念。

通常我们的微前端的模型如下:

Webpack 5 之 模块联合(Module Federation)

在微前端中,会存在一个容器应用,它的任务就是加载各个微应用

微应用需要做一些事:

  • 提供两个方法:一个是挂载方法,容器将调用它来渲染微应用。另一个是卸载方法,用于卸载微应用,并且他们都将以接口方法的形式提供给容器调用;
  • 提供远程入口文件的地址,容器应用选择合适的时机动态加载该文件,在获得挂载方法后,执行微应用渲染;
  • 提供微应用 ID,用于标识自己,对微应用的操作需要该标识;
  • 首个路由地址,在挂载微应用后,决定微应用的视图展示。

容器应用要做的事是:

  • 加载远程的微应用(下载远程 js 入口文件),并执行渲染;
  • 在合理的契机,卸载微应用。

以上便是微前端的基本功能。接下来,我们再来看看 webpack 的模块联合(module federation)。

模块联合

通常在使用 webpack 构建产生的模块都存储在本地,直接被当前应用所使用。在 webpack 5 中提出了远程模块的概念,允许运行时把当前构建的应用作为容器应用,异步加载远程模块。下面将用简称MF指代模块联合。

webpack 的提供了动态加载模块的方式,你可以使用import或者较为陈旧的方法require.ensurerequire([...])。 记得上面小节在我们说微前端中,容器应用做的事吗?其实通过webpack动态加载,就已经实现了容器应用该做的事情。所以我们完全可以认为微应用本身可以具备容器应用的功能。

当我们把微应用作为容器应用时,那么它的架构模型就发生转变,于是会产生下面的模型:

Webpack 5 之 模块联合(Module Federation)

可以看到,当我们的微应用成为容器应用后,每个应用在架构里都平等得存在,容器应用之间可以相互的依赖相互的加载及使用

在微前端中,并没有对微应用复杂度做任何架构上的约束,也就说它可能只是个按钮,而这个按钮却引爆了地球,当然这是个玩笑。但我们应该从业务上合理划为它,让其成为一个有价值的复用模块并且不受框架束缚。

而如何通过构建工具对应用进行模块划分模块共享模块加载,我想这便是 webpack 5 模块联合(Module Federation)的功能意义所在。

MF vs 微前端

我们继续思考模块联合和微前端的区别。

在微前端中:

  • 加载微应用必须预定义接口方法(mounted、unmount 等)来实现微应用的动态挂载卸载等功能,这意味着每个微应用必须手动实现这些接口方法
  • 《微前端(二)- 实现篇》中,我们了解到微应用在独立开发模式下,通常也是手动调用接口方法,来动态加载视图;
  • 如果我们想要共享某个微应用的模块给其它微应用使用,这并不是轻松地事。这意味着你需要把该模块独立出去,并以合理调用方式被其它微应用远程加载
  • 微应用的切换通常由路由状态改变来触发的。

在模块联合中:

  • 上面我们了解了模块联合每个微应用可以是一个容器应用,所以他们之间可以相互依赖加载
  • 每个应用允许暴露(exposes)多个接口,其它应用可以在动态远程加载该应用后,直接使用其接口。这解决了上面微前端提到的的模块共享问题;
  • 在模块使用上非常灵活,当你引用一个远程模块时,可以像使用普通的 npm 包一样使用它,当然也允许懒加载模块;
  • 远程模块和路由没有任何关联,加载的契机完全由 host 应用自己灵活决定。

值得注意的联合模块作为微前端的技术延展,其依然具备着微前端的特性,即每个容器应用应该独立开发独立部署,并团队自治

模块联合的架构模型更像下图展示一样,当然不只这一种,因为它非常的灵活。这取决于你如何共享模块和组合他们。

Webpack 5 之 模块联合(Module Federation)

在上面的架构图中:

  • APP AAPP BAPP C都远程(remote)加载并使用UI 组件库中暴露的ButtonText组件,Table组件由于未稳定下来,我们不准备暴露给外部使用;
  • APP BAPP C中的List模块都共享给APP A所使用(例如:业务 B 的订单列表和业务 C 的订单列表都可以直接被集成到业务 A 之中);
  • 身份验证应用作为公共模块,被APP AAPP BAPP C,我们不需要单独给新应用添加额外的身份验证模块,它将作为基础服务。

ModuleFederationPlugin

Webpack 5 通过ModuleFederationPlugin来实现模块接口暴露远程模块声明的工作。

ModuleFederationPlugin插件组合了ContainerPluginContainerReferencePlugin

ContainerPlugin 插件使用指定的公开模块来创建一个额外容器入口,这意味除了配置的输出文件(output),还会产生额外的容器入口文件

module.exports = {
  output: {
    filename: 'main.js',
  },
};

ContainerReferencePlugin 插件允许我们在使用远程模块时,以 import 标准语法方式使用,所以需要我们提前声明远程模块。

ModuleFederationPlugin允许构建一个作为提供者消费者概念的运行时独立模块,每个应用都可以成为提供者或消费者。

const { ModuleFederationPlugin } = require('webpack').container;
module.exports = {
  plugins: [
    new ModuleFederationPlugin({/* options */}),
  ],
};

你可以在这里看到所有options选项。

容器入口文件

首先,我们需要提供容器入口文件(container entry)来让其它应用能够远程加载该文件:

new ModuleFederationPlugin({
    name: "ui_lib", // 容器名称
    filename: 'ui.js', // 容器入口文件
})

你需要提供一个唯一的容器的名称(name)和文件名(filename),若没有提供filename,那么构建生成的文件名与容器名称同名。

构建后,会在 dist 目录里产生ui.js的额外容器入口文件。

暴露(expose)多个模块

你可以暴露任何你想要分享出去的模块,它可以是网络库公用业务模块UI 组件路由hooks以及任何你觉得可以分享出去的任何东西,这听起来很振奋人心,而事实也确实如此。

我们通过exposes 选项来暴露模块:

new ModuleFederationPlugin({
  name: "ui_lib", // 容器名称
  filename: 'ui.js', // 容器入口文件
  exposes: {
    "./components": "./src/components/",
  },
})

假如我们的 UI 库的入口在项目src/components/index.js文件里,那么该文件应该是这样的:

export { default as Button } from './button/index.jsx'
export { default as Text } from './text/index.jsx'

共享模块

容器通常存在基础的重复依赖库(例如:react、vue 等等)。和介绍微前端文章的共享库一样,我们也需要将其从我们的容器排除出去,而让他们作为异步模块加载。

不仅如此,MF 对共享模块做了版本化管理,你可以在这个 PR 的交流获取相关信息。

同样我们使用ModuleFederationPlugin插件中的shared 选项来指定公共模块异步模块加载使用,它的功能和 webpack 的externals类似,允许在运行时加载外部依赖库。

new ModuleFederationPlugin({
  name: "ui_lib",
  filename: 'ui.js',
  exposes: {
    "./components": "./src/components/",
  },
  shared: {
    react: { singleton: true},
    "react-dom": { singleton: true}
  },
})

你可以在这里看到所有shared 选项

注意点

1.如果你想要在本地启动项目时使用共享模块(shared module),需要指定eager: true的选项,否则将会出现下面的错误。

Uncaught Error: Shared module is not available for eager consumption

该选项允许共享模块在初始化的时候直接使用,也就是说不会把它作为一个异步模块来加载。

需要注意的是开启eager 选项,它会将模块直接打入容器文件中,作为同步模块加载并使用。 你也可以通过下面的修改,修复上面的问题,即手动异步加载共享模块。

首先,我们把原来的src/index.js文件做一些修改。

以前的你入口文件像下面这样:

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
ReactDOM.render(<App />, document.getElementById('root'));

修改之后,我们只保留异步加载的功能:

import('./bootstrap');

然后,我在同级目录下创建bootstrap.js的启动文件,把原来的index.js内容复制进来:

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
ReactDOM.render(<App />, document.getElementById('root'));

这样就实现了手动异步加载。


2.我们通过requiredVersion 选项来使用指定共享模块的版本。

它有两个值:requiredVersion 为string类型的值时,表示遵循semver规范的语义化版本号。

你可以直接用package.json里的dependencies字段中包名对应版本,这样做是为了共享模块的版本和package.json中的版本保持一致。如果不一致则会打印警告。

const deps = require("./package.json").dependencies;
// other code...
new ModuleFederationPlugin({
  name: "ui_lib",
  filename: 'ui.js',
  exposes: {
    "./components": "./src/components/",
  },
  shared: {
    react: {
      requiredVersion: deps.react,
      singleton: true,
    },
    "react-dom": {
      requiredVersion: deps['react-dom'],
      singleton: true,
    }
  }
})

requiredVersion 为boolean类型的值时,表示是否启动版本号自动推断。当其为true(默认值)时,请求的模块自动根据package.json中的包名对应的版本做推断。

使用远程模块

首先,同样我们使用ModuleFederationPlugin插件,提前声明哪些是远程模块,这里通过remotes 选项进行设置:

new ModuleFederationPlugin({
  name: "app_b",
  remotes: {
    "@lumin-ui": 'ui_lib@http://localhost:3003/ui.js',
  }
})

我们在运行时使用时,它和我们平时使用 import 语法没有任何区别:

import { Button, Text } from '@lumin-ui/components';

这看起来非常的酷!对于架构升级而言,我们将本地构建模块替换成远程模块并不需要修改任何代码。

演示源码

你可以在下面的地址找到以上用例的演示源码,该用例并不完备,但展示了 MF 的基本功能。

github.com/dun-cat/web…

通过下面的步骤启动项目:

# 安装依赖
npm run bootstrap

# 启动项目
npm run start

其它有趣的用例

上面演示了 MF 中的其中一个 UI 库用例,你可以在这里找到更多用例。

shared-routing

我建议你在看看shared-routing这个用例,该用例展示了一个完整的应用如何进行模块划分,更为重要的是每个模块都获取了完整的应用。

你必须知道模块划分也意味着项目划分任务划分。在独立开发时,通常需要确认如何保证整个应用的正确性。所以我们期望自己开发的模块能够运行在整个应用中,而这个例子提供了很好的解决方案。

Webpack 5 之 模块联合(Module Federation)

扩展阅读:

> webpack.docschina.org/concepts/mo…

> www.bilibili.com/video/BV1z5…

> www.youtube.com/watch?v=-ei…

> github.com/module-fede…

> www.nicolasdelfino.com/blog/micro-…

转载自:https://juejin.cn/post/7005450458009600036
评论
请登录