面试官:Webpack 究竟打包出来的是什么?

程序源代码

共 13745字,需浏览 28分钟

 ·

2021-11-04 22:17

前言

Webpack 作为普遍使用的打包工具,在开发和发布的过程中产出的代码结构,你是否关心过?本文为你揭开它的神秘面纱。

1、开发

一般情况,开发的过程都会使用 devServer 并开启 hot 热更新。假如我们有一个页面入口文件 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) {
}

// 模块缓存,被执行过的模块都会放到这里面
var installedModules = {};

// require 函数
function __webpack_require__(moduleId) {

// 检查模块是否在缓存中,有就取出来返回模块的 exports 属性
if(installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
// 创建模块并放入缓存
var module = installedModules[moduleId] = {
i: moduleId,
l: false,
exports: {},
hot: hotCreateModule(moduleId),
parents: (hotCurrentParentsTemp = hotCurrentParents, hotCurrentParents = [], hotCurrentParentsTemp),
children: []
};

// 执行模块
modules[moduleId].call(module.exports, module, module.exports, hotCreateRequire(moduleId));

// 修改标识
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");
})
});

打包后的代码在浏览器中格式化之后非常多,对于我们来说,我们的关注放在代码块和打包后的代码执行流程上,精简一下:

(function(modules) { // webpackBootstrap
函数体代码
}
// 省略调其他模块代码
({
"./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__) {
})
});

我们看到打包后的代码其实就是一个 IIFE。这个函数接受一个对象类型的参数,其中这个参数的 key 就是模块路径,value 则是对模块代码包裹后的一个函数,该函数有几个固定参数(这个其实可以解释 Node 中模块文件中 require 和 module 是如何来的?其实就是 Node 在模块之外包装了一层,把 require 和 module 给传了进来)。暂且先不管参数具体是什么,我们接着看函数体里是什么:

(function(modules) { // webpackBootstrap
function hotCreateRequire(moduleId) {
var me = installedModules[moduleId];
if (!me) return __webpack_require__;
var fn = function(request) {
//省略
}
return fn
}
function hotCreateModule(moduleId) {
}

// 模块缓存,被执行过的模块都会放到这里面
var installedModules = {};

// require 函数
function __webpack_require__(moduleId) {

// 检查模块是否在缓存中,有就取出来返回模块的 exports 属性
if(installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
// 创建模块并放入缓存
var module = installedModules[moduleId] = {
i: moduleId,
l: false,
exports: {},
hot: hotCreateModule(moduleId),
parents: (hotCurrentParentsTemp = hotCurrentParents, hotCurrentParents = [], hotCurrentParentsTemp),
children: []
};

// 执行模块
modules[moduleId].call(module.exports, module, module.exports, hotCreateRequire(moduleId));

// 修改标识
module.l = true;

// 返回模块的 exports 属性
return module.exports;
}

// 省略代码

// 加载入口模块
return hotCreateRequire(0)(__webpack_require__.s = 0);
}
({
// 省略调其他模块代码
});

我们看函数体里定义了很多对象和方法,含有 hot 的部分基本上和热更新模块相关。这些是被添加进来的代码,最终上线是没有的。我们直接看函数体的最后一行,它执行了 hotCreateRequire(0)(__webpack_require__.s = 0)。这一行两个括号,很明显 hotCreateRequire 是返回了一个函数出来。我们接下来看看看 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
}

启动的时候,传入了 moduleId 是 0,在 installedModules 中找不到模块,直接返回了 webpack_require__。然后继续执行这个返回的函数,并传入参数 __webpack_require.s = 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");
})

这个函数,通过 __webpack_require__ 执行了三个模块,前两个是在 devServer 添加的入口文件,为了实现开发和热更新的一些功能。最后一行 moduel.exports 属性是我们的 index.js 模块执行的返回,这样就执行到了我们的程序入口。

在 index.js 中我们看到通过 import 导入的 dateutils 也是通过 __webpack_require__ 进行模块引用的,并对里面的方法进行了调用。那么,以此类推,所有的模块引用都是这么实现的。

2、生产模式

我们把这个代码使用生产模式进行打包,可以得到如下代码:

!function(e) {
// 模块缓存
var t = {};
// 模块require函数
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
}
// 执行入口
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

因为,我们的代码足够简单,没有其他的依赖模块被打进来。和开发模式类似,整体上它也是一个 IIFE,只不过做了一些混淆和压缩的处理。另外,参数变成了数组类型。同时,去掉了开发模式下的一些辅助代码。我们很容易和上面的东西做一些对应(见注释)。

3、再进一步

OK,开发模式和生产模式的代码结构几乎一样。只是参数类型略有差别,开发模式下,key 是作为模块的标识来使用的。热更新开启后,修改模块可以很轻易的修改该模块的代码。

另外,我们的项目代码里无论是使用 require/module.exports 这种 ComomonJS 的模块化方案,还是采用 import/export 的 ESM 模块化方案。通过 webpack 打包最终其实是 ComomonJS 的模块化方案,也就是说,它可以像 CommonJs 那样进行动态模块引用。

还有就是如果使用了 ESM 的 export (仅有 export.default 这种方式除外,它和 CommonJS 没什么差别,都是只导出了一个对象出来),在打包后的代码有一些区别。比如:

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 方法,这个在之前的函数体里有定义:

  // 为 exports 定义 getter 函数
__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 函数的方式,我们在 esmtest.js 中对于 flag 的改动代码起了作用,随后在 index.js 模块 2s 后的定时器中的打印也说明了这一点。

最后一个是如果采用代码分割或动态引入的情况下,会怎么样?我们直接上打包前的代码:

index.js

import('./js/dynamicImport').then(dm=>{
dm.default.hello(123)
})

dynamicImport.js

export default {
hello(msg){
console.log('dynamicImport', msg)
}
}

我们再来看在开发模式下打包的代码在浏览器里多了一个 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 模块的代码,然后外面调用了 window["webpackJsonp"].push 这个方法。那么这个方法是哪里来的,我们看一下 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);
});
}),
// 省略代码
});

我们简单看一下上面的这部分代码。首先,这部分代码先执行,在 window 上挂载了一个 window["webpackJsonp"],并为它定义了一个 push 方法就是 webpackJsonpCallback,用于从异步请求的文件中加载模块。此外,定义一个 __webpack_require__.e 方法去异步请求 chunk 文件,并返回一个 Promise 对象。

至此,关于 Webpack 打包后的内容部分的介绍全部结束,你学废了吗?

浏览 48
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报