在webpack5中增加了一个新功能Module Federation(联邦模块),那么它到底是什么呢?我们来一探究竟
1.常规模式
在webpack5之前开发包一般有三种模式
常规lib安装
UMD安装
micro frontend(MFE)
上述三种方式使用广泛,优缺点就不再此赘述了。
2.Module Federation是什么
最近一个项目中,有一个业务场景,需要把每一个组件都打成包来供使用,利用UMD虽然解决了问题,但是解决起来还是很难受,机缘巧合之下发现了webpack5的这个新功能Module Federation(联邦模块)功能入下图:
这个功能是直接将一个应用的组件包,给另外一个应用来使用,同时支持和整体应用一起打包的公共抽取能力,类似于package center的应用。简单来说就是允许运行时动态决定代码的引入和加载。
2.2 demo
官方demo的地址:module-federation-examples
我们从中选择一个最基础的项目basic-host-remote进行安装,他的核心功能就是两个app互相引用对方的一个button组件,里边的目录大致如下
app1
---index.js 入口文件
---bootstrap.js 启动文件
---Button.js react组件
---App.js react组件
app2
---index.js 入口文件
---bootstrap.js 启动文件
---App.js react组件
---Button.js react组件
看起来一样是不是。我们看一下实际的区别APP1的webpack config如下
new ModuleFederationPlugin({
// 必填,唯一,给其他引用,类似于alias 引用为 app1/xxx
name: "app1",
// 构建后的chunk名字
filename: "remoteEntry.js",
// 引用方配置,下方为引用第三方文件配置
remotes: {
app2: "app2@http://localhost:3002/remoteEntry.js",
},
// 被引用方配置,暴露对外的module模块
exposes: {
"./Button": "./src/Button",
},
// 共享第三方资源
shared: {
...deps,
react: {
singleton: true,
},
"react-dom": {
singleton: true,
},
},
})
App2的如下
new ModuleFederationPlugin({
name: "app2",
filename: "remoteEntry.js",
remotes: {
app1: "app1@http://localhost:3001/remoteEntry.js",
},
exposes: {
"./Button": "./src/Button",
},
shared: {
...deps,
react: {
singleton: true,
},
"react-dom": {
singleton: true,
},
},
})
看完了配置,我们看下其他几个文件
/*APP.js*/
import LocalButton from "./Button";
import React from "react";
const RemoteButton = React.lazy(() => import("app1/Button"));
const App = () => (
<div>
<h1>Bi-Directional</h1>
<h2>App 2</h2>
<LocalButton />
<React.Suspense fallback="Loading Button">
<RemoteButton />
</React.Suspense>
</div>
);
export default App;
/*index.js*/
import("./bootstrap");
/*bootstrap.js*/
import App from "./App";
import React from "react";
import ReactDOM from "react-dom";
ReactDOM.render(<App />, document.getElementById("root"));
/*button.js*/
import React from "react";
const style = {
background: "#00c",
color: "#fff",
padding: 12,
};
const Button = () => <button style={style}>App 2 Button</button>;
export default Button;
/** APP1 END **/
App2的区别只有以下一行代码:
const RemoteButton = React.lazy(() => import("app1/Button"));
实际的运行效果
看一下生成页面的代码
我们来看下这个main.js里有什么乾坤
main.js内容:
1.modules转换成webpack_modules,
var __webpack_modules__ = ({
/***/ "webpack/container/reference/app2":
/*!************************************************************!*\
!*** external "app2@http://localhost:3002/remoteEntry.js" ***!
\************************************************************/
/*! unknown exports (runtime-defined) */
/*! runtime requirements: module, __webpack_require__.l, __webpack_require__.* */
/***/ ((module, __unused_webpack_exports, __webpack_require__) => {
"use strict";
eval("var error = new Error();
module.exports = new Promise((resolve, reject) => {
if(typeof app2 !== \"undefined\")
return resolve();
__webpack_require__.l(\"http://localhost:3002/remoteEntry.js\", (event) => {
if(typeof app2 !== \"undefined\") return resolve();
var errorType = event && (event.type === 'load' ? 'missing' : event.type);
var realSrc = event && event.target && event.target.src;
error.message = 'Loading script failed.\\n(' + errorType + ': ' + realSrc + ')';
error.name = 'ScriptExternalLoadError'; error.type = errorType;
error.request = realSrc; treject(error); }, \"app2\");\n})
.then(() => app2)\n\n//#
sourceURL=webpack://@automatic-vendor-sharing/app1/external_%22app2@http://localhost:3002/remoteEntry.js%22?");
})
});
2. 动态加载src_bootstrap_js
(() => {
/*!**********************!*\
!*** ./src/index.js ***!
\**********************//*! unknown exports (runtime-defined) *//*! runtime requirements: __webpack_require__.e,
__webpack_require__, __webpack_require__.* */
eval("__webpack_require__.e(/*! import() */ \"src_bootstrap_js\")
.then(__webpack_require__.bind(__webpack_require__,
/*! ./bootstrap */ \"./src/bootstrap.js\"));\n\n
//# sourceURL=webpack://@automatic-vendor-sharing/app1/./src/index.js?");})();
3.webpack_require.e做了什么呢
/* webpack/runtime/ensure chunk */
(() => {
__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;
}, []));
};
})();
4.又引出了f这个对象我们看下是什么
分别有三个方法,4.1:remotes:远程配置相关
__webpack_require__.f.remotes = (chunkId, promises) =>
{
if(__webpack_require__.o(chunkMapping, chunkId))
{
chunkMapping[chunkId].forEach((id) =>
{
if(installedModules[id])
return promises.push(installedModules[id]);
var data = idToExternalAndNameMapping[id];
var onError = (error) => {
if(!error) error = new Error("Container missing");
if(typeof error.message === "string")
error.message += '\nwhile loading "' + data[1] + '" from ' + data[2];
__webpack_modules__[id] = () => {
throw error;
}
installedModules[id] = 0;
};
var handleFunction = (fn, key, data, next, first) => {
try {
var promise = fn(key, data);
if(promise && promise.then) {
var p = promise.then((result) => next(result, data), onError);
if(first) promises.push(installedModules[id] = p); else return p;
} else {
return next(promise, data, first);
}
}
catch(error) { onError(error); } } var onExternal = (external, _, first) => external ?
handleFunction(__webpack_require__.I, data[0], external, onInitialized, first) : onError();
var onInitialized = (_, external, first) => handleFunction(external.get, data[1], external,
onFactory, first); var onFactory = (factory) => { installedModules[id] = 1;
__webpack_modules__[id] = (module) => { module.exports = factory(); } };
handleFunction(__webpack_require__, data[2], 1, onExternal, 1); }); } }
4.2.consumes方法。从代码得知应该和配置的 shared 有关
var chunkMapping = {
src_bootstrap_js: [
"webpack/sharing/consume/default/react-dom/react-dom",
"webpack/sharing/consume/default/react/react?2849"
],
webpack_sharing_consume_default_react_react: [
"webpack/sharing/consume/default/react/react?0085"
]
};
__webpack_require__.f.consumes = (chunkId, promises) => {
if (__webpack_require__.o(chunkMapping, chunkId)) {
chunkMapping[chunkId].forEach(id => {
if (__webpack_require__.o(installedModules, id))
return promises.push(installedModules[id]);
var onFactory = factory => {
installedModules[id] = 0;
__webpack_modules__[id] = module => {
delete __webpack_module_cache__[id];
module.exports = factory();
};
};
var onError = error => {
delete installedModules[id];
__webpack_modules__[id] = module => {
delete __webpack_module_cache__[id];
throw error;
};
};
try {
var promise = moduleToHandlerMapping[id]();
if (promise.then) {
promises.push(
(installedModules[id] = promise
.then(onFactory)
.catch(onError))
);
} else onFactory(promise);
} catch (e) {
onError(e);
}
});
}
};
4.3 webpack_require.f.j,从代码得知执行的就是JSONP加载chunk
__webpack_require__.f.j = (chunkId, promises) => { // JSONP chunk loading for javascript var installedChunkData = __webpack_require__.o(installedChunks, chunkId) ? installedChunks[chunkId] : undefined; if (installedChunkData !== 0) { // 0 means "already installed". // a Promise means "currently loading". if (installedChunkData) { promises.push(installedChunkData[2]); } else { if ( !/^webpack_(container_remote_app2_Button|sharing_consume_default_react_react)$/.test( chunkId ) ) { // setup Promise in chunk cache var promise = new Promise((resolve, reject) => { installedChunkData = installedChunks[chunkId] = [ resolve, reject ]; }); promises.push((installedChunkData[2] = promise)); // start chunk loading var url = __webpack_require__.p + __webpack_require__.u(chunkId); // create error before stack unwound to get useful stacktrace later var error = new Error(); var loadingEnded = event => { if (__webpack_require__.o(installedChunks, chunkId)) { installedChunkData = installedChunks[chunkId]; if (installedChunkData !== 0) installedChunks[chunkId] = undefined; if (installedChunkData) { var errorType = event && (event.type === "load" ? "missing" : event.type); var realSrc = event && event.target && event.target.src; error.message = "Loading chunk " + chunkId + " failed.\n(" + errorType + ": " + realSrc + ")"; error.name = "ChunkLoadError"; error.type = errorType; error.request = realSrc; installedChunkData[1](error); } } }; __webpack_require__.l(url, loadingEnded, "chunk-" + chunkId); } else installedChunks[chunkId] = 0; } }};
我们再来看下app1的remoteEntry:
var app1;app1 =
/******/ (() => { // webpackBootstrap
/******/ "use strict";
/******/ var __webpack_modules__ = ({
/***/ "webpack/container/entry/app1":
/*!***********************!*\
!*** container entry ***!
\***********************/
/*! unknown exports (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__) => {
// eval打包后的代码
eval("var moduleMap = {\n\t\"./Button\": () => {\n\t\treturn __webpack_require__.e(\"src_Button_js\").then(() => () => (__webpack_require__(/*! ./src/Button */ \"./src/Button.js\")));\n\t}\n};\nvar get = (module) => {\n\treturn (\n\t\t__webpack_require__.o(moduleMap, module)\n\t\t\t? moduleMap[module]()\n\t\t\t: Promise.resolve().then(() => {\n\t\t\t\tthrow new Error('Module \"' + module + '\" does not exist in container.');\n\t\t\t})\n\t);\n};\nvar init = (shareScope) => {\n\tvar oldScope = __webpack_require__.S[\"default\"];\n\tvar name = \"default\"\n\tif(oldScope && oldScope !== shareScope) throw new Error(\"Container initialization failed as it has already been initialized with a different share scope\");\n\t__webpack_require__.S[name] = shareScope;\n\treturn __webpack_require__.I(name);\n};\n\n// This exports getters to disallow modifications\n__webpack_require__.d(exports, {\n\tget: () => get,\n\tinit: () => init\n});\n\n//# sourceURL=webpack://@automatic-vendor-sharing/app1/container_entry?");
/*类似mainjs里的代码结构*/
return __webpack_require__("webpack/container/entry/app1");/******/ })()
app2的
var app2;app2 =/*基本类似app1*/
var get = (module) => {
\n\treturn (\n\t\t__webpack_require__.o(moduleMap, module)\n\t\t\t? moduleMap[module]()\n\t\t\t: Promise.resolve().then(() => {\n\t\t\t\tthrow new Error('Module \"' + module + '\" does not exist in container.');\n\t\t\t})\n\t);\n};\nvar init = (shareScope) => {\n\tvar oldScope = __webpack_require__.S[\"default\"];\n\tvar name = \"default\"\n\tif(oldScope && oldScope !== shareScope) throw new Error(\"Container initialization failed as it has already been initialized with a different share scope\");\n\t__webpack_require__.S[name] = shareScope;\n\treturn __webpack_require__.I(name);\n};\n\n// This exports getters to disallow modifications\n__webpack_require__.d(exports, {\n\tget: () => get,\n\tinit: () => init\n});\n\n//# sourceURL=webpack://@automatic-vendor-sharing/app2/container_entry?");/***/ })
return __webpack_require__("webpack/container/entry/app2");/******/ })()
加载流程:
通过以上源码的分析,我们大致就知道实现的流程了
- 先加载 main.js
- main.js 里面引用index.js
- index.js 这个 module 动态加载 src_bootstrap_js 这个 chunk
- 动态加载 src_bootstrap_js 这个 chunk 时,经过 consumes,判断依赖了 哪些文件,查看是否加载,没有加载就去加载对应的 js 文件。同时,经过 remotes,加载动态依赖
- 最后,经过 jsonp操作完成加载。
- 所有依赖以及 chunk 都加载完成后执行bootstrap.js
- app1和app2的remoteEntry分别在内部定义的get方法来require需要的代码(app2.get('Button') )。
细心的你一定发现indexjs和appjs之间有一个bootstrapjs。如果我们不用它的话那么会提示
Error: Shared module is not available for eager consumption
官网给出的是We strongly recommend using an asynchronous boundary. It will split out the initialization code of a larger chunk to avoid any additional round trips and improve performance in general.
通过上边我们的代码分析也能得到结论,需要依赖前置并且require进来。
结语
通过以上的分析,我们可以看出,这个功能具备了让webpack实现线上Runtime的效果,可以直接CDN进行共享,也不再需要构建发布了。
由于一些原因,笔者这次的项目没有使用该功能共享npm包来完成项目的开发上线,但是未来在版本稳定后会尝试该功能并分享踩坑等经验出来。
注:由于编写时webpack5的正式版还没发布,所以上述代码均为webpack5.0.0-beta.22版本下的情况。