浅谈webpack的打包机制

webpack 是一个现代的 JavaScript 应用程序的模块打包器(module bundler)。当 webpack 处理应用程序时,它会递归地构建一个依赖关系图表(dependency graph),其中包含应用程序需要的每个模块,然后将所有这些模块打包成少量的 bundle - 通常只有一个,由浏览器加载。

webpack作为一个模块打包工具,不仅可以打包js文件,还可以打包其他非js文件。下文仅针对js文件,从 CommonJS 模块和 ES6 模块两种处理模块的方式对 webpack 的打包机制进行解释。

以下过程均在webpack 2.5.1下测试

CommonJS 模块打包

webpack.config.js

1
2
3
4
5
6
7
8
9
10
const path = require('path')

module.exports = {
entry: './src/main.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'bundle.js'
}
}

main.js

1
2
3
4
var test1 = require('./test1.js');

test1 ();

test1.js

1
2
3
4
5
6
7
console.log('test1 is loaded');

var test2 = require('./test2.js');
module.exports = function () {
console.log('this is in test1 exports function');
};

test2.js

1
2
3
4
5
6
console.log('test2 is loaded');

module.exports = function () {
console.log('this is in test2 exports function');
};

bundle.js (模块启动函数说明)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
(function(modules) {// webpackBootstrap

})(
[

/*0*/
(function(module, exports, __webpack_require__) {

}),

/*1*/
(function(module, exports, __webpack_require__) {

}),

/*2*/
(function(module, exports) {

})

]
);

bundle.js 文件即为启动 webpack 命令打包后的文件。其对应了一个立即执行函数,modules 参数为一个数组,通过外部传入一个数组,数组中每一项对应了一个函数,被称之为模块启动函数。webpack 在打包时,将每一个被引用的js模块包装成这样的函数传入 modules 数组,形成一个相关联的模块列表传入 modules

bundle.js (打包机制说明)

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
(function(modules) {// webpackBootstrap
// The module cache
var installedModules = {};

// The require function
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;
}


// expose the modules object (__webpack_modules__)
__webpack_require__.m = modules;

// expose the module cache
__webpack_require__.c = installedModules;

// identity function for calling harmony imports with the correct context
__webpack_require__.i = function(value) { return value; };

// 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
});
}
};

// 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;
};

// Object.prototype.hasOwnProperty.call
__webpack_require__.o = function(object, property) {
return Object.prototype.hasOwnProperty.call(object, property);
};

// __webpack_public_path__
__webpack_require__.p = "";

// Load entry module and return exports
return __webpack_require__(__webpack_require__.s = 1);
})([]);

installedModules 对象对应了已加载过的模块列表,通过模块 id 作为其唯一属性存储模块的相关信息。每一个模块信息包含了该模块的模块 id,模块是否被加载过,模块 exports 对象。由下文的 _webpack_require_ 函数可知,当前模块被加载过后,再次引用该模块时,该模块不会被重新加载,仅仅是使用该模块的exports对象。

