背景
因为公司项目中用到webapck新特性 module federation(模块联邦),所以很好奇是怎么运行的,所以去研究了下运行时代码,故本文记录下。
基础概念
webpack.docschina.org/concepts/mo…
示例代码
Host代码
webpack.config.js
new ModuleFederationPlugin({
name: "main",
remotes: {
com: "com@http://localhost:3003/remoteEntry.js",
},
shared: {
react: {
singleton: true,
},
},
})
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"));
App.js
import React from "react";
import A from "com/a";
const App = () => {
return (
<div>
this is host
<A />
</div>
);
};
export default App;
复制代码
Remote代码
webpack.config.js
new ModuleFederationPlugin({
name: "com",
filename: "remoteEntry.js",
exposes: {
"./a": "./src/a",
},
shared: {
react: {
singleton: true,
},
},
})
a.js
import React from "react";
const A = () => {
return <div>remote</div>;
};
export default A;
复制代码
源码分析
首先提出几个疑惑?
我们先看Host build产物的入口js
"./src/index.js": (
__unused_webpack_module,
__unused_webpack_exports,
__webpack_require__
) => {
Promise.all([
__webpack_require__.e("vendors-node_modules_react-dom_index_js"),
__webpack_require__.e("src_bootstrap_js"),
]).then(
__webpack_require__.bind(__webpack_require__, "./src/bootstrap.js")
);
}
复制代码
这里使用了__webpack_require__.e去加载了react-dom和src_bootstrap_js,react-dom是因为splitChunk的缘故;而src_bootstrap_js是因为我们入口文件是import(xxx)这种写法。
webpack_require.e干了什么?
__webpack_require__.e = (chunkId) => {
/**
简单理解,就是并发的执行__webpack_require__.f上面的所有方法
*/
return Promise.all(
Object.keys(__webpack_require__.f).reduce((promises, key) => {
__webpack_require__.f[key](chunkId, promises);
return promises;
}, [])
);
}
复制代码
webpack_require.f有哪些方法?
- webpack_require.f.j:这个是利用script标签去加载其他js文件,不是本次关注对象
- webpack_require.f.remotes:去加载remote入口文件,然后将remote模块关联到runtime中。按照示例代码就是将com/a关联到runtime中
- webpack_require.f.consumes:让当前share的模块,关联到runtime中 可知remotes和consumes是实现的关键,我们一个一个分析
remotes
// map保存了文件中引用了哪些remote
var chunkMapping = {
src_bootstrap_js: ["webpack/container/remote/com/a"],
};
// 维护remote中的模块与remote的关系
var idToExternalAndNameMapping = {
"webpack/container/remote/com/a": [
"default",
"./a",
"webpack/container/reference/com",
],
};
__webpack_require__.f.remotes = (chunkId, promises) => {
// 判断当前id是否引用了remote
if (__webpack_require__.o(chunkMapping, chunkId)) {
chunkMapping[chunkId].forEach((id) => {
var getScope = __webpack_require__.R;
if (!getScope) getScope = [];
var data = idToExternalAndNameMapping[id];
if (getScope.indexOf(data) >= 0) return;
getScope.push(data);
if (data.p) return promises.push(data.p);
var onError = (error) => {···};
var handleFunction = (fn, arg1, arg2, d, next, first) => {···};
var onExternal = (external, _, first) => ···;
var onInitialized = (_, external, first) => ···;
var onFactory = (factory) => ···;
// data[2]是webpack/container/reference/com
// 请问webpack/container/reference/com文件,然后执行onExternal
handleFunction(__webpack_require__, data[2], 0, 0, onExternal, 1);
});
}
};
复制代码
webpack/container/reference/com是什么?
"webpack/container/reference/com":
(module, __unused_webpack_exports, __webpack_require__) => {
···
module.exports = new Promise((resolve, reject) => {
if (typeof com !== "undefined") return resolve();
// __webpack_require__.l 利用script标签,去加载js文件
__webpack_require__.l(
"http://localhost:3003/remoteEntry.js",
(event) => {
if (typeof com !== "undefined") return resolve();
···
},
"com"
);
}).then(() => com);
}
复制代码
可以得出webpack/container/reference/com其实就是remote的入口js文件
onExternal,onInitialized,onFactory这些方法是啥? 本质就是
graph LR
A[请求remote文件] --> B[onExternal 执行 __webpack_require__.I 下面分析];
B --> C[onInitialized调用module.get 加载对应remote];
C --> D[onFactory 将 remote结果 关联 到runtime中]
所以__webpack_require__.f.remotes其实干了2件事情
- 加载remote入口文件
- 将当前模块用到的remote 都关联 到runtime上
consumes
var moduleToHandlerMapping = {
// share模块的一些信息
"webpack/sharing/consume/default/react/react": () =>
loadSingletonVersionCheckFallback(...),
};
// 当前模块share了哪些模块
var chunkMapping = {
src_bootstrap_js: ["webpack/sharing/consume/default/react/react"],
};
__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) => {...};
var onError = (error) => {...};
try {
var promise = moduleToHandlerMapping[id]()
if (promise.then) {
promises.push(
(installedModules[id] = promise.then(onFactory).catch(onError))
);
}
...
});
}
}
复制代码
由代码可以看出,其实就是执行了loadSingletonVersionCheckFallback方法
var loadSingletonVersionCheckFallback = /*#__PURE__*/ init(
(scopeName, scope, key, version, fallback) => {
if (!scope || !__webpack_require__.o(scope, key)) return fallback();
return getSingletonVersion(scope, scopeName, key, version);
}
);
var init = (fn) =>
function (scopeName, a, b, c) {
var promise = __webpack_require__.I(scopeName);
if (promise && promise.then)
return promise.then(
fn.bind(fn, scopeName, __webpack_require__.S[scopeName], a, b, c)
);
return fn(scopeName, __webpack_require__.S[scopeName], a, b, c);
};
var getSingletonVersion = (scope, scopeName, key, requiredVersion) => {
// 获取对应版本信息
var version = findSingletonVersionKey(scope, key);
// 校验是否安全
if (!satisfy(requiredVersion, version))
typeof console !== "undefined" &&
console.warn &&
console.warn(
getInvalidSingletonVersionMessage(key, version, requiredVersion)
);
// 返回share模块内容
return get(scope[key][version]);
}
复制代码
由代码可以看出loadSingletonVersionCheckFallback其实就是调用了init高阶函数,init调用了webpack_require.I(下面讲),在执行完后,执行getSingletonVersion方法,从而得到正确版本的share模块内容。 获取完share模块内容后,执行onFactory,将share模块,关联到runtime中。
webpack_require.I
其实就是执行了
register(
"react",
"17.0.2",
() => () =>
__webpack_require__("./node_modules/react/index.js"),1);
initExternal("webpack/container/reference/com");
复制代码
register方法将share模块信息记录到__webpack_require__.S上
__webpack_require__.S = {
[scopeName: 默认为default]: {
share模块名: {
版本号: {
get: factory(加载函数)
}
}
}
}
复制代码
initExternal方法做了
- 等remoteEntry文件加载完成后
- 调用module.init方法,将__webpack_require__.S传入,module.init方法会消费__webpack_require__.S从而实现share模块共享
remoteEntry文件
var com;
...
/***/ "webpack/container/entry/com": /***/ (
__unused_webpack_module,
exports,
__webpack_require__
) => {
// remote中导出模块信息
var moduleMap = {
"./a": () => ...,
};
// 暴露remote中模块
var get = (module, getScope) => {...};
// 注册share模块到remote中
var init = (shareScope, initScope) => {...};
// 给exports上绑定get和init方法
__webpack_require__.d(exports, {
get: () => get,
init: () => init,
});
}
...
var __webpack_exports__ = __webpack_require__("webpack/container/entry/com");
// 通过往全局作用域上注册变量
com = __webpack_exports__;
复制代码
所以其实等remoteEntry文件文件加载完成后,全局作用域上会有com变量,它有get和init方法,可以让host使用。get方法其实就是将remote中模块暴露出去,init方法就是接收scope,从而实现share模块共享。
问题解答
问题1
所以我们上面的问题1的答案就是因为需要等remoteEntry文件加载完成之后,才能获取remote信息,所以需要在入口文件import,从而通过__webpack_require__.e方法先初始化remote信息,之后在开始加载本地代码,从而可以在本地加载remote模块。
问题2
问题2的答案就是,host先通过__webpack_require__.f.consumes方法在host中注册share模块到__webpack_require__.S中,然后等remoteEntry加载完后,将__webpack_require__.S中对应模块,传给module.init方法,module.init里面将share模块注册到remote中,从而实现host与remote共享模块。
总结
webpack federation是怎么运行的?
- 先并发的执行__webpack_require__.f.remotes和__webpack_require__.f.consumes方法
- 在remotes方法中加载remoteEntry,加载完成后通过module.get方法将remote中模块注册到host中
- 在consumes方法中先在host中注册share模块,然后等remoteEntry加载完成后,通过module.init方法将share模块传入,从而实现host和remote共享模块
- 在加载入口文件,正常执行host代码
第一次写文章,如果有不足之处,麻烦指出,谢谢。如果有收获,麻烦👍~~~