【webpack 進(jìn)階】Webpack 打包后的代碼是怎樣的?
webpack 是我們現(xiàn)階段要掌握的重要的打包工具之一,我們知道 webpack 會遞歸的構(gòu)建依賴關(guān)系圖,其中包含應(yīng)用程序的每個模塊,然后將這些模塊打包成一個或者多個 bundle。
那么webpack 打包后的代碼是怎樣的呢?是怎么將各個 bundle連接在一起的?模塊與模塊之間的關(guān)系是怎么處理的?動態(tài) import() 的時候又是怎樣的呢?
本文讓我們一步步來揭開 webpack 打包后代碼的神秘面紗
準(zhǔn)備工作
創(chuàng)建一個文件,并初始化
mkdir learn-webpack-output
cd learn-webpack-output
npm init -y
yarn add webpack webpack-cli -D
根目錄中新建一個文件 webpack.config.js,這個是 webpack 默認(rèn)的配置文件
const path = require('path');
module.exports = {
mode: 'development', // 可以設(shè)置為 production
// 執(zhí)行的入口文件
entry: './src/index.js',
output: {
// 輸出的文件名
filename: 'bundle.js',
// 輸出文件都放在 dist
path: path.resolve(__dirname, './dist')
},
// 為了更加方便查看輸出
devtool: 'cheap-source-map'
}
然后我們回到 package.json 文件中,在 npm script 中添加啟動 webpack 配置的命令
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "webpack"
}
新建一個 src文件夾,新增 index.js 文件和 sayHello 文件
// src/index.js
import sayHello from './sayHello';
console.log(sayHello, sayHello('Gopal'));
// src/sayHello.js
function sayHello(name) {
return `Hello ${name}`;
}
export default sayHello;
一切準(zhǔn)備完畢,執(zhí)行 yarn build
分析主流程
看輸出文件,這里不放具體的代碼,有點(diǎn)占篇幅,可以點(diǎn)擊這里查看[1]
其實(shí)就是一個 IIFE
莫慌,我們一點(diǎn)點(diǎn)拆分開看,其實(shí)總體的文件就是一個 IIFE——立即執(zhí)行函數(shù)。
(function(modules) { // webpackBootstrap
// The module cache
var installedModules = {};
function __webpack_require__(moduleId) {
// ...省略細(xì)節(jié)
}
// 入口文件
return __webpack_require__(__webpack_require__.s = "./src/index.js");
})
({
"./src/index.js": (function(module, __webpack_exports__, __webpack_require__) {}),
"./src/sayHello.js": (function(module, __webpack_exports__, __webpack_require__) {})
});
函數(shù)的入?yún)?modules 是一個對象,對象的 key 就是每個 js 模塊的相對路徑,value 就是一個函數(shù)(我們下面稱之為模塊函數(shù))。IIFE 會先 require 入口模塊。即上面就是 ./src/index.js:
// 入口文件
return __webpack_require__(__webpack_require__.s = "./src/index.js");
然后入口模塊會在執(zhí)行時 require 其他模塊例如 ./src/sayHello.js"以下為簡化后的代碼,從而不斷的加載所依賴的模塊,形成依賴樹,比如如下的模塊函數(shù)中就引用了其他的文件 sayHello.js
{
"./src/index.js": (function(module, __webpack_exports__, __webpack_require__) {
__webpack_require__.r(__webpack_exports__);
var _sayHello__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/sayHello.js");
console.log(_sayHello__WEBPACK_IMPORTED_MODULE_0__["default"],
Object(_sayHello__WEBPACK_IMPORTED_MODULE_0__["default"])('Gopal'));
})
}
重要的實(shí)現(xiàn)機(jī)制——__webpack_require__
這里去 require 其他模塊的函數(shù)主要是 __webpack_require__ 。接下來主要介紹一下 __webpack_require__ 這個函數(shù)
// 緩存模塊使用
var installedModules = {};
// The require function
// 模擬模塊的加載,webpack 實(shí)現(xiàn)的 require
function __webpack_require__(moduleId) {
// Check if module is in cache
// 檢查模塊是否在緩存中,有則直接從緩存中獲取
if(installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
// Create a new module (and put it into the cache)
// 沒有則創(chuàng)建并放入緩存中,其中 key 值就是模塊 Id,也就是上面所說的文件路徑
var module = installedModules[moduleId] = {
i: moduleId, // Module ID
l: false, // 是否已經(jīng)執(zhí)行
exports: {}
};
// Execute the module function
// 執(zhí)行模塊函數(shù),掛載到 module.exports 上。this 指向 module.exports
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
// Flag the module as loaded
// 標(biāo)記這個 module 已經(jīng)被加載
module.l = true;
// Return the exports of the module
// module.exports通過在執(zhí)行module的時候,作為參數(shù)存進(jìn)去,然后會保存module中暴露給外界的接口,如函數(shù)、變量等
return module.exports;
}
第一步,webpack 這里做了一層優(yōu)化,通過對象 installedModules 進(jìn)行緩存,檢查模塊是否在緩存中,有則直接從緩存中獲取,沒有則創(chuàng)建并放入緩存中,其中 key 值就是模塊 Id,也就是上面所說的文件路徑
第二步,然后執(zhí)行模塊函數(shù),將 module, module.exports, __webpack_require__ 作為參數(shù)傳遞,并把模塊的函數(shù)調(diào)用對象指向 module.exports,保證模塊中的 this 指向永遠(yuǎn)指向當(dāng)前的模塊。
第三步,最后返回加載的模塊,調(diào)用方直接調(diào)用即可。
所以這個__webpack_require__就是來加載一個模塊,并在最后返回模塊 module.exports 變量
webpack 是如何支持 ESM 的
可能大家已經(jīng)發(fā)現(xiàn),我上面的寫法是 ESM 的寫法,對于模塊化的一些方案的了解,可以看看我的另外一篇文章【面試說】Javascript 中的 CJS, AMD, UMD 和 ESM是什么?[2]
我們重新看回模塊函數(shù)
{
"./src/index.js": (function(module, __webpack_exports__, __webpack_require__) {
__webpack_require__.r(__webpack_exports__);
var _sayHello__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/sayHello.js");
console.log(_sayHello__WEBPACK_IMPORTED_MODULE_0__["default"],
Object(_sayHello__WEBPACK_IMPORTED_MODULE_0__["default"])('Gopal'));
})
}
我們看看 __webpack_require__.r 函數(shù)
__webpack_require__.r = function(exports) {
object.defineProperty(exports, '__esModule', { value: true });
};
就是為 __webpack_exports__ 添加一個屬性 __esModule,值為 true
再看一個 __webpack_require__.n 的實(shí)現(xiàn)
// getDefaultExport function for compatibility with non-harmony modules
__webpack_require__.n = function(module) {
var getter = module && module.__esModule ?
function getDefault() { return module['default']; } :
function getModuleExports() { return module; };
__webpack_require__.d(getter, 'a', getter);
return getter;
};
__webpack_require__.n會判斷module是否為es模塊,當(dāng)__esModule為 true 的時候,標(biāo)識 module 為es 模塊,默認(rèn)返回module.default,否則返回module。
最后看 __webpack_require__.d,主要的工作就是將上面的 getter 函數(shù)綁定到 exports 中的屬性 a 的 getter 上
// define getter function for harmony exports
__webpack_require__.d = function(exports, name, getter) {
if(!__webpack_require__.o(exports, name)) {
Object.defineProperty(exports, name, {
configurable: false,
enumerable: true,
get: getter
});
}
};
我們最后再看會 sayHello.js 打包后的模塊函數(shù),可以看到這里的導(dǎo)出是 __webpack_exports__["default"] ,實(shí)際上就是 __webpack_require__.n 做了一層包裝來實(shí)現(xiàn)的,其實(shí)也可以看出,實(shí)際上 webpack 是可以支持 CommonJS 和 ES Module 一起混用的
"./src/sayHello.js":
/*! exports provided: default */
(function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
function sayHello(name) {
return `Hello ${name}`;
}
/* harmony default export */ __webpack_exports__["default"] = (sayHello);
})
目前為止,我們大致知道了 webpack 打包出來的文件是怎么作用的了,接下來我們分析下代碼分離的一種特殊場景——動態(tài)導(dǎo)入
動態(tài)導(dǎo)入
代碼分離是 webpack 中最引人注目的特性之一。此特性能夠把代碼分離到不同的 bundle 中,然后可以按需加載或并行加載這些文件。代碼分離可以用于獲取更小的 bundle,以及控制資源加載優(yōu)先級,如果使用合理,會極大影響加載時間。
常見的代碼分割有以下幾種方法:
入口起點(diǎn):使用 `entry`[3] 配置手動地分離代碼。 防止重復(fù):使用 Entry dependencies[4] 或者 `SplitChunksPlugin`[5] 去重和分離 chunk。 動態(tài)導(dǎo)入:通過模塊的內(nèi)聯(lián)函數(shù)調(diào)用來分離代碼。
本文我們主要看看動態(tài)導(dǎo)入,我們在 src 下面新建一個文件 another.js
function Another() {
return 'Hi, I am Another Module';
}
export { Another };
修改 index.js
import sayHello from './sayHello';
console.log(sayHello, sayHello('Gopal'));
// 單純?yōu)榱搜菔荆褪怯袟l件的時候才去動態(tài)加載
if (true) {
import('./Another.js').then(res => console.log(res))
}
我們來看下打包出來的內(nèi)容,忽略 .map 文件,可以看到多出一個 0.bundle.js 文件,這個我們稱它為動態(tài)加載的 chunk,bundle.js 我們稱為主 chunk

