沧澜.png

> 这是第 144 篇不掺水的原创,想获取更多原创好文,请搜索公众号关注我们吧~ 本文首发于政采云前端博客:模块联邦浅析open in new window

引言

作为前端打包工具的重要工具人--webpack,相信大家在项目中并不陌生。 前段时间 webpack5 新出了个特性: 模块联邦。大家可能虽然听说过,但还没在项目中使用,今天就带大家通过一个小实战来熟悉一下它的用法。

业务场景

假设公司有个业务集群,公共业务组件库升级了,希望能够尽可能少得影响业务线,仅仅在基础组件库版本升级即可全业务线升级,那么可以考虑使用模块联邦来实现。

他和利用 npm 发包来实现的方案的区别在于,npm 发布的组件库从 1.0.1 升级到 1.0.2 的时候,必须要把业务线项目重新构建,打包,发布才能使用到最新的特性,而模块联邦可以实现实时动态更新而无需打包业务线项目。

大致的原型图如下:

我们看到,project1 的 home 页的 specialItem,project2 的 about 页的 searchItem 组件被用于 project2 的 home 中, project2 的 about 直接用的 project1 的 about 页。

总体上的源代码来自于模块联邦的示例代码open in new window,稍作改动。

以下只列出改动的关键部分目录结构,冗余文件已省略。戳我open in new window查看本项目代码示例地址。

├── README.md
├── app-exposes
│   ├── babel.config.js
│   ├── src
│   │   ├── App.vue
│   │   ├── assets
│   │   ├── components
│   │   │   ├── SearchItem.vue  ---搜索组件
│   │   │   └── SpecialItem.vue  ---自定义业务组件
│   │   ├── index.ts
│   │   ├── main.ts
│   │   ├── router
│   │   │   └── index.ts
│   │   └── views
│   │       ├── AboutView.vue   ---关于页
│   │       └── HomeView.vue  ---首页
│   ├── tsconfig.json
│   └── vue.config.js
├── app-general
│   ├── babel.config.js
│   ├── src
│   │   ├── router
│   │   │   └── index.ts
│   │   └── views
│   │       └── HomeView.vue
│   ├── tsconfig.json
│   └── vue.config.js

利用脚手架分别创建 app-exposes 与 app-general 的 vue3 项目,此部分大家应该都轻车熟路在此就略过了。嫌麻烦的可以直接用我提供的 demo 样本。

首先克隆本项目代码地址后,分别在 app-exposes 与 app-general 项目下执行 npm i 安装依赖,然后分别执行 npm run serve 运行代码。 此时能够看到本地起了两个服务,端口号分别为 8083 与 8081,其中 app-exposes 为 8083,app-general 为 8081。

项目运行示意效果图如下

然后我们看看两个项目的配置文件如何配置的。

app-exposes 的 vue.config.js 配置:

app-general 的 vue.config.js 配置:

可以看到,总体上我们用到了 webpack 原生的插件 ModuleFederationPlugin 来实现模块联邦的效果的。

在首页中,我们异步引用的 app-exposes 提供的 SearchItem 以及 SpecialItem 组件。

在 about 页面的路由配置中,我们直接引入的远程连接的 AboutView 页面。

如果想查看更多关于联邦模块的案例,可以访问官方仓库open in new window

二.联邦模块插件的结构及其常见的调用方式(Module Federation Plugin)

上面我们大概了解了下模块联邦插件的大致使用方法。不过知其然也要知其所以然,所以我接下来从个人角度简单聊一聊他的实现原理。

webpack 的整体流程上来说大体分为三个主要阶段

  • 初始化阶段
  • 构建阶段
  • 生成阶段

在这三大阶段时拥有极其庞大的插件库在各个阶段以及节点中发挥各自的作用,而模块联邦插件就是其中之一。

模块联邦作为一个 webpack5 时期新出的插件,形态上看通常是一个带有 apply 方法的类。

class ModuleFederationPlugin {
    apply(compiler) {}
}

参数 compiler 是 webpack 上下文,可以调用 hook 对象注册各种钩子回调。

如下文中的 compiler.hooks.thisCompilation.tap,表明调用 afterPlugins 这个钩子的 tap 方法,传入插件名称与回调函数,执行我们指定的逻辑,webpack 通过这种方式来构建其庞大繁杂的插件体系。

class ModuleFederationPlugin {
    apply(compiler) {
        compiler.hooks.afterPlugins.tap("ModuleFederationPlugin", () => {
          ...
        }
    }
}

钩子的核心逻辑定义在 Tapableopen in new window 仓库,内部定义了如下类型的钩子。

const {
    SyncHook,
    SyncBailHook,
    SyncWaterfallHook,
    SyncLoopHook,
    AsyncParallelHook,
    AsyncParallelBailHook,
    AsyncSeriesHook,
    AsyncSeriesBailHook,
    AsyncSeriesWaterfallHook
 } = require("tapable");

三.联邦模块的原理分析

联邦模块有两个主要概念:Host(消费其他 Remote)和 Remote(被 Host 消费)。 每个项目可以是 Host 也可以是 Remote,也可以两个都是。可以通过 webpack 配置来区分,可以参考例子open in new window

  • 作为 Host 需要配置 remote 列表和 shared 模块。
  • 作为 Remote 需要配置项目名(name),打包方式(library),打包后的文件名(filename),提供的模块(exposes),和 Host 共享的模块(shared)。

webpack 打包原理

webpack4 对于异步模块加载步骤

