webpack5 新特性 Module federation
引言
我们知道 Webpack 在项目打包的时候,可以通过设置 DLL 或者 Externals 来做代码共享时 Common Chunk,这些功能只能对于单独的项目可以实现,但对于不同应用和项目间这个任务就变得困难了,我们几乎无法在项目之间做到按需热插拔。无法实现跨项目,跨平台,跨框架去实现
Module federation “模块联邦”是 Webpack5 新内置的一个重要功能,可以让跨应用间真正做到模块共享,这里贴一篇,大家可以先去了解一下
什么是 “模块联邦” 功能。
什么是Module federation “模块联邦”
“模块联邦”,它允许多个 webpack 构建一起工作。 从运行时的角度来看,多个构建的模块将表现得像一个巨大的连接模块图。 从开发者的角度来看,模块可以从指定的远程构建中导入,并以最小的限制来使用。使 JavaScript 应用得以在客户端或 服务器 上动态运行或者动态加载另一个 bundle.js 或者 build 之后生成的代码,且共享依赖。
代码是可以共享的,但每种情况都有降级方案。Module federated 可以总是加载自己的依赖,但在下载前会去尝试使用消费者的依赖。更少的代码冗余,依赖共享就像一个单一的 Webpack 构建。
描述的通俗一些就是允许运行时动态决定代码的引入和加载,且可以多个应用引用多个公共模板。且相互之间有可以互相引用,类似套娃这样的引用也是可以的,无需去每个项目中使用npm包插件的形式引入,可以直接引用其他项目的功能,我们可以把一些公共的部分抽离出来单独维护成一个公共的项目集合库,而这个公共的集合库,涉及模块升级改造一次,其他被使用到改模块的应用就全部都会自动更新完成,无需人为的去介入。例如npm 包插件的引入,每次升级npm 包的版本,就需要去各自的项目里面npm update xxx@版本号,而引用Module federated 这无需去这样操作。
Module federation “模块联邦” 功能与目的
- 功能:webpack4 以下的构建结果标明,webpack对外只提供了一个全局的webpackJsonp数组(注意不是方法),每个异步chunk加载后通过该数组将自身的modules push(该push方法实际上被劫持修改过的)到内部webpack_modules这个对象上,内部变量可以访问到该对象,但外部是无法获取到的,完全属于“暗箱操作”,这也导致了无法跟外界环境进行模块“联邦”,这也是为什么webpack5中引进了模块联邦机制。通过该机制,可以让构建后的代码库动态的、运行时的跑在另一个代码库中。
- 目的:通过细化功能模块、组件复用、共享第三方库、runtime dependencies线上加载npm包等,可以更好的服务于多页应用、微前端等开发模式。
共享模块概述
UMD模块化规范方式共享模块
UMD:Universal Module Definition(通用模块规范)是由社区想出来的一种整合了CommonJS和AMD两个模块定义规范的方法。
UMD:基本原理
用一个工厂函数来统一不同的模块定义规范。
UMD:原则
所有定义模块的方法需要单独传入依赖
所有定义模块的方法都需要返回一个对象,供其他模块使用
以下示例演示了如何使用UMD开发跨平台 UI (用户界面) 组件, 并同时避免客户代码与具体 UI 类之间的耦合。:
说简单点,这个就是设计模式中的工厂模式,UMD模块化规范方式就是采用这样的方式来实践的
共享模块方式这类就是我们开发的时候,我们经常会把某些功能封装成可复用的模块。模块封装了功能,并且对外暴露一个API,比如jquery,lodash等这类第三方插件,也可以通过CND的方式直接应用,然后页面上直接使用。后续这个公共函数的升级改造,我们更新CND引用链接地址,那么应用就自然也更新了。
NPM 方式共享模块
npm 的方式共享模块,就是需要代码共享的项目中,需要将依赖使用npm方式安装到项目,然后进行 Webpack 打包构建再上线。
比如对于不同的项目 A 与 B,需要共享一个模块C时,最常见的办法就是将其抽成通用依赖并分别安装在各自项目中npm install C,然后本地编辑打包即可,版本更替更新模板C即可。
npm 的方式共享模块如下:
微前端方式共享模块
微前端:micro-frontends (MFE) 也是最近比较火的模块共享管理方式,微前端就是要解决多项目并存问题,多项目并存的最大问题就是模块共享,不能有冲突。
微前端一般有两种打包方式:
- 子应用独立打包,模块更解耦,但无法抽取公共依赖等。
- 整体应用一起打包,很好解决上面的问题,但打包速度实在是太慢了,不具备水平扩展能力。
微前端架构如下:
微前端架构的使用,具体如何就不展开讨论了,大家可以参考Single-SPA,官网详细解释了这个框架的使用。也可以自行去搜索相关微前端架构的相关知识。
模块联邦方式
Webpack5 内置核心特性之一的 Federated Module:
从图中可以看到,这个方案是直接将一个应用的包应用于另一个应用,同时具备整体应用一起打包的公共依赖抽取能力。
比如A应用由1,2,3模块组合而成,而B应用也需要用到1,2模块,而模块1,2应用到B应用,我们无需去B应用中去重新编译集成就可以直接复用A应用的 Npm 包和模块。
引用的1,2模块在A应用中就把相关的依赖都集成完成,直接引用接口,达到按需热插拔。
这张图给我们的启发,对于模块化复用或者微前端架构中的细分化,独立化模块开发功能,提供了完美的架构解决方案。因为所有子应用都可以利用 Runtime 方式复用主应用的 Npm 包和模块,更好的集成到主应用中。
Federated Module 让应用具备模块化输出能力,其实开辟了一种新的应用形态,即 “中心应用”,这个中心应用用于在线动态分发 Runtime 子模块,并不直接提供给用户使用。
Module federation “模块联邦”的使用方式
注: 基于 webpack5版本开发
(一) 基于模块/组件库类的webpack配置
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin")
new ModuleFederationPlugin({
name: "zCompRemote",
library: { type: "var", name: "zComp" },
filename: "zComp-remote-entry.js",
exposes: {
myButton: "./src/myButton.vue",
EleInput: './node_modules/element-ui/packages/input/src/input.vue'
},
remotes: {
zLib: 'zLib'
},
shared: ['vue']
})
(二) 基于js函数/类库的webpack配置
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin")
new ModuleFederationPlugin({
name: "zLibRemote",
library: { type: "var", name: "zLib" },
filename: "zLib-remote-entry.js",
exposes: {
utils: "./src/utils.js"
}
})
Module federation 应用的内部参数说明:
- name:必传且唯一,作为关键名称用于第三方引用,相当于一个alias,引用方式 n a m e / {name}/ name/{expose}
- library: 声明一个挂载在全局下的变量名,其中name即为umd的name
- filename: 构建后的chunk名称
- Exposes: 作为被引用方最关键的配置项,用于暴露对外提供的modules模块
- shared: 声明共享的第三方资源
- remotes:作为引用方最关键的配置项,用于声明需要引用的远程资源包的名称与模块名称
在引用远程资源的项目中使用时,需要先远程资源入口文件引入,可以异步加载,也可以使用script标签引入。这一个功能是向全局挂载一个zComp变量,并提供一个get方法用于获取模块。
//index.html 页面引用远程资料
<script src="../../zLib/dist/zLib-remote-entry.js"></script>
当需要引用某个资源模块时,通过异步应用的方式直接引入。
异步引入资源模板代码如下:
const asyncJsonp = (() => {
const cacheMap = {}
return (path, delay = 120) => {
if (!path || cacheMap[path]) return
return new Promise((resolve, reject) => {
const script = document.createElement('script')
script.charset = 'utf-8'
script.timeout = delay
script.src = path
const onScriptComplete = event => {
script.onerror = script.onload = null
clearTimeout(timeout)
if (event.type === 'load') {
cacheMap[path] = true
return resolve()
}
const error = new Error()
error.name = 'Loading chunk failed.'
error.type = event.type
error.url = path
reject(error)
}
const timeout = setTimeout(() => {
onScriptComplete({ type: 'timeout', target: script })
}, delay * 1000)
script.onerror = script.onload = onScriptComplete
document.head.appendChild(script)
})
}
})()
页面调用如下:
remoteInput: async () => {
await asyncJsonp('../../zComp/dist/zComp-remote-entry.js')
const inputFactory = await zComp.get('EleInput')
return inputFactory().default
},
上述引用的地址
- …/…/zComp/dist/zComp-remote-entry.js
- …/…/zLib/dist/zLib-remote-entry.js
当前的举例是应用都是再同一个目录下的,相对地址应用而已,而我们实际的项目开发过程中,比如我们用三个项目,那么肯定是三个不同的git地址,三个不同的仓库的,那么这些资源模块的加载就需要写成绝对地址引用,比如:https://www.xxx.com/zComp/dist/zComp-remote-entry.js。
具体的demo 可查看参考文章中的webpack5-module-federation-demo
Module federation “模块联邦”打包之后的源码解析
注:我们对打包之后生成的 zLib-remote-entry.js 进行解析
打包之后的源码如下:
//核心代码
moduleMap = {
"utils": () => {
return __webpack_require__.e("src_utils_js").then(() => () => __webpack_require__(/*! ./src/utils.js */ "./src/utils.js"));
}
};
var get = (module) => {
return (
__webpack_require__.o(moduleMap, module)
? moduleMap[module]()
: Promise.resolve().then(() => {
throw new Error("Module " + module + " does not exist in container.");
})
);
};
var override = (override) => {
Object.assign(__webpack_require__.O, override);
}
// This exports getters to disallow modifications
__webpack_require__.d(exports, {
get: () => get,
override: () => override
});
可以看到,代码中包括三个部分:
- moduleMap:通过exposes生成的模块集合
- get: 通过该函数,可以拿到remote中的组件
- webpack_require.d host通过该函数将依赖注入remote中
再看moduleMap,返回对应组件前,先通过__webpack_require__.e加载了其对应的依赖,让我们看看__webpack_require__.e做了什么:
/******/ __webpack_require__.f = {};
/******/ // This file contains only the entry chunk.
/******/ // The chunk loading function for additional chunks
/******/ __webpack_require__.e = (chunkId) => {
/******/ return Promise.all(Object.keys(__webpack_require__.f).reduce((promises, key) => {
/******/ __webpack_require__.f[key](chunkId, promises);
/******/ return promises;
/******/ }, []));
/******/ };
提供其他代码的解读,我们了解其使用的逻辑:
- Module federation让webpack以filename作为文件名生成文件
- 文件中以var的形式暴露了一个名为name的全局变量,其中包含了exposes以及shared中配置的内容
- 当我们使用该模块的应用时候,将自身shared写入remote中,再通过get获取remote中expose的组件,而作为remote时,判断使用该模块的应用中是否有可用的共享依赖,若有,则加载应用中的这部分依赖,若无,则加载自身依赖。
Module federation “模块联邦”主要应用场景
- 代码复用开箱解决方案
- 微前端,通过shared与remote提供公共依赖资源载入,减少线上体积与便于维护。
- 编译提速,可以将node_modules资源提前打包好,通过runtime方式引用,编译时只构建项目源文件。
- 多页应用资源复用,包括运行时依赖引入、组件复用、甚至整个页面共享。
参考文章:
Webpack 5 官网
探索webpack5新特性Module-federation
探索webpack5新特性Module-federation
Webpack 5 Module Federation: JavaScript 架构的变革者