博主最近一直在学习算法相关的内容,因此挺长一段时间没有更新技术文章了,正好最近有个朋友问了我一个问题,webpack
是怎么实现模块化的?我也就顺便把这块相关的内容写成一篇掘文,分享给那些对这块内容不太清楚的同窗。前端
经过本文,你会搞清楚下面这些问题:webpack
webpack
的模块化实现import
会被webpack
编译成什么?import
引入commonjs
规范的模块?为何反向引用也能够?对于前端的模块化,相信你们都很熟悉。在如今的前端开发中,由于三大前端框架以及webpack
等一系列打包工具的普及,模块化的应用已是屡见不鲜。咱们再也不须要像之前用对象来定义js
模块,或者使用AMD
及CMD
的js
规范。如今在浏览器端,使用模块的方法就一个,import
。随着时代发展,如今已经有不少浏览器原生支持了import
语法,可是为了兼容性,咱们仍是须要经过webpack
来处理import
语法。web
PS:前不久尤大的vite2.0
已经正式发布了,构建速度真是快到飞起,相信这也是将来的主流打包构建方式。算法
咱们先来写个最简单的例子,来让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");
})();
复制代码
首先编译出来的这个代码就是一个自执行函数,里面的内容能够分为三部分。性能优化
modules
对象__webpack_require__
方法以及子方法的定义__webpack_require__
方法运行入口的index.js
文件这个对象里存放了全部你代码里写的做为一个个模块的js
,它以js
的文件路径做为key
,值为一个可执行的函数。前端框架
__webpack_require__
是一个关键的方法,负责实际的模块加载并执行这些模块内容,返回执行结果。它的子方法都是用来帮助模块的加载和执行。markdown
经过__webpack_require__
方法运行入口文件index.js
app
咱们如今从入口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('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
规范的文件会被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
规范的模块, 反向引用也能够。
本文若是对你有所帮助,请帮忙点个赞,感谢。