  • import(chunkId) => webpack_require.e(chunkId) 将相关的请求回调存入 installedChunks。
  • 发起 JSONP 请求。
  • 将下载的模块录入 modules。
  • 执行 chunk 请求回调。
  • 加载 module。
  • 执行用户回调。

联邦模块是基于 webpack 做的优化,所以在深入联邦模块之前我们首先得知道 webpack 是怎么做的打包工作。 webpack 每次打包都会将资源全部包裹在一个立即执行函数里面,这样虽然避免了全局环境的污染,但也使得外部不能访问内部模块。 在这个立即执行函数里面,webpack 使用 webpack_modules 对象保存所有的模块代码,然后用内部定义的 webpack_require 方法从 webpack_modules 中加载模块。并且在异步加载和文件拆分两种情况下向全局暴露一个 webpackChunk 数组用于沟通多个 webpack 资源,这个数组通过被 webpack 重写 push 方法,会在其他资源向 webpackChunk 数组中新增内容时同步添加到 webpack_modules 中从而实现模块整合。

联邦模块就是基于这个机制,修改了 webpack_require 的部分实现,在 require 的时候从远程加载资源,缓存到全局对象 window["webpackChunk"+appName] 中,然后合并到 webpack_modules 中。

ModuleFederationPlugin 的原理

源码中 ModuleFederationPlugin 主流程 主要做了三件事:

  • 通过参数是否配置 shared 来判断是否使用共享依赖 SharePlugin 模块。
  • 通过参数是否配置 exposes 来判断是否使用公开 ContainerPlugin 模块。
  • 通过参数是否配置 remotes 来判断是否使用 ContainerReferencePlugin 引用模块。

下面是项目源码,部分代码以及判断条件已省略。

// 源码目录 lib/container/ModuleFederationPlugin
class ModuleFederationPlugin {
  ...
	apply(compiler) {
		if (library && ...) {
			compiler.options.output.enabledLibraryTypes.push(library.type);
		}
		compiler.hooks.afterPlugins.tap("ModuleFederationPlugin", () => {
			if (options.exposes && ...) {
				new ContainerPlugin({
					...
				}).apply(compiler);
			}
			if (options.remotes && ...) {
				new ContainerReferencePlugin({
					remoteType,
					remotes: options.remotes
				}).apply(compiler);
			}
			if (options.shared) {
				new SharePlugin({
					shared: options.shared,
					shareScope: options.shareScope
				}).apply(compiler);
			}
		});
	}
}

module.exports = ModuleFederationPlugin;

webpack5 模块联邦对异步模块加载的处理

  • 下载并执行 remoteEntry.js,挂载入口点对象到 window.app-exposes,他有两个函数属性,init 和 get。init 方法用于初始化作用域对象 initScope,get 方法用于下载 moduleMap 中导出的远程模块。
  • 加载 app-exposes 到本地模块。
  • 创建 app-exposes.init 的执行环境,收集依赖到共享作用域对象 shareScope。
  • 执行 app-exposes.init,初始化 initScope。
  • 用户 import 远程模块时调用 app-exposes.get(moduleName) 通过 Jsonp 懒加载远程模块,然后缓存在全局对象 window['webpackChunk' + appName]。
  • 通过 webpack_require 读取缓存中的模块,执行用户回调。

四.使用场景

目前模块联邦已经在微前端领域发挥了巨大的作用,也起到 webpack 能够越来越强大。

利用模块联邦强大的跨应用级模块共享能力,我们可以搭建一个非业务的中台搭建系统,实现 app 级别的低代码搭建平台,这与市场上常见页面级低代码搭建不同,能够实现系统级能力复用的同时降低维护成本。后续比如说 sso 单点登录,页面跳转,埋点,异常捕获等都可以考虑抽象封装成系统内置的方法到里面。

总结 通过这篇文章,我们收获了

  • 模块联邦的基础概念。
  • 模块联邦常用的配置项。
  • 通过简易配置实现雏形项目开发。
  • 模块联邦的基本原理。

参考文章

推荐阅读

性能优化——图片压缩、加载和格式选择open in new window

[如何基于 WebComponents 封装 UI 组件库](https://juejin.cn/post/7096265630466670606 "# 如何基于 WebComponents 封装 UI 组件库")

Web Workeropen in new window

如何落地一个智能机器人open in new window

一名练习时长 2 年零 8 个月的前端练习生自述open in new window

开源作品

  • 政采云前端小报

开源地址 www.zoo.team/openweekly/open in new window (小报官网首页有微信交流群)

  • 商品选择 sku 插件

开源地址 https://github.com/zcy-inc/skuPathFinder-back/open in new window

招贤纳士

政采云前端团队(ZooTeam),一个年轻富有激情和创造力的前端团队,隶属于政采云产品研发部,Base 在风景如画的杭州。团队现有 60 余个前端小伙伴,平均年龄 27 岁,近 4 成是全栈工程师,妥妥的青年风暴团。成员构成既有来自于阿里、网易的“老”兵,也有浙大、中科大、杭电等校的应届新人。团队在日常的业务对接之外,还在物料体系、工程平台、搭建平台、性能体验、云端应用、数据分析及可视化等方向进行技术探索和实战,推动并落地了一系列的内部技术产品,持续探索前端技术体系的新边界。

如果你想改变一直被事折腾,希望开始能折腾事;如果你想改变一直被告诫需要多些想法,却无从破局;如果你想改变你有能力去做成那个结果,却不需要你;如果你想改变你想做成的事需要一个团队去支撑,但没你带人的位置;如果你想改变既定的节奏,将会是“5 年工作时间 3 年工作经验”;如果你想改变本来悟性不错,但总是有那一层窗户纸的模糊… 如果你相信相信的力量,相信平凡人能成就非凡事,相信能遇到更好的自己。如果你希望参与到随着业务腾飞的过程,亲手推动一个有着深入的业务理解、完善的技术体系、技术创造价值、影响力外溢的前端团队的成长历程,我觉得我们该聊聊。任何时间,等着你写点什么,发给 ZooTeam@cai-inc.com