前言
博主最近一直在学习算法相关的内容,所以挺长一段时间没有更新技术文章了,正好最近有个朋友问了我一个问题,webpack
是怎么实现模块化的?我也就顺便把这块相关的内容写成一篇掘文,分享给那些对这块内容不太清楚的同学。
通过本文,你会搞清楚下面这些问题:
- 1.
webpack
的模块化实现 - 2.
import
会被webpack
编译成什么? - 3.为什么你可以使用
import
引入commonjs
规范的模块?为什么反向引用也可以?
前端模块化
对于前端的模块化,相信大家都很熟悉。在现在的前端开发中,因为三大前端框架以及webpack
等一系列打包工具的普及,模块化的应用已经是家常便饭。我们不再需要像以前用对象来定义js
模块,或者使用AMD
及CMD
的js
规范。现在在浏览器端,使用模块的方法就一个,import
。随着时代发展,现在已经有很多浏览器原生支持了import
语法,但是为了兼容性,我们还是需要通过webpack
来处理import
语法。
PS:前不久尤大的vite2.0
已经正式发布了,构建速度真是快到飞起,相信这也是未来的主流打包构建方式。
import会被编译成什么
我们先来写个最简单的例子,来让webpack
编译一下。本文的例子使用的webpack5
编译,部分命名可能跟webpack4
有些许差异,但是模块化的思想是一致的。
// index.js
import { read } from './a';
import run from './b';
read();
run();
// a.js
export const read = () => {
console.log('阅读');
};
// b.js
export default run = () => {
console.log('跑步');
};
代码很简单,现在我们来看下,webpack
编译出来的代码是什么样的。`(去掉了很多注释)
(() => {
"use strict";
var __webpack_modules__ = ({
"./a.js": ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"read\": () => (/* binding */ read)\n/* harmony export */ });\nconst read = () => {\r\n console.log('阅读');\r\n};\n\n//# sourceURL=webpack://my-leetcode/./a.js?");
}),
"./b.js": ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"default\": () => (__WEBPACK_DEFAULT_EXPORT__)\n/* harmony export */ });\n/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (run = () => {\r\n console.log('跑步');\r\n});\n\n//# sourceURL=webpack://my-leetcode/./b.js?");
}),
"./index.js": ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _a__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./a */ \"./a.js\");\n/* harmony import */ var _b__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./b */ \"./b.js\");\n\r\n\r\n(0,_a__WEBPACK_IMPORTED_MODULE_0__.read)();\r\n(0,_b__WEBPACK_IMPORTED_MODULE_1__.default)();\n\n//# sourceURL=webpack://my-leetcode/./index.js?");
})
});
var __webpack_module_cache__ = {};
function __webpack_require__(moduleId) {
if(__webpack_module_cache__[moduleId]) {
return __webpack_module_cache__[moduleId].exports;
}
var module = __webpack_module_cache__[moduleId] = {
exports: {}
};
__webpack_modules__[moduleId](module, module.exports, __webpack_require__);
return module.exports;
}
(() => {
__webpack_require__.d = (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_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
})();
(() => {
__webpack_require__.r = (exports) => {
if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
}
Object.defineProperty(exports, '__esModule', { value: true });
};
})();
// 执行入口的index.js
var __webpack_exports__ = __webpack_require__("./index.js");
})();
首先编译出来的这个代码就是一个自执行函数,里面的内容可以分为三部分。
- 1.
modules
对象 - 2.
__webpack_require__
方法以及子方法的定义 - 3.通过
__webpack_require__
方法运行入口的index.js
文件
modules对象
这个对象里存放了所有你代码里写的作为一个个模块的js
,它以js
的文件路径作为key
,值为一个可执行的函数。
__webpack_require__方法以及子方法的定义
__webpack_require__
是一个关键的方法,负责实际的模块加载并执行这些模块内容,返回执行结果。它的子方法都是用来帮助模块的加载和执行。
运行index.js文件
通过__webpack_require__
方法运行入口文件index.js
webpack模块化实现
我们现在从入口index.js
开始,一步步跟随代码。
__webpack_require__("./index.js");
我们先来看看__webpack_require__
方法
// 模块缓存
var __webpack_module_cache__ = {};
// 传入引用模块的路径
function __webpack_require__(moduleId) {
// 如果引用的模块存在缓存,直接返回缓存内容
if(__webpack_module_cache__[moduleId]) {
return __webpack_module_cache__[moduleId].exports;
}
// 定义一个module对象,再给它初始化一个exports对象
var module = __webpack_module_cache__[moduleId] = {
exports: {}
};
// 运行__webpack_modules__里的相关模块,传入相关参数
__webpack_modules__[moduleId](module, module.exports, __webpack_require__);
return module.exports;
}
__webpack_require__
方法其实就是运行__webpack_modules__
里的相关模块。我们现在来看看index.js
模块的可执行函数。
"./index.js": ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _a__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./a */ \"./a.js\");\n/* harmony import */ var _b__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./b */ \"./b.js\");\n\r\n\r\n(0,_a__WEBPACK_IMPORTED_MODULE_0__.read)();\r\n(0,_b__WEBPACK_IMPORTED_MODULE_1__.default)();\n\n//# sourceURL=webpack://my-leetcode/./index.js?");
})
// 把eval里的代码提取出来,等价于
(__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
__webpack_require__.r(__webpack_exports__);
// 定义一个变量,通过__webpack_require__加载a.js文件
var _a__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./a.js");
// 定义一个变量,通过__webpack_require__加载b.js文件
var _b__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__("./b.js");
// 通过之前定义的变量,来运行相关的方法
(0,_a__WEBPACK_IMPORTED_MODULE_0__.read)();
(0,_b__WEBPACK_IMPORTED_MODULE_1__.default)();
}
里面的方法其实很简单,就是通过__webpack_require__
加载a.js
和b.js
,通过返回值来运行a.js
和b.js
模块里的方法。
我们现在来看看,__webpack_require__
是怎么加载a.js
和b.js
模块,并把它们内部的方法返回出来使用的。我们先从eval
中提取出相关函数。
// a.js
(__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
__webpack_require__.r(__webpack_exports__);
__webpack_require__.d(__webpack_exports__, { "read": () => read });
const read = () => { console.log('阅读'); };
}
// b.js
(__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
__webpack_require__.r(__webpack_exports__);
__webpack_require__.d(__webpack_exports__, { "default": () =>__WEBPACK_DEFAULT_EXPORT__ });
const __WEBPACK_DEFAULT_EXPORT__ = (run = () => { console.log('跑步') });
}
因为一个是read
方法是export
导出的,run
方法是export default
导出的,但是两者除了在命名上稍微有所区别,其他都一致。
首先,函数里,都存在我们写在模块里的业务代码,read
和run
。然后我们先重点来看下__webpack_require__.d
方法。
__webpack_require__.d = (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] });
}
}
};
//这里的重点其实就是一句话,把key的内容,定义到exports的get方法中
Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
关于Object.defineProperty
的内容不在本文讨论范围内,如果你不清楚这个方法,请先去了解一下它的使用。
我们再把a.js
和__webpack_require__.d
结合一下。
// a.js
(__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
__webpack_require__.r(__webpack_exports__);
// 这里的__webpack_exports__其实就是__webpack_require__里定义的module.exports。
// 这里就是把read方法定义到module.exports.read上
Object.defineProperty(__webpack_exports__, "read", { enumerable: true, get: read });
const read = () => { console.log('阅读'); };
}
这样定义之后
// index.js
// 这里__webpack_require__返回出来的module.exports.read上就定义了一个read方法
var _a__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./a.js");
// 后面自然就可以使用a.js里定义的read方法了。b.js也是相同的道理
_a__WEBPACK_IMPORTED_MODULE_0__.read()
其实就是相当于,webpack
将每一个模块暴露出来的方法,都定义在了各自的module.exports
对象上,然后返回出来,给其他的模块使用。通过这种方法,webpack
就实现了js
的模块化。
这不但跟Commonjs
的导出方法命名一样,实现上也是类似。Commonjs
中,每个js文件一创建,也会生成一个
var exports = module.exports = {}, 开发者定义的方法,都会定义到exports
或者module.exports
。
import懒加载实现
懒加载是前端非常常用的一种性能优化手段,使用上也很简单,只要import('xxx.js')
就行,现在我们来看下webpack
是怎么实现懒加载的。我们稍微改下之前的代码,然后再重新编译一下。
// index.js
import('./a.js').then(res => {
res.read();
})
// a.js
export const read = () => {
console.log('阅读');
};
编译之后,我们会发现除了主的js
文件之外,还会生成一个懒加载的时候需要加载的js
文件。
主文件步骤跟之前一致,还是通过__webpack_require__
加载index.js
文件。
这里的代码量比较大,详细的流程,我也不在这里贴代码了,总的来说,当用户触发其加载的动作时,会通过__webpack_require__.l
方法动态的在head
标签中创建一个script
标签,然后加载模块,通过script
标签的onload
和onerror
事件监听模块加载状态,如果完成,自动执行其中的代码。
commonjs的文件加载
接下来,我们看下commonjs
规范的文件会被webpack
编译成什么样,改造一下代码
// index.js
const a = require('./a');
const run = require('./b');
a.read();
run();
// a.js
exports.read = () => {
console.log('阅读');
};
// b.js
module.exports = run = () => {
console.log('跑步');
};
别的代码都一致,主要就来看下__webpack_modules__
对象中各个模块的key
对应的函数
// index.js
(__unused_webpack_module, __unused_webpack_exports, __webpack_require__) => {
const a = __webpack_require__("./a.js");
a.read();
const run = __webpack_require__("./b.js");
run();
}
// a.js
(__unused_webpack_module, exports) => {
exports.read = () => { console.log('阅读') };
}
// b.js
(module) => {
module.exports = run = () => { console.log('跑步'); };
},
编译之后的index.js
文件跟原来的文件,只是把require
换成了__webpack_require__
,其他没有变化。而a.js
和b.js
跟原来的代码是一模一样的。但是这里的exports
和module
是__webpack_require__
调用时候传入的。相当于,a.js
和b.js
都直接在__webpack_require__
的module.exports
上定义了相关的方法。那index.js
自然也就可以调用到这些方法了。
这也说明了,为什么可以使用import
引入commonjs
规范的模块,反向引用也可以。
总结
webpack
的模块化主要是通过__webpack_require__
方法,将各个模块里定义的方法,esm
定义的方法使用Object.defineProperty
,commonjs
定义的方法直接定义,最终都会统一加到自己定义的module.exports
对象上,然后返回出来,给其他的模块引用。
import
进来的文件经过webpack
打包以后会存放在一个对象里,key
为模块路径,value
为模块的可执行函数。import
懒加载会单独打成一个包,在需要加载的时候,动态进行加载。
因为webpack
会把import
的方法都会转换成__webpack_require__
方法,使用类似commonjs
规范的方式,获取其他模块里的方法。所以可以使用import
引入commonjs
规范的模块, 反向引用也可以。
感谢
本文如果对你有所帮助,请帮忙点个赞,感谢。