輸出的代碼的話,主 chunk 看這里[6],動態(tài)加載的 chunk 看這里[7] ,下面是針對這兩份代碼的分析
主 chunk 分析
我們先來看看主 chunk
內(nèi)容多了很多,我們來細(xì)看一下:
首先我們注意到,我們動態(tài)導(dǎo)入的地方編譯后變成了以下,這是看起來就像是一個異步加載的函數(shù)
if (true) {
__webpack_require__.e(/*! import() */ 0).then(__webpack_require__.bind(null, /*! ./Another.js */ "./src/Another.js")).then(res => console.log(res))
}
所以我們來看 __webpack_require__.e 這個函數(shù)的實(shí)現(xiàn)
__webpack_require__.e ——使用 JSONP 動態(tài)加載
// 已加載的chunk緩存
var installedChunks = {
"main": 0
};
// ...
__webpack_require__.e = function requireEnsure(chunkId) {
// promises 隊(duì)列,等待多個異步 chunk 都加載完成才執(zhí)行回調(diào)
var promises = [];
// JSONP chunk loading for javascript
var installedChunkData = installedChunks[chunkId];
// 0 代表已經(jīng) installed
if(installedChunkData !== 0) { // 0 means "already installed".
// a Promise means "currently loading".
// 目標(biāo)chunk正在加載,則將 promise push到 promises 數(shù)組
if(installedChunkData) {
promises.push(installedChunkData[2]);
} else {
// setup Promise in chunk cache
// 利用Promise去異步加載目標(biāo)chunk
var promise = new Promise(function(resolve, reject) {
// 設(shè)置 installedChunks[chunkId]
installedChunkData = installedChunks[chunkId] = [resolve, reject];
});
// i設(shè)置chunk加載的三種狀態(tài)并緩存在 installedChunks 中,防止chunk重復(fù)加載
// nstalledChunks[chunkId] = [resolve, reject, promise]
promises.push(installedChunkData[2] = promise);
// start chunk loading
// 使用 JSONP
var head = document.getElementsByTagName('head')[0];
var script = document.createElement('script');
script.charset = 'utf-8';
script.timeout = 120;
if (__webpack_require__.nc) {
script.setAttribute("nonce", __webpack_require__.nc);
}
// 獲取目標(biāo)chunk的地址,__webpack_require__.p 表示設(shè)置的publicPath,默認(rèn)為空串
script.src = __webpack_require__.p + "" + chunkId + ".bundle.js";
// 請求超時的時候直接調(diào)用方法結(jié)束,時間為 120 s
var timeout = setTimeout(function(){
onScriptComplete({ type: 'timeout', target: script });
}, 120000);
script.onerror = script.onload = onScriptComplete;
// 設(shè)置加載完成或者錯誤的回調(diào)
function onScriptComplete(event) {
// avoid mem leaks in IE.
// 防止 IE 內(nèi)存泄露
script.onerror = script.onload = null;
clearTimeout(timeout);
var chunk = installedChunks[chunkId];
// 如果為 0 則表示已加載,主要邏輯看 webpackJsonpCallback 函數(shù)
if(chunk !== 0) {
if(chunk) {
var errorType = event && (event.type === 'load' ? 'missing' : event.type);
var realSrc = event && event.target && event.target.src;
var error = new Error('Loading chunk ' + chunkId + ' failed.\n(' + errorType + ': ' + realSrc + ')');
error.type = errorType;
error.request = realSrc;
chunk[1](error "1");
}
installedChunks[chunkId] = undefined;
}
};
head.appendChild(script);
}
}
return Promise.all(promises);
};
可以看出將
import()轉(zhuǎn)換成模擬JSONP去加載動態(tài)加載的chunk文件設(shè)置
chunk加載的三種狀態(tài)并緩存在installedChunks中,防止chunk重復(fù)加載。這些狀態(tài)的改變會在webpackJsonpCallback中提到// 設(shè)置 installedChunks[chunkId]
installedChunkData = installedChunks[chunkId] = [resolve, reject];installedChunks[chunkId]為0,代表該chunk已經(jīng)加載完畢installedChunks[chunkId]為undefined,代表該chunk加載失敗、加載超時、從未加載過installedChunks[chunkId]為Promise對象,代表該chunk正在加載
看完__webpack_require__.e,我們知道的是,我們通過 JSONP 去動態(tài)引入 chunk 文件,并根據(jù)引入的結(jié)果狀態(tài)進(jìn)行處理,那么我們怎么知道引入之后的狀態(tài)呢?我們來看異步加載的 chunk 是怎樣的
異步 Chunk
// window["webpackJsonp"] 實(shí)際上是一個數(shù)組,向中添加一個元素。這個元素也是一個數(shù)組,其中數(shù)組的第一個元素是chunkId,第二個對象,跟傳入到 IIFE 中的參數(shù)一樣
(window["webpackJsonp"] = window["webpackJsonp"] || []).push([[0],{
/***/ "./src/Another.js":
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "Another", function() { return Another; });
function Another() {
return 'Hi, I am Another Module';
}
/***/ })
}]);
//# sourceMappingURL=0.bundle.js.map
主要做的事情就是往一個數(shù)組 window['webpackJsonp'] 中塞入一個元素,這個元素也是一個數(shù)組,其中數(shù)組的第一個元素是 chunkId,第二個對象,跟主 chunk 中 IIFE 傳入的參數(shù)類似。關(guān)鍵是這個 window['webpackJsonp'] 在哪里會用到呢?我們回到主 chunk 中。在 return __webpack_require__(__webpack_require__.s = "./src/index.js"); 進(jìn)入入口之前還有一段
var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || [];
// 保存原始的 Array.prototype.push 方法
var oldJsonpFunction = jsonpArray.push.bind(jsonpArray);
// 將 push 方法的實(shí)現(xiàn)修改為 webpackJsonpCallback
// 這樣我們在異步 chunk 中執(zhí)行的 window['webpackJsonp'].push 其實(shí)是 webpackJsonpCallback 函數(shù)。
jsonpArray.push = webpackJsonpCallback;
jsonpArray = jsonpArray.slice();
// 對已在數(shù)組中的元素依次執(zhí)行webpackJsonpCallback方法
for(var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]);
var parentJsonpFunction = oldJsonpFunction;
jsonpArray 就是 window["webpackJsonp"] ,重點(diǎn)看下面這一句代碼,當(dāng)執(zhí)行 push 方法的時候,就會執(zhí)行 webpackJsonpCallback,相當(dāng)于做了一層劫持,也就是執(zhí)行完 push 操作的時候就會調(diào)用這個函數(shù)
jsonpArray.push = webpackJsonpCallback;
webpackJsonpCallback ——加載完動態(tài) chunk 之后的回調(diào)
我們再來看看 webpackJsonpCallback 函數(shù),這里的入?yún)⒕褪莿討B(tài)加載的 chunk 的 window['webpackJsonp'] push 進(jìn)去的參數(shù)。
var installedChunks = {
"main": 0
};
function webpackJsonpCallback(data) {
// window["webpackJsonp"] 中的第一個參數(shù)——即[0]
var chunkIds = data[0];
// 對應(yīng)的模塊詳細(xì)信息,詳見打包出來的 chunk 模塊中的 push 進(jìn) window["webpackJsonp"] 中的第二個參數(shù)
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];
// 所以此處是找到那些未加載完的chunk,他們的value還是[resolve, reject, promise]
// 這個可以看 __webpack_require__.e 中設(shè)置的狀態(tài)
// 表示正在執(zhí)行的chunk,加入到 resolves 數(shù)組中
if(installedChunks[chunkId]) {
resolves.push(installedChunks[chunkId][0]);
}
// 標(biāo)記成已經(jīng)執(zhí)行完
installedChunks[chunkId] = 0;
}
// 挨個將異步 chunk 中的 module 加入主 chunk 的 modules 數(shù)組中
for(moduleId in moreModules) {
if(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
modules[moduleId] = moreModules[moduleId];
}
}
// parentJsonpFunction: 原始的數(shù)組 push 方法,將 data 加入 window["webpackJsonp"] 數(shù)組。
if(parentJsonpFunction) parentJsonpFunction(data);
// 等到 while 循環(huán)結(jié)束后,__webpack_require__.e 的返回值 Promise 得到 resolve
// 執(zhí)行 resolove
while(resolves.length) {
resolves.shift()();
}
};
當(dāng)我們 JSONP 去加載異步 chunk 完成之后,就會去執(zhí)行 window["webpackJsonp"] || []).push,也就是 webpackJsonpCallback。主要有以下幾步
遍歷要加載的 chunkIds,找到未執(zhí)行完的 chunk,并加入到 resolves 中
for(;i < chunkIds.length; i++) {
chunkId = chunkIds[i];
// 所以此處是找到那些未加載完的chunk,他們的value還是[resolve, reject, promise]
// 這個可以看 __webpack_require__.e 中設(shè)置的狀態(tài)
// 表示正在執(zhí)行的chunk,加入到 resolves 數(shù)組中
if(installedChunks[chunkId]) {
resolves.push(installedChunks[chunkId][0]);
}
// 標(biāo)記成已經(jīng)執(zhí)行完
installedChunks[chunkId] = 0;
}
這里未執(zhí)行的是非 0 狀態(tài),執(zhí)行完就設(shè)置為0
installedChunks[chunkId][0]實(shí)際上就是 Promise 構(gòu)造函數(shù)中的 resolve// __webpack_require__.e
var promise = new Promise(function(resolve, reject) {
installedChunkData = installedChunks[chunkId] = [resolve, reject];
});挨個將異步
chunk中的module加入主chunk的modules數(shù)組中原始的數(shù)組
push方法,將data加入window["webpackJsonp"]數(shù)組執(zhí)行各個
resolves方法,告訴__webpack_require__.e中回調(diào)函數(shù)的狀態(tài)
只有當(dāng)這個方法執(zhí)行完成的時候,我們才知道 JSONP 成功與否,也就是script.onload/onerror 會在 webpackJsonpCallback 之后執(zhí)行。所以 onload/onerror 其實(shí)是用來檢查 webpackJsonpCallback 的完成度:有沒有將 installedChunks 中對應(yīng)的 chunk 值設(shè)為 0
動態(tài)導(dǎo)入小結(jié)
大致的流程如下圖所示

