面試官:Webpack 究竟打包出來的是什么?
前言
Webpack 作為普遍使用的打包工具,在開發(fā)和發(fā)布的過程中產(chǎn)出的代碼結(jié)構(gòu),你是否關(guān)心過?本文為你揭開它的神秘面紗。
1、開發(fā)模式
一般情況,開發(fā)的過程都會(huì)使用 devServer 并開啟 hot 熱更新。假如我們有一個(gè)頁面入口文件 index.js 和依賴模塊 dateUtils.js,代碼如下:
src\pages\index\index.js
import dateUtils from '@/utils/dateUtils'
dateUtils.print()
src\utils\dateUtils.js
export default {
print() {
console.log('DateUtils.js==>>print', new Date())
}
}
Ok,我們來看打包后的代碼:
(function(modules) { // webpackBootstrap
function hotCreateRequire(moduleId) {
var me = installedModules[moduleId];
if (!me) return __webpack_require__;
var fn = function(request) {
//省略
}
return fn
}
function hotCreateModule(moduleId) {
}
// 模塊緩存,被執(zhí)行過的模塊都會(huì)放到這里面
var installedModules = {};
// require 函數(shù)
function __webpack_require__(moduleId) {
// 檢查模塊是否在緩存中,有就取出來返回模塊的 exports 屬性
if(installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
// 創(chuàng)建模塊并放入緩存
var module = installedModules[moduleId] = {
i: moduleId,
l: false,
exports: {},
hot: hotCreateModule(moduleId),
parents: (hotCurrentParentsTemp = hotCurrentParents, hotCurrentParents = [], hotCurrentParentsTemp),
children: []
};
// 執(zhí)行模塊
modules[moduleId].call(module.exports, module, module.exports, hotCreateRequire(moduleId));
// 修改標(biāo)識
module.l = true;
// 返回模塊的 exports 屬性
return module.exports;
}
// 省略代碼
// 加載入口模塊
return hotCreateRequire(0)(__webpack_require__.s = 0);
}
({
// 省略掉其他模塊代碼
"./src/pages/index/index.js":
(function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
var _utils_dateUtils__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! @/utils/dateUtils */ "./src/utils/dateUtils.js");
_utils_dateUtils__WEBPACK_IMPORTED_MODULE_0__["default"].print();
}),
"./src/utils/dateUtils.js":
(function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
__webpack_exports__["default"] = ({
print: function print() {
console.log('DateUtils.js==>>print', new Date());
}
});
}),
0:
(function(module, exports, __webpack_require__) {
__webpack_require__(/*! G:\WebDev\webpack-study\node_modules\webpack-dev-server\client\index.js?http://0.0.0.0:85 */"./node_modules/webpack-dev-server/client/index.js?http://0.0.0.0:85");
__webpack_require__(/*! G:\WebDev\webpack-study\node_modules\webpack\hot\dev-server.js */"./node_modules/webpack/hot/dev-server.js");
module.exports = __webpack_require__(/*! G:\WebDev\webpack-study\src\pages\index\index.js */"./src/pages/index/index.js");
})
});
打包后的代碼在瀏覽器中格式化之后非常多,對于我們來說,我們的關(guān)注放在代碼塊和打包后的代碼執(zhí)行流程上,精簡一下:
(function(modules) { // webpackBootstrap
函數(shù)體代碼
}
// 省略調(diào)其他模塊代碼
({
"./src/pages/index/index.js":
(function(module, __webpack_exports__, __webpack_require__) {
}),
"./src/utils/dateUtils.js":
(function(module, __webpack_exports__, __webpack_require__) {
}),
0:
(function(module, exports, __webpack_require__) {
})
});
我們看到打包后的代碼其實(shí)就是一個(gè) IIFE。這個(gè)函數(shù)接受一個(gè)對象類型的參數(shù),其中這個(gè)參數(shù)的 key 就是模塊路徑,value 則是對模塊代碼包裹后的一個(gè)函數(shù),該函數(shù)有幾個(gè)固定參數(shù)(這個(gè)其實(shí)可以解釋 Node 中模塊文件中 require 和 module 是如何來的?其實(shí)就是 Node 在模塊之外包裝了一層,把 require 和 module 給傳了進(jìn)來)。暫且先不管參數(shù)具體是什么,我們接著看函數(shù)體里是什么:
(function(modules) { // webpackBootstrap
function hotCreateRequire(moduleId) {
var me = installedModules[moduleId];
if (!me) return __webpack_require__;
var fn = function(request) {
//省略
}
return fn
}
function hotCreateModule(moduleId) {
}
// 模塊緩存,被執(zhí)行過的模塊都會(huì)放到這里面
var installedModules = {};
// require 函數(shù)
function __webpack_require__(moduleId) {
// 檢查模塊是否在緩存中,有就取出來返回模塊的 exports 屬性
if(installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
// 創(chuàng)建模塊并放入緩存
var module = installedModules[moduleId] = {
i: moduleId,
l: false,
exports: {},
hot: hotCreateModule(moduleId),
parents: (hotCurrentParentsTemp = hotCurrentParents, hotCurrentParents = [], hotCurrentParentsTemp),
children: []
};
// 執(zhí)行模塊
modules[moduleId].call(module.exports, module, module.exports, hotCreateRequire(moduleId));
// 修改標(biāo)識
module.l = true;
// 返回模塊的 exports 屬性
return module.exports;
}
// 省略代碼
// 加載入口模塊
return hotCreateRequire(0)(__webpack_require__.s = 0);
}
({
// 省略調(diào)其他模塊代碼
});
我們看函數(shù)體里定義了很多對象和方法,含有 hot 的部分基本上和熱更新模塊相關(guān)。這些是被添加進(jìn)來的代碼,最終上線是沒有的。我們直接看函數(shù)體的最后一行,它執(zhí)行了 hotCreateRequire(0)(__webpack_require__.s = 0)。這一行兩個(gè)括號,很明顯 hotCreateRequire 是返回了一個(gè)函數(shù)出來。我們接下來看看看 hotCreateRequire :
function hotCreateRequire(moduleId) {
var me = installedModules[moduleId];
if (!me) return __webpack_require__;
var fn = function(request) {
if (me.hot.active) {
if (installedModules[request]) {
if (installedModules[request].parents.indexOf(moduleId) === -1) {
installedModules[request].parents.push(moduleId);
}
} else {
hotCurrentParents = [moduleId];
hotCurrentChildModule = request;
}
if (me.children.indexOf(request) === -1) {
me.children.push(request);
}
} else {
console.warn(
"[HMR] unexpected require(" +
request +
") from disposed module " +
moduleId
);
hotCurrentParents = [];
}
return __webpack_require__(request);
};
}
//
return fn
}
啟動(dòng)的時(shí)候,傳入了 moduleId 是 0,在 installedModules 中找不到模塊,直接返回了?webpack_require__。然后繼續(xù)執(zhí)行這個(gè)返回的函數(shù),并傳入?yún)?shù) __webpack_require.s = 0,那么其實(shí)是執(zhí)行了下面這個(gè)函數(shù)代碼:
(function(module, exports, __webpack_require__) {
__webpack_require__(/*! G:\WebDev\webpack-study\node_modules\webpack-dev-server\client\index.js?http://0.0.0.0:85 */"./node_modules/webpack-dev-server/client/index.js?http://0.0.0.0:85");
__webpack_require__(/*! G:\WebDev\webpack-study\node_modules\webpack\hot\dev-server.js */"./node_modules/webpack/hot/dev-server.js");
module.exports = __webpack_require__(/*! G:\WebDev\webpack-study\src\pages\index\index.js */"./src/pages/index/index.js");
})
這個(gè)函數(shù),通過 __webpack_require__ 執(zhí)行了三個(gè)模塊,前兩個(gè)是在 devServer 添加的入口文件,為了實(shí)現(xiàn)開發(fā)和熱更新的一些功能。最后一行 moduel.exports 屬性是我們的 index.js 模塊執(zhí)行的返回,這樣就執(zhí)行到了我們的程序入口。
在 index.js 中我們看到通過 import 導(dǎo)入的 dateutils 也是通過 __webpack_require__ 進(jìn)行模塊引用的,并對里面的方法進(jìn)行了調(diào)用。那么,以此類推,所有的模塊引用都是這么實(shí)現(xiàn)的。
2、生產(chǎn)模式
我們把這個(gè)代碼使用生產(chǎn)模式進(jìn)行打包,可以得到如下代碼:
!function(e) {
// 模塊緩存
var t = {};
// 模塊require函數(shù)
function n(r) {
if (t[r])
return t[r].exports;
var o = t[r] = {
i: r,
l: !1,
exports: {}
};
return e[r].call(o.exports, o, o.exports, n),
o.l = !0,
o.exports
}
// 執(zhí)行入口
n(n.s = 0)
}([function(e, t, n) {
e.exports = n(1)
}
, function(e, t, n) {
"use strict";
n.r(t),
n(2).a.print()
}
, function(e, t, n) {
"use strict";
t.a = {
print: function() {
console.log("DateUtils.js==>>print", new Date)
}
}
}
]);
//# sourceMappingURL=entry_index~._m_nosources-source-map.min.js.map
因?yàn)?,我們的代碼足夠簡單,沒有其他的依賴模塊被打進(jìn)來。和開發(fā)模式類似,整體上它也是一個(gè) IIFE,只不過做了一些混淆和壓縮的處理。另外,參數(shù)變成了數(shù)組類型。同時(shí),去掉了開發(fā)模式下的一些輔助代碼。我們很容易和上面的東西做一些對應(yīng)(見注釋)。
3、再進(jìn)一步
OK,開發(fā)模式和生產(chǎn)模式的代碼結(jié)構(gòu)幾乎一樣。只是參數(shù)類型略有差別,開發(fā)模式下,key 是作為模塊的標(biāo)識來使用的。熱更新開啟后,修改模塊可以很輕易的修改該模塊的代碼。
另外,我們的項(xiàng)目代碼里無論是使用 require/module.exports 這種 ComomonJS 的模塊化方案,還是采用 import/export 的 ESM 模塊化方案。通過 webpack 打包最終其實(shí)是 ComomonJS 的模塊化方案,也就是說,它可以像 CommonJs 那樣進(jìn)行動(dòng)態(tài)模塊引用。
還有就是如果使用了 ESM 的 export (僅有 export.default 這種方式除外,它和 CommonJS 沒什么差別,都是只導(dǎo)出了一個(gè)對象出來),在打包后的代碼有一些區(qū)別。比如:
esmtest.js
export let flag = false
setTimeout(()=>{
flag = true
}, 1000)
index.js
const j = require('./js/esmtest')
console.log('0', j, j.flag) // f.falg 為 false
setTimeout(()=>{
const j = require('./js/esmtest')
console.log('2000', j, j.flag) // f.falg 為 true
}, 2000)
打包后代碼
"./src/pages/index/js/esmtest.js":
(function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "flag", function() { return flag; });
var flag = false;
var obj = {};
setTimeout(function () {
flag = true;
obj.objFlag = true;
}, 1000);
})
這里出來了 .d 和 .r 方法,這個(gè)在之前的函數(shù)體里有定義:
// 為 exports 定義 getter 函數(shù)
__webpack_require__.d = function(exports, name, getter) {
if(!__webpack_require__.o(exports, name)) {
Object.defineProperty(exports, name, { enumerable: true, get: getter });
}
};
// 在 exports 上定義 __esModule 屬性
__webpack_require__.r = function(exports) {
if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
}
Object.defineProperty(exports, '__esModule', { value: true });
};
Ok,通過定義 getter 函數(shù)的方式,我們在 esmtest.js 中對于 flag 的改動(dòng)代碼起了作用,隨后在 index.js 模塊?2s 后的定時(shí)器中的打印也說明了這一點(diǎn)。
最后一個(gè)是如果采用代碼分割或動(dòng)態(tài)引入的情況下,會(huì)怎么樣?我們直接上打包前的代碼:
index.js
import('./js/dynamicImport').then(dm=>{
dm.default.hello(123)
})
dynamicImport.js
export default {
hello(msg){
console.log('dynamicImport', msg)
}
}
我們再來看在開發(fā)模式下打包的代碼在瀏覽器里多了一個(gè) chunk 文件請求,里面打包后的代碼如下:
(window["webpackJsonp"] = window["webpackJsonp"] || []).push([[0],{
"./src/pages/index/js/dynamicImport.js":
(function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony default export */ __webpack_exports__["default"] = ({
hello: function hello(msg) {
console.log('dynamicImport', msg);
}
});
})
}]);
主體代碼沒什么差別,就是我們的 dynamicImport.js 模塊的代碼,然后外面調(diào)用了?window["webpackJsonp"].push?這個(gè)方法。那么這個(gè)方法是哪里來的,我們看一下 index.js 打包后的代碼部分:
(function(modules) { // webpackBootstrap
function webpackJsonpCallback(data) {
var chunkIds = data[0];
var moreModules = data[1];
// add "moreModules" to the modules object,
// then flag all "chunkIds" as loaded and fire callback
var moduleId, chunkId, i = 0, resolves = [];
for(;i < chunkIds.length; i++) {
chunkId = chunkIds[i];
if(Object.prototype.hasOwnProperty.call(installedChunks, chunkId) && installedChunks[chunkId]) {
resolves.push(installedChunks[chunkId][0]);
}
installedChunks[chunkId] = 0;
}
for(moduleId in moreModules) {
if(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
modules[moduleId] = moreModules[moduleId];
}
}
if(parentJsonpFunction) parentJsonpFunction(data);
while(resolves.length) {
resolves.shift()();
}
};
__webpack_require__.e = function requireEnsure(chunkId) {
var promises = [];
var installedChunkData = installedChunks[chunkId];
if(installedChunkData !== 0) { // 0 means "already installed".
if(installedChunkData) {
promises.push(installedChunkData[2]);
} else {
// 開始請求 chunk 文件
var script = document.createElement('script');
var onScriptComplete;
onScriptComplete = function (event) {
// avoid mem leaks in IE.
script.onerror = script.onload = null;
clearTimeout(timeout);
var chunk = installedChunks[chunkId];
if(chunk !== 0) {
if(chunk) {
var errorType = event && (event.type === 'load' ? 'missing' : event.type);
var realSrc = event && event.target && event.target.src;
error.message = 'Loading chunk ' + chunkId + ' failed.\n(' + errorType + ': ' + realSrc + ')';
error.name = 'ChunkLoadError';
error.type = errorType;
error.request = realSrc;
chunk[1](error);
}
installedChunks[chunkId] = undefined;
}
};
document.head.appendChild(script);
}
}
return Promise.all(promises);
};
var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || [];
jsonpArray.push = webpackJsonpCallback;
return hotCreateRequire(0)(__webpack_require__.s = 0);
}
({
"./src/pages/index/index.js":
(function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
__webpack_require__.e(/*! import() */ 0).then(__webpack_require__.bind(null, /*! ./js/dynamicImport */ "./src/pages/index/js/dynamicImport.js")).then(function (dm) {
console.log('dm', dm);
dm.default.hello(123);
});
}),
// 省略代碼
});
我們簡單看一下上面的這部分代碼。首先,這部分代碼先執(zhí)行,在 window 上掛載了一個(gè) window["webpackJsonp"],并為它定義了一個(gè) push 方法就是 webpackJsonpCallback,用于從異步請求的文件中加載模塊。此外,定義一個(gè)?__webpack_require__.e?方法去異步請求 chunk 文件,并返回一個(gè) Promise 對象。
至此,關(guān)于 Webpack 打包后的內(nèi)容部分的介紹全部結(jié)束,你學(xué)廢了嗎?
