webpack系列之输出文件分析

写在前面

上一篇文章我们讲了如何使用 webpack,执行打包会在 dist 生成一堆文件,那么 webpack 输出的文件里面到底长啥样呢?用过的人100%看过,大部分的还是压缩混淆后的代码,一般我们不会去关心它,只管当前持续运行正常就行了。今天我们来看看 webpack 输出的文件

配置

安装

开发环境 webpack: 4.34.0

1
2
> npm inin -y
> cnpm i webpack webpack-cli -D

新建文件

新建文件 webpack.config.js
新建文件夹 src
webpack.config.js

1
2
3
4
5
6
7
8
9
10
const path = require('path');
module.exports = {
devtool: 'inline-source-map',
mode: 'development',
entry: './src/main.js',
output: {
path: path.resolve(__dirname, './dist'),
filename: 'bundle.js',
},
};

src 文件夹下,新增三个文件 main.js(入口文件) a.js b.js
main.js

1
2
3
4
import { A1, A2 } from './a';
import B from './b';
console.log(A1, A2);
B();

a.js

1
2
export const A1 = 'a1';
export const A2 = 'a2';

b.js

1
2
3
export default function() {
console.log('b');
}

打包

1
npx webpack --config webpack.config.js

然后就会在 dist 下生成一个 bundle.js 文件,接下来开始分析文件

文件分析

首先先来看看大致的结果

1
2
3
4
5
6
7
8
9
10
11
(function(modules) {
function __webpack_require__(moduleId) {
...
}
...
return __webpack_require__(__webpack_require__.s = "./src/main.js");
})({
"./src/a.js": (function(module, __webpack_exports__, __webpack_require__) {}
"./src/b.js": (function(module, __webpack_exports__, __webpack_require__) {}
"./src/main.js": (function(module, __webpack_exports__, __webpack_require__) {}
})

从上面我们可以看到一个立即执行的函数,传递了一个对象,也就是 modules 的值,最终执行了 __webpack_require__ 函数,执行的这个方法其实是我们在 webpack 里面设置的 entry: ./src/main.js,对象里还有key./src/a.js./src/b.js,也就是我们的 a.jsb.js

我们知道最开始执行了 __webpack_require__(__webpack_require__.s = "./src/main.js") 方法,也就是 __webpack_require__("./src/main.js"),那么这个 __webpack_require__ 方法又做了什么的

原始的 __webpack_require__ 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// The module cache
var installedModules = {};
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)
var module = installedModules[moduleId] = {
i: moduleId,
l: false,
exports: {}
};
// Execute the module function
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
// Flag the module as loaded
module.l = true;
// Return the exports of the module
return module.exports;
}

现在我在这上面写上注释

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 模块缓存
var installedModules = {};
function __webpack_require__(moduleId) {
// 首先全局有一个模块对象,最先判断是否存在这个模块,是否做过相应操作,如果有则直接返回当前模块的一个对象,这里的exports其实就是一个对象
if(installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
// 这里就直接创建了一个模块,并且缓存在全局的模块中,这里重点关注这个exports,
// i 指的是模块的名称,比如 './src/main.js'
// l 意思是当前模块是否加载
// exports 就是返回出去的对象内容
var module = installedModules[moduleId] = {
i: moduleId,
l: false,
exports: {}
};
// 到这里就开始通过key去执行 modules(就是刚开始立即执行函数传过来的对象)对象的方法
// 然后使用call来指向 对象的方法 的this,并且把 module, module.exports, __webpack_require__ 三个值传过去,
// 这里先做预告,module这个参数传过去其实是没有用到的,主要使用 module.exports 对象, 以及__webpack_require__方法
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
// 上面说到有一个参数 l,到这里当前模块已经加载
module.l = true;
// 最后把module.exports这个模块返回出去
return module.exports;
}

接下来我们看看主入口 ./src/main.js 这个 key 的值的内容

./src/main.js,原内容是这样的,接下来来解释一下

1
2
3
4
5
6
7
8
9
10
11
12
{
"./src/main.js": (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony import */ var _a__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./a */ "./src/a.js");
/* harmony import */ var _b__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./b */ "./src/b.js");

console.log(_a__WEBPACK_IMPORTED_MODULE_0__["A1"], _a__WEBPACK_IMPORTED_MODULE_0__["A2"]);
Object(_b__WEBPACK_IMPORTED_MODULE_1__["default"])();

})
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
"./src/main.js": (function(module, __webpack_exports__, __webpack_require__) {
// 上面我们说到 __webpack_require__ 方法内部会执行 modules[moduleId].call,并传递了三个参数,那么他执行的方法就是这个内部方法

// 这段代码可以先忽略,在当前项目没有作用
__webpack_require__.r(__webpack_exports__);

// 我们看到下面有两段__webpack_require__函数代码的执行,你可以回顾一下main.js的内容,我们是不是做了 import { A1, A2 } from './a';并且 console.log(A1, A2);
/* harmony import */ var _a__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./a */ "./src/a.js");
/* harmony import */ var _b__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./b */ "./src/b.js");
console.log(_a__WEBPACK_IMPORTED_MODULE_0__["A1"], _a__WEBPACK_IMPORTED_MODULE_0__["A2"]);

