博主最近一直在學習算法相關的內容,因此挺長一段時間沒有更新技術文章了,正好最近有個朋友問了我一個問題,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
規範的模塊, 反向引用也能夠。
本文若是對你有所幫助,請幫忙點個贊,感謝。