作者 | dabai
來(lái)源 |https://segmentfault.com/a/1190000016524677
前言
雖然每天都在用webpack,但一直覺(jué)得隔著一層神秘的面紗,對(duì)它的工作原理一直似懂非懂。它是如何用原生JS實(shí)現(xiàn)模塊間的依賴管理的呢?對(duì)于按需加載的模塊,它是通過(guò)什么方式動(dòng)態(tài)獲取的?打包完成后那一堆/******/開(kāi)頭的代碼是用來(lái)干什么的?本文將圍繞以上3個(gè)問(wèn)題,對(duì)照著源碼給出解答。
如果你對(duì)webpack的配置調(diào)優(yōu)感興趣,可以看看我之前寫的這篇文章:webpack調(diào)優(yōu)總結(jié)
模塊管理
先寫一個(gè)簡(jiǎn)單的JS文件,看看webpack打包后會(huì)是什么樣子:
// main.js
console.log('Hello Dickens');
// webpack.config.js
const path = require('path');
module.exports = {
entry: './main.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist')
}
};
在當(dāng)前目錄下運(yùn)行webpack,會(huì)在dist目錄下面生成打包好的bundle.js文件。去掉不必要的干擾后,核心代碼如下:
// webpack啟動(dòng)代碼
(function (modules) {
// 模塊緩存對(duì)象
var installedModules = {};
// webpack實(shí)現(xiàn)的require函數(shù)
function __webpack_require__(moduleId) {
// 檢查緩存對(duì)象,看模塊是否加載過(guò)
if (installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
// 創(chuàng)建一個(gè)新的模塊緩存,再存入緩存對(duì)象
var module = installedModules[moduleId] = {
i: moduleId,
l: false,
exports: {}
};
// 執(zhí)行模塊代碼
modules[moduleId].call(module.exports,
module, module.exports, __webpack_require__);
// 將模塊標(biāo)識(shí)為已加載
module.l = true;
// 返回export的內(nèi)容
return module.exports;
}
...
// 加載入口模塊
return __webpack_require__(__webpack_require__.s = 0);
})
([
/* 0 */
(function (module, exports) {
console.log('Hello Dickens');
})
]);
代碼是一個(gè)立即執(zhí)行函數(shù),參數(shù)modules是由各個(gè)模塊組成的數(shù)組,本例子只有一個(gè)編號(hào)為0的模塊,由一個(gè)函數(shù)包裹著,注入了module和exports2個(gè)變量(本例沒(méi)用到)。
核心代碼是__webpack_require__這個(gè)函數(shù),它的功能是根據(jù)傳入的模塊id,返回模塊export的內(nèi)容。模塊id由webpack根據(jù)文件的依賴關(guān)系自動(dòng)生成,是一個(gè)從0開(kāi)始遞增的數(shù)字,入口文件的id為0。
所有的模塊都會(huì)被webpack用一個(gè)函數(shù)包裹,按照順序存入上面提到的數(shù)組實(shí)參當(dāng)中。
模塊export的內(nèi)容會(huì)被緩存在installedModules中。當(dāng)獲取模塊內(nèi)容的時(shí)候,如果已經(jīng)加載過(guò),則直接從緩存返回,否則根據(jù)id從modules形參中取出模塊內(nèi)容并執(zhí)行,同時(shí)將結(jié)果保存到緩存對(duì)象當(dāng)中(將在下文講解)。
我們?cè)偬砑右粋€(gè)文件,在入口文件處導(dǎo)入,再來(lái)看看生成的啟動(dòng)文件是怎樣的。
// main.js
import logger from './logger';
console.log('Hello Dickens');
logger();
//logger.js
export default function log() {
console.log('Log from logger');
}
啟動(dòng)文件的模塊數(shù)組:
[
/* 0 */
(function (module, __webpack_exports__, __webpack_require__) {
'use strict';
Object.defineProperty(__webpack_exports__, '__esModule', {
value: true
});
/* harmony import */
var __WEBPACK_IMPORTED_MODULE_0__logger__ =
__webpack_require__(1);
console.log('Hello Dickens');
Object(__WEBPACK_IMPORTED_MODULE_0__logger
__['a' /* default */ ])();
}),
/* 1 */
(function (module, __webpack_exports__,
__webpack_require__) {
'use strict';
/* harmony export (immutable) */
__webpack_exports__['a'] = log;
function log() {
console.log('Log from logger');
}
})
]
可以看到現(xiàn)在有2個(gè)模塊,每個(gè)模塊的包裹函數(shù)都傳入了module, __webpack_exports__, __webpack_require__三個(gè)參數(shù),它們是通過(guò)上文提到的__webpack_require__注入的:
// 執(zhí)行模塊代碼
modules[moduleId].call(module.exports,
module, module.exports,
__webpack_require__);
執(zhí)行的結(jié)果也保存在緩存對(duì)象中了。
按需加載
再對(duì)代碼進(jìn)行改造,來(lái)研究webpack是如何實(shí)現(xiàn)動(dòng)態(tài)加載的:
// main.js
console.log('Hello Dickens');
import('./logger').then(logger => {
logger();
});
logger文件保持不變,編譯后比之前多出了1個(gè)chunk。
bundle_asy的內(nèi)容如下:
(function (modules) {
// 加載成功后的JSONP回調(diào)函數(shù)
var parentJsonpFunction = window['webpackJsonp'];
// 加載成功后的JSONP回調(diào)函數(shù)
window['webpackJsonp'] = function webpackJsonpCallback(
chunkIds, moreModules, executeModules) {
var moduleId, chunkId, i = 0,
resolves = [],
result;
for (; i <>
chunkId = chunkIds[i];
// installedChunks[chunkId]不為0且不為undefined,
將其放入加載成功數(shù)組
if (installedChunks[chunkId]) {
// promise的resolve
resolves.push(installedChunks[chunkId][0]);
}
// 標(biāo)記模塊加載完成
installedChunks[chunkId] = 0;
}
// 將動(dòng)態(tài)加載的模塊添加到modules數(shù)組中,以供后續(xù)的require使用
for (moduleId in moreModules) {
if (Object.prototype.hasOwnProperty.call(moreModules,
moduleId)) {
modules[moduleId] = moreModules[moduleId];
}
}
if (parentJsonpFunction) parentJsonpFunction(chunkIds,
moreModules, executeModules);
while (resolves.length) {
resolves.shift()();
}
};
// 模塊緩存對(duì)象
var installedModules = {};
// 記錄正在加載和已經(jīng)加載的chunk的對(duì)象,0表示已經(jīng)加載成功
// 1是當(dāng)前模塊的編號(hào),已加載完成
var installedChunks = {
1: 0
};
// require函數(shù),跟上面的一樣
function __webpack_require__(moduleId) {
if (installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
var module = installedModules[moduleId] = {
i: moduleId,
l: false,
exports: {}
};
modules[moduleId].call(module.exports, module,
module.exports, __webpack_require__);
module.l = true;
return module.exports;
}
// 按需加載,通過(guò)動(dòng)態(tài)添加script標(biāo)簽實(shí)現(xiàn)
__webpack_require__.e = function requireEnsure(chunkId) {
var installedChunkData = installedChunks[chunkId];
// chunk已經(jīng)加載成功
if (installedChunkData === 0) {
return new Promise(function (resolve) {
resolve();
});
}
// 加載中,返回之前創(chuàng)建的promise(數(shù)組下標(biāo)為2)
if (installedChunkData) {
return installedChunkData[2];
}
// 將promise相關(guān)函數(shù)保持到installedChunks中方便后續(xù)resolve或reject
var promise = new Promise(function (resolve, reject) {
installedChunkData = installedChunks[chunkId] = [resolve,
reject];
});
installedChunkData[2] = promise;
// 啟動(dòng)chunk的異步加載
var head = document.getElementsByTagName('head')[0];
var script = document.createElement('script');
script.type = 'text/javascript';
script.charset = 'utf-8';
script.async = true;
script.timeout = 120000;
if (__webpack_require__.nc) {
script.setAttribute('nonce', __webpack_require__.nc);
}
script.src = __webpack_require__.p + '' + chunkId +
'.bundle_async.js';
script.onerror = script.onload = onScriptComplete;
var timeout = setTimeout(onScriptComplete, 120000);
function onScriptComplete() {
script.onerror = script.onload = null;
clearTimeout(timeout);
var chunk = installedChunks[chunkId];
// 正常的流程,模塊加載完后會(huì)調(diào)用webpackJsonp方法,將chunk置為0
// 如果不為0,則可能是加載失敗或者超時(shí)
if (chunk !== 0) {
if (chunk) {
// 調(diào)用promise的reject
chunk[1](new Error('Loading chunk ' + chunkId +
' failed.'));
}
installedChunks[chunkId] = undefined;
}
};
head.appendChild(script);
return promise;
};
...
// 加載入口模塊
return __webpack_require__(__webpack_require__.s = 0);
})
([
/* 0 */
(function (module, exports, __webpack_require__) {
console.log('Hello Dickens');
// promise resolve后,會(huì)指定加載哪個(gè)模塊
__webpack_require__.e /* import() */(0)
.then(__webpack_require__.bind(null, 1))
.then(logger => {
logger();
});
})
]);
掛在到window下面的webpackJsonp函數(shù)是動(dòng)態(tài)加載模塊代碼下載后的回調(diào),它會(huì)通知webpack模塊下載完成并將模塊加入到modules當(dāng)中。
__webpack_require__.e函數(shù)是動(dòng)態(tài)加載的核心實(shí)現(xiàn),它通過(guò)動(dòng)態(tài)創(chuàng)建一個(gè)script標(biāo)簽來(lái)實(shí)現(xiàn)代碼的異步加載。加載開(kāi)始前會(huì)創(chuàng)建一個(gè)promise存到installedChunks對(duì)象當(dāng)中,加載成功則調(diào)用resolve,失敗則調(diào)用reject。resolve后不會(huì)傳入模塊本身,而是通過(guò)__webpack_require__來(lái)加載模塊內(nèi)容,require的模塊id由webpack來(lái)生成:
__webpack_require__.e /* import() */(0)
.then(__webpack_require__.bind(null, 1))
.then(logger => {
logger();
});
接下來(lái)看下動(dòng)態(tài)加載的chunk的代碼,0.bundle_asy的內(nèi)容如下:
webpackJsonp([0], [
/* 0 */
,
/* 1 */
(function (module, __webpack_exports__,
__webpack_require__) {
'use strict';
Object.defineProperty(__webpack_exports__, '__esModule', {
value: true
});
/* harmony export (immutable) */
__webpack_exports__['default'] = log;
function log() {
console.log('Log from logger');
}
})
]);
代碼非常好理解,加載成功后立即調(diào)用上文提到的webpackJsonp方法,將chunkId和模塊內(nèi)容傳入。
這里要分清2個(gè)概念,一個(gè)是chunkId,一個(gè)moduleId。這個(gè)chunk的chunkId是0,里面只包含一個(gè)module,moduleId是1。一個(gè)chunk里面可以包含多個(gè)module。
總結(jié)
本文通過(guò)分析webpack生成的啟動(dòng)代碼,講解了webpack是如何實(shí)現(xiàn)模塊管理和動(dòng)態(tài)加載的,希望對(duì)你有所幫助。
如果你對(duì)webpack的配置調(diào)優(yōu)感興趣,可以看看我之前寫的這篇文章:webpack調(diào)優(yōu)總結(jié)。
本文完~
聯(lián)系客服