// 这段代码其实是 import B from './b'; B();代码的执行
Object(_b__WEBPACK_IMPORTED_MODULE_1__["default"])();

})
}

好了,现在来分析一下 __webpack_require__("./src/a.js") 做了哪些操作,我们先来看看 模块./src/a.js 的内容
./src/a.js

1
2
3
4
5
6
7
8
9
{
"./src/a.js": (function(module, __webpack_exports__, __webpack_require__) {
__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "A1", function() { return A1; });
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "A2", function() { return A2; });
const A1 = 'a1';
const A2 = 'a2';
}),
}

代码内容很简单,首先 __webpack_require__("./src/a.js") 执行之后,会创建一个模块,然后去执行模块 ./src/a.js 内部得方法,也是是上面这段,执行完成之后最终会把 module.exports 返回处理,
那么 module.exports 这个是什么内容呢?
看看 webpack_require 内部

1
2
3
4
5
6
7
8
var module = installedModules[moduleId] = {
i: moduleId,
l: false,
exports: {}
};
// Execute the module function
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
return module.exports

其实就是我们创建模块时的 exports,然后执行到了模块 ./src/a.js 内部得方法,它内部方法关键的地方在于又调用了 __webpack_require__.d 方法

__webpack_require__.d

1
2
3
4
5
__webpack_require__.d = function(exports, name, getter) {
if(!__webpack_require__.o(exports, name)) {
Object.defineProperty(exports, name, { enumerable: true, get: getter });
}
};

执行 __webpack_require__.d(__webpack_exports__, "A1", function() { return A1; }); 可以看出来他给 module.exports 定义了一个 key"key",然后取值 get 的时候返回的是 A1(也就是a1)

所以最终 return module.exports 的值为 {A1: 'a1'}

我们回到 ./src/main.js 模块,所以这段代码:var _a__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./a */ "./src/a.js");

_a__WEBPACK_IMPORTED_MODULE_0__ 其实就是等于 {A1: 'a1'} console.log(_a__WEBPACK_IMPORTED_MODULE_0__["A1"]),取值为 a1A2 同理

bundle-a-js

接下来我们看看模块 ./src/b.js,在主模块它做了什么呢?看看 ./src/main.js

1
2
3
...
var _b__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./b */ "./src/b.js");
Object(_b__WEBPACK_IMPORTED_MODULE_1__["default"])();

看上面模块 ./src/b.js 在主模块的执行第一段代码和之前介绍的一样,第二段代码直接执行了一个方法,这里我们可以猜测出 _b__WEBPACK_IMPORTED_MODULE_1__ 其实就是一个对象,访问了它的 key: default
然后它的key值其实是一个函数,最后执行了这个函数

同理我们可以看看模块 ./src/b.js 内部的方法,以及我们在 src 文件夹下的 b.js 是怎么写的
模块 ./src/b.js

1
2
3
4
5
6
7
{
"./src/b.js": (function(module, __webpack_exports__, __webpack_require__) {
__webpack_require__.r(__webpack_exports__);
/* harmony default export */ __webpack_exports__["default"] = (function() {
console.log('b');
});
}

b.js

1
2
3
export default function() {
console.log('b');
}

从我们源代码看出,我们是直接导出了一个方法,内部执行了打印字符串b,然后再来看看 webapck 的源码部分,__webpack_require__.r(__webpack_exports__); 这段可以忽略,解释一下,其实这段代码在对象里定义了一个 __esModule: true,接着看下面一段,我们从之前讲的知道知道 __webpack_exports__ 其实就是一个单纯的空对象(其实不是,执行了 __webpack_require__.r(__webpack_exports__ )就变成了 {__esModule: true}),然后它又在对象里增加了一个 default 属性,然后把一个方法赋值给它(其实就是我们打包之前写的一个方法),最终在主入口里执行的模块 var _b__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./b */ "./src/b.js");其实就等于 {"default": function{}},然后下一步执行访问对象 default 的值去执行函数

bundle-b-js

思考

可能现在你可能会思考🤔,通过上面的比较可以得出一个结论

通过 export 出来,如果 import {a,b,c} from '..',打包出来的代码执行简单操作之后(执行 __webpack_require__ 函数)首先会是一个对象,对象会是 {a: ..., b: ..., c: ...}

同过 export default 出来,如果 import a from '..',打包处理的代码执行简单操作之后(执行 __webpack_require__ 函数)首先会是一个对象,然后会往对象里添加一个default的key,类似 {default: ...}

总结

整个过程还是挺绕的,你可以自己去 debugger 看看他的执行过程,应该就明白得差不多了,今天就讲了这些吧

本文地址 webpack系列之输出文件分析

坚持原创技术分享,您的支持将鼓励我继续创作!