我们知道 Webpack 在项目打包的时候,可以通过设置 DLL 或者 Externals 来做代码共享时 Common Chunk,这些功能只能对于单独的项目可以实现,但对于不同应用和项目间这个任务就变得困难了,我们几乎无法在项目之间做到按需热插拔。无法实现跨项目,跨平台,跨框架去实现
Module federation “模块联邦”是 Webpack5 新内置的一个重要功能,可以让跨应用间真正做到模块共享,这里贴一篇,大家可以先去了解一下
Module federation allows a JavaScript application to dynamically run code from another bundle/build, on both client and server
什么是 “模块联邦” 功能。
“模块联邦”,它允许多个 webpack 构建一起工作。 从运行时的角度来看,多个构建的模块将表现得像一个巨大的连接模块图。 从开发者的角度来看,模块可以从指定的远程构建中导入,并以最小的限制来使用。使 JavaScript 应用得以在客户端或 服务器 上动态运行或者动态加载另一个 bundle.js 或者 build 之后生成的代码,且共享依赖。
代码是可以共享的,但每种情况都有降级方案。Module federated 可以总是加载自己的依赖,但在下载前会去尝试使用消费者的依赖。更少的代码冗余,依赖共享就像一个单一的 Webpack 构建。
描述的通俗一些就是允许运行时动态决定代码的引入和加载,且可以多个应用引用多个公共模板。且相互之间有可以互相引用,类似套娃这样的引用也是可以的,无需去每个项目中使用npm包插件的形式引入,可以直接引用其他项目的功能,我们可以把一些公共的部分抽离出来单独维护成一个公共的项目集合库,而这个公共的集合库,涉及模块升级改造一次,其他被使用到改模块的应用就全部都会自动更新完成,无需人为的去介入。例如npm 包插件的引入,每次升级npm 包的版本,就需要去各自的项目里面npm update xxx@版本号,而引用Module federated 这无需去这样操作。
UMD:Universal Module Definition(通用模块规范)是由社区想出来的一种整合了CommonJS和AMD两个模块定义规范的方法。
UMD:基本原理
用一个工厂函数来统一不同的模块定义规范。
UMD:原则
所有定义模块的方法需要单独传入依赖
所有定义模块的方法都需要返回一个对象,供其他模块使用
以下示例演示了如何使用UMD开发跨平台 UI (用户界面) 组件, 并同时避免客户代码与具体 UI 类之间的耦合。:
说简单点,这个就是设计模式中的工厂模式,UMD模块化规范方式就是采用这样的方式来实践的
共享模块方式这类就是我们开发的时候,我们经常会把某些功能封装成可复用的模块。模块封装了功能,并且对外暴露一个API,比如jquery,lodash等这类第三方插件,也可以通过CND的方式直接应用,然后页面上直接使用。后续这个公共函数的升级改造,我们更新CND引用链接地址,那么应用就自然也更新了。
npm 的方式共享模块,就是需要代码共享的项目中,需要将依赖使用npm方式安装到项目,然后进行 Webpack 打包构建再上线。
比如对于不同的项目 A 与 B,需要共享一个模块C时,最常见的办法就是将其抽成通用依赖并分别安装在各自项目中npm install C,然后本地编辑打包即可,版本更替更新模板C即可。
微前端: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 子模块,并不直接提供给用户使用。
注: 基于 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 应用的内部参数说明:
在引用远程资源的项目中使用时,需要先远程资源入口文件引入,可以异步加载,也可以使用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
},
上述引用的地址
当前的举例是应用都是再同一个目录下的,相对地址应用而已,而我们实际的项目开发过程中,比如我们用三个项目,那么肯定是三个不同的git地址,三个不同的仓库的,那么这些资源模块的加载就需要写成绝对地址引用,比如:https://www.xxx.com/zComp/dist/zComp-remote-entry.js。
具体的demo 可查看参考文章中的webpack5-module-federation-demo
注:我们对打包之后生成的 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,返回对应组件前,先通过__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;
/******/ }, []));
/******/ };
提供其他代码的解读,我们了解其使用的逻辑:
参考文章:
Webpack 5 官网
探索webpack5新特性Module-federation
探索webpack5新特性Module-federation
Webpack 5 Module Federation: JavaScript 架构的变革者
精读《Webpack5 新特性 - 模块联邦》
设计模式之工厂模式
webpack5-module-federation-demo