webpack5之模块化原理
模块化原理
在 webpack5 中,我们既能使用 CommonJS
,又能使用ES Module
,还能支持其他如AMD
,CMD
,UMD
等模块化规则。那 webpack 怎么实现这些模块化呢。我们讲下目前主流的CommonJS
和ES Module
这两种模块化原理,我们可以分为四种导出引入方式并查找对应的实现机制。
CommonJS
模块化实现原理ES Module
实现原理CommonJS
加载ES Module
的原理ES Module
加载CommonJS
的原理
目前我们有两种导出文件 dateFormat.js
通过CommonJS
导出,priceFormat.js
通过ES Module
导出。
// dateFormat.js
const dateNow = () => {
const date = new Date();
return date.toLocaleString();
};
module.exports = {
dateNow,
};
// priceFormat.js
const add = (a, b) => {
return a + b;
};
const minus = (a, b) => {
return a - b;
};
export { add, minus };
CommonJS 模块化实现原理
我们在入口文件 main.js 引入 dateFormat.js。
// main.js
const { dateNow } = require("./js/dateFormat");
console.log(dateNow());
因为我们还需要能够查看 webpack 打包源码,因此还需要做以下配置
module.exports = {
//...
mode: "development",
devtool: "source-map",
//...
};
重新打包后我们可以查看 bundle.js 源码,并将一些无效注释去掉。
var __webpack_modules__ = {
"./src/js/dateFormat.js": function (module) {
const dateNow = () => {
const date = new Date();
return date.toLocaleString();
};
module.exports = {
dateNow,
};
},
};
// 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__ = {};
// This entry need to be wrapped in an IIFE because it need to be isolated against other modules in the chunk.
!(function () {
const { dateNow } = __webpack_require__(
/*! ./js/dateFormat */ "./src/js/dateFormat.js"
);
console.log(dateNow());
})();
//# sourceMappingURL=bundle.js.map
去掉注释后的源码已经很简单了,可以从中得知首先会定义所有模块 __webpack_modules__
,其中里面 key 为模块路径,value 为模块函数,里面存放了相应的执行函数。第二步会定义一个缓存空对象 __webpack_module_cache__
。第三步会定义一个引用模块方法 __webpack_require__
,最后一步才是真正执行我们要执行的方法,可以看到 const { dateNow } = __webpack_require__(/*! ./js/dateFormat */ "./src/js/dateFormat.js")
,调用了引用方法并将模块路径传入进去,在 __webpack_require__
这个引用方法中会先对缓存对象 __webpack_module_cache__
做判断,如果存在缓存则返回 cachedModule.exports
,否则就将 key 值: 模块路径和 value 值: { exports: {} }
放进缓存对象中,并赋值给 var module
,之后再调用模块对象 __webpack_modules__
取对应的模块,将 module
作为参数传进去,并返回 module.exports = { dateNow }
。最后返回 module.exports
,__webpack_require__
返回并将 dateNow
解构出来打印。
做个小结:
- 生成模块对象
__webpack_modules__
- 申明模块缓存对象
__webpack_module_cache__
- 申明模块引用函数
__webpack_require__
- 执行模块引用函数从
__webpack_modules__
模块对象里面返回方法并执行后返回属性方法对象
ES Module 模块化实现原理
现在改造 main.js 引入 priceFormat.js。
// main.js
import { add, minus } from './js/priceFormat'
console.log(add(2, 3))
console.log(minus(5, 3))
重新打包并去掉一些干扰的注视后可以看到
var __webpack_modules__ = {
"./src/js/priceFormat.js": function (
__unused_webpack_module,
__webpack_exports__,
__webpack_require__
) {
__webpack_require__.r(__webpack_exports__);
__webpack_require__.d(__webpack_exports__, {
add: function () {
return /* binding */ add;
},
minus: function () {
return /* binding */ minus;
},
});
const add = (a, b) => {
return a + b;
};
const minus = (a, b) => {
return a - b;
};
},
};
// The module cachevar
__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;
}
/* webpack/runtime/define property getters */
!(function () {
// define getter functions for harmony exports
__webpack_require__.d = function (exports, definition) {
for (var key in definition) {
if (
__webpack_require__.o(definition, key) &&
!__webpack_require__.o(exports, key)
) {
Object.defineProperty(exports, key, {
enumerable: true,
get: definition[key],
});
}
}
};
})();
/* webpack/runtime/hasOwnProperty shorthand */
!(function () {
__webpack_require__.o = function (obj, prop) {
return Object.prototype.hasOwnProperty.call(obj, prop);
};
})();
/* webpack/runtime/make namespace object */
!(function () {
// define __esModule on exports
__webpack_require__.r = function (exports) {
if (typeof Symbol !== "undefined" && Symbol.toStringTag) {
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
}
Object.defineProperty(exports, "__esModule", { value: true });
};
})();
var __webpack_exports__ = {};
// This entry need to be wrapped in an IIFE because it need to be isolated against other modules in the chunk.
!(function () {
/*!*********************!*\ !*** ./src/main.js ***! *********************/
__webpack_require__.r(__webpack_exports__);
/* harmony import */
var _js_priceFormat__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(
/*! ./js/priceFormat */ "./src/js/priceFormat.js"
);
console.log((0, _js_priceFormat__WEBPACK_IMPORTED_MODULE_0__.add)(2, 3));
console.log((0, _js_priceFormat__WEBPACK_IMPORTED_MODULE_0__.minus)(5, 3));
})();
//# sourceMappingURL=bundle.js.map
ES Module 模块话显然比 CommonJS 代码多了很对,我们来看下,首先也是先定义一个模块对象 __webpack_modules__
,里面存放着 ./src/js/priceFormat.js
模块,第二步,定义一个 __webpack_module_cache__
缓存对象,第三步定义引用模块函数 __webpack_require__
。可以看到第二步和第三步和 CommonJS
定义的一摸一样,之后又定义了几个函数 __webpack_require__.d
,__webpack_require__.o
,__webpack_require__.r
,这些函数之后执行到了再说,之后定义了__webpack_exports__
为空对象。在最后一个执行函数里面才开始真正执行我们需要用的代码。可以看到先调用__webpack_require__.r(__webpack_exports__)
方法。__webpack_require__.r
其实为传入的exports
打上 __esModule
属性,用来表示是 ES Module 模块,因为我们入口是 main.js 用 ES Module 模块化引入。最后执行引用方法 __webpack_require__
,传入模块路径 ./src/js/priceFormat.js
,通过之前的 CommonJS 介绍,引用方法里取的就是 __webpack_modules__
对应模块路径的函数并调用。在模块函数里面首先也是先调用了 __webpack_require__.r
方法,给导入的 module.exports
标识为 ES Module 模块,然后调用 __webpack_require__.d
方法,可以看到我们除了传入 module.exports
,还传入了definition: { "add": function () { return add; }, "minus": function () { return minus; }});
这个对象,可以看到这是一个导出的方法名为 key,value 为一个返回该导出方法的函数。我们看下这个 __webpack_require__.d
干了什么呢,首先遍历 definition
的 key,调用 __webpack_require__.o
方法判断 key 是否是传入该对象自身的属性,且在传入的module.exports
上没有该属性。如果条件满足,则对 module.exports
做一层代理,Object.defineProperty(exports, key, { enumerable: true, get: definition[key] })
,当我们访问 module.exports
上的属性方法时,它会从 definition
对象上取。
小结:
- 生成模块对象
__webpack_modules__
- 申明模块缓存对象
__webpack_module_cache__
- 申明模块引用函数
__webpack_require__
- 申明
__webpack_require__.d
,__webpack_require__.o
,__webpack_require__.r
- 执行模块引用函数从
__webpack_modules__
模块对象里面返回方法后执行 __webpack_modules__
取得的方法里面首先调用__webpack_require__.r
定义模块__esModule
属性,代表是 ES Module 模块。- 之后定义一个由属性方法为 key,value 为函数且返回该属性方法的对象
definition
和module.exports
一起传入__webpack_require__.d
方法里面 __webpack_require__.d
方法里面首先会对definition
和module.exports
通过__webpack_require__.o
方法判断属性的归属- 条件满足后执行
Object.defineProperty(exports, key, { enumerable: true, get: definition[key] })
对module.exports
做一层代理,当我们访问module.exports
上的属性方法时,它会从definition
对象上取。
剩下的CommonJS
加载ES Module
和 ES Module
加载CommonJS
的原理其实跟上述一样,对于 CommonJS 会直接从 __webpack_modules__
取,对于 ES Module
会对属性方法做一层代理,我们在访问 ES Module
引入的模块时,会代理到代理对象上面去取。