_webpack_require_ 函数是 webpack 处理模块关联的关键。其内部首先根据模块id去 `installedModules 列表中查看该 id 对应的模块是否被加载过,如果已加载过直接返回该模块的 exports 对象。如果未曾加载,则构建这样的一个模块对象,并去根据该模块 id 去调用模块启动函数。然后将该模块的是否加载标志位置为 true ,并返回该模块的 exports 对象。

m,c,i,d,n,o,p,s对应了 _webpack_require_ 的一些工具函数和变量,用于模块启动函数的一些操作。

bundle.js (模块启动函数说明)

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
26
27
28
29
30
[

/*0*/
(function(module, exports, __webpack_require__) {
console.log('test1 is loaded');
var test2 = __webpack_require__(2);
module.exports = function () {
console.log('this is in test1 exports function');
};
}),

/*1*/
(function(module, exports, __webpack_require__) {

var test1 = __webpack_require__(0);

test1 ();
}),

/*2*/
(function(module, exports) {

console.log('test2 is loaded');

module.exports = function () {
console.log('this is in test2 exports function');
};
})

]

在立即启动函数中最后一句中,通过 _webpack_require_ 函数调用了在modules数组中下标为1的模块启动函数,moduleId 为1对应的模块启动函数,即在 webpack.config.js 中配置的入口文件,在本例中即 main.js 对应的模块启动函数。在入口文件中又通过 _webpack_require_ 函数按照模块引用关系递归调用其他模块,并将模块内部的 exports 对象赋值给该模块对应的 exports 属性,用于模块的缓存。

ES6模块

注意:在 webpack2 中对于 ES6 的模块语法是开箱即用的,如果您还是 webpack1.*版本,请配置对应的 loader 处理 ES6 的模块语法。

webpack.config.js

1
2
3
4
5
6
7
8
9
10
const path = require('path')

module.exports = {
entry: './src/main.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'bundle.js'
}
}

main.js

1
2
3
4

import {test1} from './test1.js';
test1();

test1.js

1
2
3
4
5
6
7
8
9
10
11

console.log('test1 is loaded');

import {test2} from './test2.js';

function test1 () {
console.log('this is in test1 export function');
}

export {test1};

test2.js

1
2
3
4
5
6
7
8
9

console.log('test2 is loaded');

function test2 () {
console.log('this is in test2 export function');
}

export {test2};

相比于 CommonJS 模块,webpack 对 ES6 模块的处理的不同在于对 ES6 模块包装后的模块启动函数。

bundle.js (模块启动函数说明)

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
26
27
28
29
30
31
32
33
34
35
36
[
/* 0 */
(function(module, __webpack_exports__, __webpack_require__) {

"use strict";
/* harmony export (binding) */
__webpack_require__.d(__webpack_exports__, "a", function() { return test1; });
/* harmony import */
var __WEBPACK_IMPORTED_MODULE_0__test2_js__ = __webpack_require__(2);
console.log('test1 is loaded');
function test1 () {
console.log('this is in test1 export function');
}
}),
/* 1 */
(function(module, __webpack_exports__, __webpack_require__) {

"use strict";
Object.defineProperty(__webpack_exports__, "__esModule", { value: true });
/* harmony import */
var __WEBPACK_IMPORTED_MODULE_0__test1_js__ = __webpack_require__(0);
__webpack_require__.i(__WEBPACK_IMPORTED_MODULE_0__test1_js__["a" /* test1 */])();

}),
/* 2 */
(function(module, __webpack_exports__, __webpack_require__) {

"use strict";
/* unused harmony export test2 */
console.log('test2 is loaded');
function test2 () {
console.log('this is in test2 export function');
}
})

]

ES6 的模块启动函数列表中同样将下标为1也就是 moduleId 为1的模块(即入口模块)作为启动模块,与 CommonJS 模块的模块启动函数不同,ES6 模块的启动函数参数中,_webpack_exports_ 对象替换了 exports 对象,不过与 exports 对象一样,他们都指向 module.exports 对象。在入口模块的模块启动函数中,首先定义了 module.exports 对象中的 __esModule 属性为true,然后按照引用关系依次递归调用其他模块。此处需要注意对 ES6 的模块处理中稍有不同的是,webpack 将模块导出的对象名作为一个a属性添加在了 module.exports 对象上。其后通过读取a属性的值进行相应的调用处理。

在本实验中又发现了一个比较有意思的事情,ES6 模块中 import 是静态的,无法用于按需加载,因此 import 语句必须放在模块的顶层。然而在本试验中,在 import 语句上方添加了打印语句,webpack 对此的处理是,不论 import 语句出现的位置,提前加载对应的模块,然后执行该文件中剩余的部分。

更新: ES6 中import命令只能出现在模块的顶层,出现在块级作用域中会报错。另外 import 命令具有提升的效果,会提升到整个模块的顶部,首先执行。

具体测试过程见 Github: webpack-bundle-study

参考文章:

webpack指南

从 Bundle 文件看 Webpack 模块机制

作者

monster1935

发布于

2017-08-24

更新于

2024-09-25

许可协议