它是什么
Module Federation allows a JavaScript application to dynamically load code from another application and in the process, share dependencies.
Module Federation [ˌfedəˈreɪʃn] 使 一个JavaScript 应用在运行过程中可以动态加载另一个应用的代码,并支持共享依赖。
基本配置
既然要共享依赖,那么就需要知道依赖关系。Webpack Module Federation借助ModuleFederationPlugin进行依赖关系的配置,具体配置方式如下:
被依赖方
// 引入插件
const { ModuleFederationPlugin } = require("webpack").container;
// 插件配置
new ModuleFederationPlugin({
name: "app2", // 必传,且唯一,作为被依赖的key标志,依赖方使用方式 ${name}/${expose}
library: { type: "var", name: "app2" }, // library用于声明一个挂载的变量名(全局),其中这里的 name 为作为 umd 的 name
filename: "remoteEntry.js", // 构建后被依赖部分的入口文件名称
exposes: { // exposes暴露对外提供的modules模块
Button: "./src/Button",
},
shared: ["react", "react-dom"], // 声明共享的第三方依赖,声明后,依赖方使用时,优先使用依赖方自己的,如果没有使用被依赖方的
}),
依赖方
// webpack module federation配置
new ModuleFederationPlugin({
name: "app1",
library: { type: "var", name: "app1" },
remotes: { // 依赖方声明需要依赖的资源,对应被依赖方的key
app2: "app2",
},
shared: ["react", "react-dom"]
})
// 引用被依赖方暴露的资源文件
<script src="http://localhost:3002/remoteEntry.js"></script>
// 使用方式,通过import('远程资源包名称/模块名')的方式直接引入。
const RemoteButton = React.lazy(() => import("app2/Button"));
一个应用既可以是依赖方,也可以是被依赖方,还可以同时作为依赖方和被依赖方。
它做了些什么
讲module federation打包做了什么之前,先简单回顾下webpack的打包原理,便于理解module federation实现。
webpack的打包
核心概念
-
entry: 入口文件
-
module:模块,webpack里一切皆模块,一个模块对应一个文件
-
chunk:代码块,对应多个module,每一个打包结果都是一个chunk,每个chunk包含多个module。
-
loader:模块转换器,用于模块内容的转换。
-
plugin:插件,在构建流程中监听特定的事件来做一些处理。
和我们这次了解关系比较大的是module和chunk,chunk数量根据入口文件决定,动态加载的文件和提取的公共包也会生产新的chunk。
从小demo开始看打包代码
demo来自webpack module federation官方最简单的示例,我们先来看不使用module federation的情况下的打包结果。
demo很简单,文件列表大致如下:
// APP1
APP.js
Index.js
// APP2
Index.js
APP.js
Button.js
Test.js
对于没有引入module federation插件的情况下,APP1和APP2的打包没有区别,我们主要来看下APP2的打包。
文件关系很简单,入口文件是Index.js,内部import App.js,应用核心实现都在App.js。App.js内部引入Button.js和Test.js(动态引入)。
那么最终生成的文件为两个chunk,分别是:
- index.js、app.js、button.js组成的main.js
- test.js组成的src_test_js
来看源码,整个main.js打包后代码基本是这样:
可以看到,import 变成了 webpack_require,基本上所有的文件加载,核心都在__webpack_require__。
__webpack_require__实现
既然找到了核心,那么我们来看下__webpack_require__它是怎么实现的。
大致流程就是先看文件是否已经加载,没有加载就去加载,加载了就读取已经加载的数据,避免重复加载。
// main.js
/************************************************************************/
/******/ // The module cache,模块加载缓存
/******/ var __webpack_module_cache__ = {};
/******/
/******/ // The require function
/******/ function __webpack_require__(moduleId) {
/******/ // Check if module is in cache,先检查模块是否已加载,如果加载了直接返回已加载内容
/******/ if(__webpack_module_cache__[moduleId]) {
/******/ return __webpack_module_cache__[moduleId].exports;
/******/ }
/******/ // Create a new module (and put it into the cache),如果是第一次加载,那执行文件加载流程
/******/ var module = __webpack_module_cache__[moduleId] = {
/******/ // no module.id needed
/******/ // no module.loaded needed
/******/ exports: {}
/******/ };
/******/
/******/ // Execute the module function,去__webpack_modules__中查找对应的module,并加载
/******/ __webpack_modules__[moduleId](module, module.exports, __webpack_require__);
/******/
/******/ // Return the exports of the module
/******/ return module.exports;
/******/ }
/******/
/******/ // expose the modules object (__webpack_modules__)
/******/ __webpack_require__.m = __webpack_modules__;
/******/
/************************************************************************/
查看上述源码,会发现所有的模块加载,来自于__webpack_modules__,查找一下main.js,发现文件最上方有列出所有的__webpack_modules__,包含所有的依赖。
大致简化一下:
/// main.js
var __webpack_modules__ = ({
"./src/App.js": ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {}),
"./src/Button.js": ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {}),
"./node_modules/ajax/lib/ajax.js": ...,
"...": ...
})
查看"./src/App.js",能看到内部实现和main.js类似,其中有一段引入test.js不太一样。
// app.js
__webpack_require__.e(/*! import() */ "src_test_js").then(__webpack_require__.t.bind(__webpack_require__, /*! ./test.js */ "./src/test.js", 7));
实际上是套了一层__webpack_require__.e,用于加载src_test_js,然后执行完成后执行__webpack_require__。
那怎么把test.js里的modules放到main.js中呢?来看下test.js的打包结果。
// src_test_js
(window["webpackJsonp_basic_host_remote_app2"] = window["webpackJsonp_basic_host_remote_app2"] || []).push([["src_test_js"],{
/***/ "./src/test.js":
/*!*********************!*\
!*** ./src/test.js ***!
\*********************/
/*! unknown exports (runtime-defined) */
/*! exports [maybe provided (runtime-defined)] [maybe used (runtime-defined)] */
/*! runtime requirements: */
/***/ (() => {
console.log('this is App2');
/***/ })
}]);
有一个全局变量webpackJsonp_basic_host_remote_app2,数组内部包含src_test_js,及其对应的实现。
再来看main.js中,对webpackJsonp_basic_host_remote_app2的处理。
可以看到,它对push函数进行了重新定义,test.js加载完成后,会执行callback,拿到所有的module放在main中。
module federation
简单了解完基础的打包模块加载,我们来看下module federation。
webpack配置加上ModuleFederationPlugin,实现App1内部import App2的Button并展示。
其实module federation的核心也就是动态加载文件,但是基本的webpack打包结果,动态加载的module是存在于内部的,不能被外部访问。
如果我们需要在应用A获取应用B内部的模块,那就需要把B内部的模块暴露出来,加载过程中添加到应用A中。
接下来我们从打包结果来看下怎么实现的。
App2文件增加了remoteEntry.js,button.js和一依赖的文件包。
首先看App1引入App2的部分,App.js。
可以看到和正常的动态加载打包结果没什么区别。
那我们再来看main.js,发现webpack_require.f多了remotes,overridables这两个函数。加载过程中核心实现也就在这两部分。
- remotes:远程加载,和remotes配置有关。
// main.js(App1)
/******/ var installedModules = {}; // 缓存加载的module
/******/ var chunkMapping = { // 依赖的chunk和其内部依赖关系,告诉我们需要的button来自webpack/container/remote/app2/Button
/******/ "webpack_container_remote_app2_Button": [
/******/ "webpack/container/remote/app2/Button"
/******/ ]
/******/ };
/******/ var idToExternalAndNameMapping = { // 需要模块的所有依赖
/******/ "webpack/container/remote/app2/Button": [
/******/ "webpack/container/remote-overrides/a46c3e",
/******/ "webpack/container/reference/app2",
/******/ "Button"
/******/ ]
/******/ };
/******/ __webpack_require__.f.remotes = (chunkId, promises) => {
/******/ if(__webpack_require__.o(chunkMapping, chunkId)) { // 检查当前需要加载的chunk是否在配置项中声明为remote远程资源,如果是,则push
/******/ chunkMapping[chunkId].forEach((id) => {
/******/ if(__webpack_require__.o(installedModules, id)) return installedModules[id] && promises.push(installedModules[id]);
/******/ var data = idToExternalAndNameMapping[id]; // 根据资源map映射,找到其他需要加载的模块
/******/ var onError = (error) => {
/******/ if(error && typeof error.message === "string") error.message += '\nwhile loading "' + data[2] + '" from ' + data[1];
/******/ __webpack_modules__[id] = () => {
/******/ throw error;
/******/ }
/******/ delete installedModules[id];
/******/ };
/******/ var onFactory = (factory) => {
/******/ __webpack_modules__[id] = (module) => {
/******/ module.exports = factory(); // 直接export当前加载的模块
/******/ }
/******/ };
/******/ try { // 如果通过get方法能在另一个应用中找到对应modules,则异步加载远程资源使用,并缓存到当前__webpack_modules__对象上
/******/ var promise = __webpack_require__(data[0])(__webpack_require__(data[1])).get(data[2]);
/******/ if(promise && promise.then) {
/******/ promises.push(installedModules[id] = promise.then(onFactory, onError));
/******/ } else {
/******/ onFactory(promise);
/******/ }
/******/ } catch(error) {
/******/ onError(error);
/******/ }
/******/ });
/******/ }
/******/ }
/******/ })();
- overridables:覆盖加载,和shared有关。从源码也可以看到,shared模式会优先考虑当前已经拥有的资源,没有的话再去加载。
// main.js(App2)
/******/ __webpack_require__.f.overridables = (chunkId, promises) => {
/******/ if(__webpack_require__.o(chunkMapping, chunkId)) { // 判断当前加载的chunk是否在配置中声明为shard共享资源,如果能在__webpack_require__.o上找到,则使用当前资源,找不到则请求加载(
/******/ chunkMapping[chunkId].forEach((id) => {
/******/ promises.push(__webpack_require__.o(installedModules, id) ? installedModules[id] : installedModules[id] = Promise.resolve((__webpack_require__.O[idToNameMapping[id]] || fallbackMapping[id])()).then((factory) => {
/******/ installedModules[id] = 0;
/******/ __webpack_modules__[id] = (module) => {
/******/ module.exports = factory();
/******/ }
/******/ }))
/******/ });
/******/ }
/******/ }
/******/ })();
能看到当前App1依赖模块webpack/container/reference/app2,模块内容如下,直接了当的export了app2,
/***/ "webpack/container/reference/app2":
/*!***********************!*\
!*** external "app2" ***!
\***********************/
/*! unknown exports (runtime-defined) */
/*! exports [maybe provided (runtime-defined)] [maybe used (runtime-defined)] */
/*! runtime requirements: module */
/***/ ((module) => {
module.exports = app2;
/***/ }),
运行时大致简化一下就是:
__webpack_require__('webpack/container/remote-overrides/a46c3e')(__webpack_require__('webpack/container/reference/app2')).get('Button')
所以,最终就是app2.get('Button');那怎么知道app2下面有这些东西呢?核心就在我们一开始加载的remoteEntry.js,核心代码如下:
// remoteEntry.js
// 定义app2的变量
var app2;app2 =
/******/ (() => { // webpackBootstrap
/******/ "use strict";
/******/ var __webpack_modules__ = ({
/***/ "webpack/container/entry/app2":
/*!***********************!*\
!*** container entry ***!
\***********************/
/*! unknown exports (runtime-defined) */
/*! exports [maybe provided (runtime-defined)] [maybe used (runtime-defined)] */
/*! runtime requirements: __webpack_require__.d, __webpack_require__.o, __webpack_exports__, __webpack_require__.e, __webpack_require__, __webpack_require__.* */
/***/ ((__unused_webpack_module, exports, __webpack_require__) => {
// 提供模块关系
var moduleMap = {
"Button": () => {
return Promise.all([__webpack_require__.e("vendors-node_modules_ajax_lib_ajax_js"), __webpack_require__.e("src_Button_js")]).then(() => () => __webpack_require__(/*! ./src/Button */ "./src/Button.js"));
}
};
// 提供get函数,用来加载具体模块
var get = (module) => {
return (
__webpack_require__.o(moduleMap, module)
? moduleMap[module]()
: Promise.resolve().then(() => {
throw new Error('Module "' + module + '" does not exist in container.');
})
);
};
// override用于shared的模块覆盖
var override = (override) => {
Object.assign(__webpack_require__.O, override);
};
// This exports getters to disallow modifications
__webpack_require__.d(exports, {
get: () => get,
override: () => override
});
/***/ })
/******/ });
so,总结一下,核心主要是:
- 重新webpack_require.e,引入webpack_require.f上的overrides、remotes两个方法,实现不同应用直接的依赖加载、共享。
- 不同应用之间模块共享的本质是window全局变量,通过get和override方法来连接。
流程
App1的整个加载流程如下:
它能干什么
前端应用经常有重复的需求,我们来看下发展史(from某twitter上讲module federation的视频,地址查找ING):
Library时代->ESI(网页缓存)->MEF(微前端)->fedrated modules
有了Module federation后,可以很好的解决依赖和共享的问题,为应用之间相互依赖和加载提供了一种思路。但是依然有一些问题,比如:
- 包需要被export出来,才能使用,不是所有的包都支持umd,有一定成本。
- 不同的版本生成的公共库 id 不同,还是会导致重复加载
- 需要手动修改html里的引用,有维护成本。例如,app2 的 remotEntry 更新后如何获取最新地址
- 所有的应用在使用时,需要知道应用可被依赖的接口。
最后来看一个对比图,来自developer.aliyun.com/article/755…