總結(jié)
本篇文章分析了 webpack 打包主流程以及和動態(tài)加載情況下輸出代碼,總結(jié)如下
總體的文件就是一個 IIFE——立即執(zhí)行函數(shù)webpack會對加載過的文件進(jìn)行緩存,從而優(yōu)化性能主要是通過 __webpack_require__來模擬import一個模塊,并在最后返回模塊export的變量webpack是如何支持ES Module的動態(tài)加載 import()的實(shí)現(xiàn)主要是使用JSONP動態(tài)加載模塊,并通過webpackJsonpCallback判斷加載的結(jié)果
參考
分析 webpack 打包后的文件[8] webpack 打包產(chǎn)物代碼分析[9] 『Webpack系列』—— 路由懶加載的原理[10]
參考資料
這里查看: https://github.com/GpingFeng/learn-webpack/blob/main/output/main.js
[2]【面試說】Javascript 中的 CJS, AMD, UMD 和 ESM是什么?: https://juejin.cn/post/6935973925004247077?utm_source=gold_browser_extension#heading-0
[3]entry: https://webpack.docschina.org/configuration/entry-context
Entry dependencies: https://webpack.docschina.org/configuration/entry-context/#dependencies
[5]SplitChunksPlugin: https://webpack.docschina.org/plugins/split-chunks-plugin
這里: https://github.com/GpingFeng/learn-webpack/blob/main/output/bundle.js
[7]這里: https://github.com/GpingFeng/learn-webpack/blob/main/output/0.bundle.js
[8]分析 webpack 打包后的文件: https://juejin.cn/post/6844903492063068167
[9]webpack 打包產(chǎn)物代碼分析: https://hellogithub2014.github.io/2019/01/02/webpack-bundle-code-analysis/
[10]『Webpack系列』—— 路由懶加載的原理: https://juejin.cn/post/6844904180285456398
1.看到這里了就點(diǎn)個在看支持下吧,你的「點(diǎn)贊,在看」是我創(chuàng)作的動力。
2.關(guān)注公眾號
程序員成長指北,回復(fù)「1」加入高級前端交流群!「在這里有好多 前端 開發(fā)者,會討論 前端 Node 知識,互相學(xué)習(xí)」!3.也可添加微信【ikoala520】,一起成長。
“在看轉(zhuǎn)發(fā)”是最大的支持
