Appearance
模块联邦打包后代码分析
一、案例
1. 远程模块
项目中分别导出两个工具方法
- add.js
- sub.js
模块联邦配置:
js
new ModuleFederationPlugin({
name: "lib_app",
filename: "remoteEntry.js",
exposes: {
"./add":"./src/add.js",
"./sub":"./src/sub.js"
}
})
new ModuleFederationPlugin({
name: "lib_app",
filename: "remoteEntry.js",
exposes: {
"./add":"./src/add.js",
"./sub":"./src/sub.js"
}
})
最后多产生了三个文件:
2. 本地项目
模块联邦配置如下:
js
new ModuleFederationPlugin({
name: "main_app",
remotes: {
'lib-app': 'lib_app@http://localhost:3000/remoteEntry.js'
}
})
new ModuleFederationPlugin({
name: "main_app",
remotes: {
'lib-app': 'lib_app@http://localhost:3000/remoteEntry.js'
}
})
示例代码:
js
import add from 'lib-app/add'
console.log(add(1, 2));
import add from 'lib-app/add'
console.log(add(1, 2));
二、remoteEntry 远程模块做了什么?
整个文件最后将导出的内容赋值给了lib_app
js
var lib_app;
(() => {
// ...各种代码
lib_app = __webpack_exports__;
})()
var lib_app;
(() => {
// ...各种代码
lib_app = __webpack_exports__;
})()
里面的内容重点有如下几部分
1. ensure chunk
用于加载chunk
js
(() => {
__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_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;
}, []));
};
})();
2. 模块映射表
这里是模块联邦定义,需要暴露出去的模块
js
var moduleMap = {
"./add": () => {
return __webpack_require__.e("src_add_js").then(() => (() => ((__webpack_require__(/*! ./src/add.js */ "./src/add.js")))));
},
"./sub": () => {
return __webpack_require__.e("src_sub_js").then(() => (() => ((__webpack_require__(/*! ./src/sub.js */ "./src/sub.js")))));
}
};
var moduleMap = {
"./add": () => {
return __webpack_require__.e("src_add_js").then(() => (() => ((__webpack_require__(/*! ./src/add.js */ "./src/add.js")))));
},
"./sub": () => {
return __webpack_require__.e("src_sub_js").then(() => (() => ((__webpack_require__(/*! ./src/sub.js */ "./src/sub.js")))));
}
};
3. get加载某个模块
js
var get = (module, getScope) => {
__webpack_require__.R = getScope;
getScope = (
__webpack_require__.o(moduleMap, module)
? moduleMap[module]()
: Promise.resolve().then(() => {
throw new Error('Module "' + module + '" does not exist in container.');
})
);
__webpack_require__.R = undefined;
return getScope;
};
var get = (module, getScope) => {
__webpack_require__.R = getScope;
getScope = (
__webpack_require__.o(moduleMap, module)
? moduleMap[module]()
: Promise.resolve().then(() => {
throw new Error('Module "' + module + '" does not exist in container.');
})
);
__webpack_require__.R = undefined;
return getScope;
};
- 初始化共享模块
js
var init = (shareScope, initScope) => {
if (!__webpack_require__.S) return;
var name = "default"
var oldScope = __webpack_require__.S[name];
if(oldScope && oldScope !== shareScope) throw new Error("Container initialization failed as it has already been initialized with a different share scope");
__webpack_require__.S[name] = shareScope;
return __webpack_require__.I(name, initScope);
};
var init = (shareScope, initScope) => {
if (!__webpack_require__.S) return;
var name = "default"
var oldScope = __webpack_require__.S[name];
if(oldScope && oldScope !== shareScope) throw new Error("Container initialization failed as it has already been initialized with a different share scope");
__webpack_require__.S[name] = shareScope;
return __webpack_require__.I(name, initScope);
};
三、exposes的模块做了什么
我们发现,其实exposes中的模块,都被单独打包成了chunk
js
(self["webpackChunkremote"] = self["webpackChunkremote"] || []).push([["src_add_js"],{
/***/ "./src/add.js":
/*!********************!*\
!*** ./src/add.js ***!
\********************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ "default": () => (/* binding */ add)
/* harmony export */ });
function add(a, b) {
return a + b
}
/***/ })
}]);
(self["webpackChunkremote"] = self["webpackChunkremote"] || []).push([["src_add_js"],{
/***/ "./src/add.js":
/*!********************!*\
!*** ./src/add.js ***!
\********************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ "default": () => (/* binding */ add)
/* harmony export */ });
function add(a, b) {
return a + b
}
/***/ })
}]);
四、本地模块是如何引入远程模块的
先来看下打包后的代码,下面只列出关键点
js
var __webpack_modules__ = ({
"webpack/container/reference/lib-app": ((module, __unused_webpack_exports, __webpack_require__) => {
// 创建一个error对象
var __webpack_error__ = new Error();
module.exports = new Promise((resolve, reject) => {
if(typeof lib_app !== "undefined") return resolve();
__webpack_require__.l("http://localhost:3000/remoteEntry.js", (event) => {
if(typeof lib_app !== "undefined") return resolve();
var errorType = event && (event.type === 'load' ? 'missing' : event.type);
var realSrc = event && event.target && event.target.src;
__webpack_error__.message = 'Loading script failed.\n(' + errorType + ': ' + realSrc + ')';
__webpack_error__.name = 'ScriptExternalLoadError';
__webpack_error__.type = errorType;
__webpack_error__.request = realSrc;
reject(__webpack_error__);
}, "lib_app");
}).then(() => (lib_app));
})
})
// ...其他代码
__webpack_require__(/*! lib-app/add */ "webpack/container/remote/lib-app/add")
// ...其他代码
var __webpack_modules__ = ({
"webpack/container/reference/lib-app": ((module, __unused_webpack_exports, __webpack_require__) => {
// 创建一个error对象
var __webpack_error__ = new Error();
module.exports = new Promise((resolve, reject) => {
if(typeof lib_app !== "undefined") return resolve();
__webpack_require__.l("http://localhost:3000/remoteEntry.js", (event) => {
if(typeof lib_app !== "undefined") return resolve();
var errorType = event && (event.type === 'load' ? 'missing' : event.type);
var realSrc = event && event.target && event.target.src;
__webpack_error__.message = 'Loading script failed.\n(' + errorType + ': ' + realSrc + ')';
__webpack_error__.name = 'ScriptExternalLoadError';
__webpack_error__.type = errorType;
__webpack_error__.request = realSrc;
reject(__webpack_error__);
}, "lib_app");
}).then(() => (lib_app));
})
})
// ...其他代码
__webpack_require__(/*! lib-app/add */ "webpack/container/remote/lib-app/add")
// ...其他代码
从上面的代码中,不难发现lib_app
其实是, 远程模块暴露出的变量。这一点从上面的分析就可以看到。
js
var lib_app;
(() => {
// ...各种代码
lib_app = __webpack_exports__;
})()
var lib_app;
(() => {
// ...各种代码
lib_app = __webpack_exports__;
})()
关键点在这一行, 最后返回了lib_app
js
__webpack_require__.l("http://localhost:3000/remoteEntry.js", (event) => {
// 一些错误处理
}, 'lib_app').then(() => (lib_app));
__webpack_require__.l("http://localhost:3000/remoteEntry.js", (event) => {
// 一些错误处理
}, 'lib_app').then(() => (lib_app));
loadJs加载远程模块
下面的代码就很简单了,直接创建了script标签,最后触发onload事件,执行最终的回调.
比如我们导入了lib-app/add
, 就加载add.js
的chunk
js
__webpack_require__.l = (url, done, key, chunkId) => {
if(inProgress[url]) { inProgress[url].push(done); return; }
var script, needAttach;
if(key !== undefined) {
var scripts = document.getElementsByTagName("script");
for(var i = 0; i < scripts.length; i++) {
var s = scripts[i];
if(s.getAttribute("src") == url || s.getAttribute("data-webpack") == dataWebpackPrefix + key) { script = s; break; }
}
}
if(!script) {
needAttach = true;
script = document.createElement('script');
script.charset = 'utf-8';
script.timeout = 120;
if (__webpack_require__.nc) {
script.setAttribute("nonce", __webpack_require__.nc);
}
script.setAttribute("data-webpack", dataWebpackPrefix + key);
script.src = url;
}
inProgress[url] = [done];
var onScriptComplete = (prev, event) => {
// avoid mem leaks in IE.
script.onerror = script.onload = null;
clearTimeout(timeout);
var doneFns = inProgress[url];
delete inProgress[url];
script.parentNode && script.parentNode.removeChild(script);
doneFns && doneFns.forEach((fn) => (fn(event)));
if(prev) return prev(event);
}
;
var timeout = setTimeout(onScriptComplete.bind(null, undefined, { type: 'timeout', target: script }), 120000);
script.onerror = onScriptComplete.bind(null, script.onerror);
script.onload = onScriptComplete.bind(null, script.onload);
needAttach && document.head.appendChild(script);
};
})();
__webpack_require__.l = (url, done, key, chunkId) => {
if(inProgress[url]) { inProgress[url].push(done); return; }
var script, needAttach;
if(key !== undefined) {
var scripts = document.getElementsByTagName("script");
for(var i = 0; i < scripts.length; i++) {
var s = scripts[i];
if(s.getAttribute("src") == url || s.getAttribute("data-webpack") == dataWebpackPrefix + key) { script = s; break; }
}
}
if(!script) {
needAttach = true;
script = document.createElement('script');
script.charset = 'utf-8';
script.timeout = 120;
if (__webpack_require__.nc) {
script.setAttribute("nonce", __webpack_require__.nc);
}
script.setAttribute("data-webpack", dataWebpackPrefix + key);
script.src = url;
}
inProgress[url] = [done];
var onScriptComplete = (prev, event) => {
// avoid mem leaks in IE.
script.onerror = script.onload = null;
clearTimeout(timeout);
var doneFns = inProgress[url];
delete inProgress[url];
script.parentNode && script.parentNode.removeChild(script);
doneFns && doneFns.forEach((fn) => (fn(event)));
if(prev) return prev(event);
}
;
var timeout = setTimeout(onScriptComplete.bind(null, undefined, { type: 'timeout', target: script }), 120000);
script.onerror = onScriptComplete.bind(null, script.onerror);
script.onload = onScriptComplete.bind(null, script.onload);
needAttach && document.head.appendChild(script);
};
})();
五、本地模块如何执行远程模块的
回顾一下示例代码:
js
import add from 'lib-app/add'
console.log(add(1, 2));
import add from 'lib-app/add'
console.log(add(1, 2));
被编译成了:
var lib_app_add__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! lib-app/add */ "webpack/container/remote/lib-app/add");
var lib_app_add__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(lib_app_add__WEBPACK_IMPORTED_MODULE_0__);
console.log(lib_app_add__WEBPACK_IMPORTED_MODULE_0___default()(1, 2));
最后直接调用了方法
拉取远程代码
- 获取当前chunk依赖的远程模块
- 根据远程模块,获取远程应用的ID, 然后webpack_require对应的模块代码,此时会去加载远程应用的入口文件
消费公共模块 获取拉取公共模块,并注册到modules中
拉取入口chunk代码