- 作者:老汪软件技巧
- 发表时间:2024-08-18 00:02
- 浏览量:
过程复现
在正式讲解原理之前,你可以先在本地尝试打包几个简单的 cjs 模块,并观察打包后的结果。
首先,我们创建两个模块文件,内容如下:
// console.js
exports.myConsole = function console(x) {
console.log(x);
}
exports.myWarn = function myWarn(x) {
console.warn(x);
}
// sum.js
const { myWarn, myConsole } = require('./console');
module.exports = function sum(a, b) {
myConsole(a + b);
myWarn(a + b);
}
接着,创建一个入口文件,内容如下:
// index.js
const sum = require('./sum');
const sum2 = require('./sum');
sum(2, 3);
sum2(1, 8);
你可能会觉得这些模块的写法比较凌乱,但其实是为了后续打包的结果能够展示尽可能多的情况。最后,创建一个打包脚本,内容如下:
// 需要先安装webpack依赖(`npm install webpack`)
const { webpack } = require("webpack");
const w = webpack({
entry: "./index.js",
mode: "none",
output: {
iife: false,
pathinfo: 'verbose'
},
})
w.run((err, stats) => {})
ok, 现在执行 node build.js, 你可以看见产生了一个dist目录,里面包含了打包后的文件(名字通常为main.js)
打开main.js,你会看到如下内容:
/******/ var __webpack_modules__ = ([
/* 0 */,
/* 1 */
/*!****************!*\
!*** ./sum.js ***!
\****************/
/*! unknown exports (runtime-defined) */
/*! runtime requirements: module, __webpack_require__ */
/*! CommonJS bailout: module.exports is used directly at 3:0-14 */
/***/ ((module, __unused_webpack_exports, __webpack_require__) => {
const { myWarn, console } = __webpack_require__(/*! ./console */ 2);
module.exports = function sum(a, b) {
myConsole(a + b);
myWarn(a + b);
}
/***/ }),
/* 2 */
/*!********************!*\
!*** ./console.js ***!
\********************/
/*! default exports */
/*! export console [provided] [no usage info] [missing usage info prevents renaming] */
/*! export myWarn [provided] [no usage info] [missing usage info prevents renaming] */
/*! other exports [not provided] [no usage info] */
/*! runtime requirements: __webpack_exports__ */
/***/ ((__unused_webpack_module, exports) => {
exports.console = function myConsole(x) {
console.log(x);
}
exports.myWarn = function myWarn(x) {
console.warn(x);
}
/***/ })
/******/ ]);
/************************************************************************/
/******/ // The module cache
/******/ var __webpack_module_cache__ = {};
/******/
/******/ // The require function
/******/ function __webpack_require__(moduleId) {
/******/ // Check if module is in cache
/******/ var cachedModule = __webpack_module_cache__[moduleId];
/******/ if (cachedModule !== undefined) {
/******/ return cachedModule.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__[moduleId](module, module.exports, __webpack_require__);
/******/
/******/ // Return the exports of the module
/******/ return module.exports;
/******/ }
/******/
/************************************************************************/
var __webpack_exports__ = {};
/*!******************!*\
!*** ./index.js ***!
\******************/
/*! unknown exports (runtime-defined) */
/*! runtime requirements: __webpack_require__ */
const sum = __webpack_require__(/*! ./sum */ 1);
const sum2 = __webpack_require__(/*! ./sum */ 1);
sum(2, 3);
sum2(1, 8);
原理解析
不需要感到恐慌,我们来一一拆解其中的内容。
__webpack_modules__
首先,webpack会将所有模块都打包到一个名为__webpack_modules__的数组中,数组的下标就是模块的id(0下标没有被使用), 数组的每一项都是一个副作用函数。你会发现,函数的内容其实就是每个模块文件中的内容,只是像require这样的关键字被替换成了__webpack_require__。
让我们再来观察函数的参数,参数的数量似乎不是统一的,但最多的情况下,函数的第一个参数是module(或者带有__unused前缀),第二个参数是module.exports(或者带有__unused前缀),第三个参数是__webpack_require__。
为什么数量不一呢,因为webpack在打包的时候会进行优化,如果模块没有使用到相应的关键字,就会剔除其对应的参数。比如console.js中没有用到require,webpack便会将第三个参数__webpack_require__移除。
至于__unused前缀,则完全是js位置参数的原因,必须要有一个占位符。
__webpack_module_cache__
__webpack_module_cache__是一个对象,用于缓存模块,当模块被多次引入时,webpack会直接从缓存中取出模块的导出对象,而不是重新执行模块。
__webpack_require__
__webpack_require__也就是require函数的webpack实现,它接收模块的id作为参数,然后返回模块的导出对象。过程很简单,先检查缓存,如果缓存中没有该模块的导出对象,则创建一个新的模块对象,这里的新建对象比较有意思,var module = __webpack_module_cache__[moduleId] = {exports: {}}是一个连等的形式,这在模块化系统中很常见,由于js中的对象赋值是引用传递,这样的形式使得module和__webpack_module_cache__[moduleId]缓存对象指向同一个对象。
后续__webpack_modules__[moduleId](module, module.exports, __webpack_require__);执行了模块函数,module被传入了,模块函数的副作用便是将模块的导出对象赋值给module.exports, 那么与此同时,缓存对象的exports属性也被赋值了,因为它们指向相同的对象。
最后,返回module.exports,也就是模块中导出的函数、值、对象等等。
const sum = __webpack_require__(/*! ./sum */ 1);
此处sum接收了导出的函数
function sum(a, b) {
myConsole(a + b);
myWarn(a + b);
}
这就是所有的流程啦
你可以在打包结果中进行一步步调试,从而有更加深刻的理解
彩蛋
我们发现,模块一旦导出,就会被缓存下来,那么也就是说,如果你导出了一个对象,那么你在所有文件中使用的该对象都是同一个对象,这也就意味着,如果你修改了这个对象,那么其他文件中也会受到影响。
可以尝试新增一个obj.js模块,其中导出了一个对象:
exports.obj = {
data: 1
}
然后在sum.js和index.js中引入该模块,并修改obj的属性:
// sum.js
const { myWarn, myConsole } = require('./console');
const { obj } = require('./obj');
module.exports = function sum(a, b) {
myConsole(a + b);
myWarn(a + b);
obj.data = a + b;
}
// index.js
const sum = require('./sum');
const sum2 = require('./sum');
const { obj } = require('./obj');
console.log(obj);
sum(2, 3);
console.log(obj);
sum2(1, 8);
console.log(obj);
其实,webpack中的模块实现和node中的基本一致,所以为了快速验证,你可以直接在node中运行index.js,然后观察输出结果。
可以看到,当obj的属性在sum模块中被修改后,index.js中的obj也被修改了。也就是说obj的状态是全局的。
同步更新:Kiameow的个人博客