webpack模块化原理解析(二)—— 兼容各JavaScript模块化规范的原理

上篇文章介绍了 webpack 打包文件的分析,其中用到的例子是 commonjs规范的模块作为依赖,分析了webpack对 commonjs 规范模块的处理。

转念一想,如果依赖是ES Module规范呢?这就带来了两个问题:

  1. webpack 针对 ES Module 模块是如何处理的?
  2. index.js 是否还能用 require 来进行加载ES Module模块?反过来说,是否还能用 import 来加载 commonjs 规范的模块

当然了,这里第二个问题的答案是显而易见的,webpack 下是可以使用混用各JavaScript模块化规范的,至于原理得要从第一个问题的剖析开始讲起。


关于 webpack 的模块

  • An ES2015 import statement
  • A CommonJS require() statement
  • An AMD define and require statement
  • An @import statement inside of a css/sass/less file.
  • An image url in a stylesheet (url(…)) or html () file.

webpack文档显示,webpack 支持 ES Module、Commonjs、AMD 模块化规范,以及 css里的@import、img url/src。这里就不针对 css img 做深入研究了,毕竟JavaScript模块化是大头。


ES Module 规范

项目结构

1
2
3
4
5
6
7
8
9
10
11
// index.js
import { counter, incCounter } from './es';
console.log(counter); // 3
incCounter();
console.log(counter); // 4

// es.js
export let counter = 3;
export function incCounter() {
counter++;
}

webpack配置

1
2
3
4
5
6
7
// webpack.config.js
module.exports = {
entry:'./index.js',
output:{
filename:'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
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
/******/ (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;
/******/
/******/ // 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 = 0);
/******/ })
/************************************************************************/
/******/ ([
/* 0 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {

"use strict";
Object.defineProperty(__webpack_exports__, "__esModule", { value: true });
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__es__ = __webpack_require__(1);

console.log(__WEBPACK_IMPORTED_MODULE_0__es__["a" /* counter */]); // 3
Object(__WEBPACK_IMPORTED_MODULE_0__es__["b" /* incCounter */])();
console.log(__WEBPACK_IMPORTED_MODULE_0__es__["a" /* counter */]); // 4

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

"use strict";
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "a", function() { return counter; });
/* harmony export (immutable) */ __webpack_exports__["b"] = incCounter;
let counter = 3;
function incCounter() {
counter++;
}

/***/ })
/******/ ]);

分析

看过本系列的第一篇文章的知道,对比 commonjs规范的打包代码,ES Modules下的打包代码只有模块数组部分的代码产生了变化,而IIFE 函数本身是没有变化的,所以重点说模块数组部分。

先说入口模块,去除注释:

1
2
3
4
5
6
Object.defineProperty(__webpack_exports__, "__esModule", { value: true });
var __WEBPACK_IMPORTED_MODULE_0__es__ = __webpack_require__(1);

console.log(__WEBPACK_IMPORTED_MODULE_0__es__["a" /* counter */]); // 3
Object(__WEBPACK_IMPORTED_MODULE_0__es__["b" /* incCounter */])();
console.log(__WEBPACK_IMPORTED_MODULE_0__es__["a" /* counter */]); // 4

首先给__webpack_exports__添加属性__esModule为 true,说明这是个 ES Module 模块。这个属性得跟__webpack_require__.n结合起来,针对 ES Modules的 export default以及 commonjs 的export的处理,这在后面 commonjs与 ES Modules 混淆使用会谈到。

接着调用__webpack_require__函数,moduleId 为1,加载es.js模块,缓存模块 module里的 exports 赋值为:

1
2
3
4
5
6
__webpack_require__.d(__webpack_exports__, "a", function() { return counter; });;
__webpack_exports__["b"] = incCounter;
let counter = 3;
function incCounter() {
counter++;
}

接着返回 modules.exports,赋值到__WEBPACK_IMPORTED_MODULE_0__es_js__,从而加载了 es模块,接着

1
2
3
console.log(__WEBPACK_IMPORTED_MODULE_0__es__["a" /* counter */]); // 3
Object(__WEBPACK_IMPORTED_MODULE_0__es__["b" /* incCounter */])();
console.log(__WEBPACK_IMPORTED_MODULE_0__es__["a" /* counter */]); // 4

打印 counter 变量,执行 incCounter 函数,再一次打印 counter 变量。

这里有一个重点,是commonjs 和 ES Module的重要差异之一,CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。

所以,在counter的赋值,这里是采取了__webpack_require__.d

1
2
3
4
5
6
7
8
9
10
11
12
__webpack_require__.d(__webpack_exports__, "a", function() { return counter; });;
let counter = 3;

__webpack_require__.d = function (exports, name, getter) {
if (!__webpack_require__.o(exports, name)) {
Object.defineProperty(exports, name, {
configurable: false,
enumerable: true,
get: getter
});
}
};

这里输出的counter属性实际上是一个取值器函数,保证 counter 变量取的是值的引用。

结论

  1. ES Module的 export export default 都转换成了类似 commonjs 规范的modules.exports,在这里是modules.exports["a"] modules.exports["b"]
  2. ES Module导出的依赖,赋值到__WEBPACK_IMPORTED_MODULE_0__es_js__,缓存到缓存模块数组
  3. 保证ES Module 输出的是值的引用,采用的是取值器函数

ES Module与 Commonjs规范混用

这里还是举出两个例子,尝试解答本文开头提出的第二个问题

情景一:ES Module 加载 Commonjs 模块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// index.js
import foo from './cj';
import {foo1} from './cj1';
foo()
foo1();

// cj.js
function foo() {
console.log("CommonJS");
}
module.exports = foo;

// cj1.js
function foo1() {
console.log("CommonJS1");
}
module.exports = {foo1};

webpack配置

1
2
3
4
5
6
7
// webpack.config.js
module.exports = {
entry:'./index.js',
output:{
filename:'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
[
/* 0 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {

"use strict";
Object.defineProperty(__webpack_exports__, "__esModule", { value: true });
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__cj__ = __webpack_require__(1);
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__cj___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_0__cj__);
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_1__cj1__ = __webpack_require__(2);
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_1__cj1___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_1__cj1__);


__WEBPACK_IMPORTED_MODULE_0__cj___default()()
Object(__WEBPACK_IMPORTED_MODULE_1__cj1__["foo1"])();

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

function foo() {
console.log("CommonJS");
}
module.exports = foo;

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

function foo1() {
console.log("CommonJS1");
}
module.exports = {foo1};

/***/ })
/******/ ]

前面分析 ES Module 说过,入口模块 index为 ES 模块,添加__esModule属性为true, 接着调用__webpack_require__导入 cj模块,cj 模块为 commonjs 规范,webpack 对其处理没多大变化,完成对__WEBPACK_IMPORTED_MODULE_0__cj__赋值,接着比 ES Module 调用 ES Module 多出的一步是:

1
var __WEBPACK_IMPORTED_MODULE_0__cj___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_0__cj__);

其中__webpack_require__.n定义为:

1
2
3
4
5
6
7
8
9
10
11
__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;
};

这里判断依赖模块是否为 ES Module,当__esModule为true时,那么返回的是该模块的default属性的值,如果传入的模块原来就是commonjs模块,返回模块本身,并且令该模块的a属性等于模块本身。

这里调用 cj1模块也是一样的,不同的地方在于,es模块调用cj模块取的是 default 属性,调用 cj1模块取的是 foo1属性,所以在执行这一步又是不同的:

1
2
__WEBPACK_IMPORTED_MODULE_0__cj___default()()
Object(__WEBPACK_IMPORTED_MODULE_1__cj1__["foo1"])();

可以看出 cj1模块是原样输出,执行foo1属性

结论

  • ES Module 引用commonjs模块时,因为import foo from ‘./cj’想取的是模块的default属性,而commonjs模块没有暴露default的方法,所以webpack将整个模块作为default属性的值输出
  • 如果只是引入commonjs模块 exports的某个属性,则 commonjs 模块是原样输出

情景二:Commonjs 加载 ES Module 模块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// index.js
// commonjs 模块导入 es 模块
var es = require("./es.js");
var es1 = require("./es1.js");

es.default();
es1.es1();

// es.js
export default function es() {
console.log('ES Modules');
}

// es1.js
export function es1() {
console.log('ES1 Modules');
}

webpack配置

1
2
3
4
5
6
7
// webpack.config.js
module.exports = {
entry:'./index.js',
output:{
filename:'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
[
/* 0 */
/***/ (function(module, exports, __webpack_require__) {

// commonjs 模块导入 es 模块
var es = __webpack_require__(1);
var es1 = __webpack_require__(2);

es.default();
es1.es1();

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

"use strict";
Object.defineProperty(__webpack_exports__, "__esModule", { value: true });
/* harmony export (immutable) */ __webpack_exports__["default"] = es;
function es() {
console.log('ES Modules');
}

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

"use strict";
Object.defineProperty(__webpack_exports__, "__esModule", { value: true });
/* harmony export (immutable) */ __webpack_exports__["es1"] = es1;
function es1() {
console.log('ES1 Modules');
}

/***/ })
/******/ ]

入口模块,调用两次__webpack_require__分别赋值给变量eses1,es、es1模块中分别声明__esModule为 true,但是 es 模块中,给__webpack_exports__添加default属性,赋值 es 函数,es1模块则是给__webpack_exports__添加es1属性,赋值 es1函数。

所以在入口模块中,调用函数方式如下:

1
2
es.default();
es1.es1();

结论

  • ES Modules依赖,export default 转为 __webpack_exports__["default"],所以 commonjs 模块调用其默认值时,需调用default属性

这部分内容虽然看起来比较困难,但是专研代码的同时参考本文,还是很容易搞懂的~ 下一篇文章,会探讨Code Splitting的实现,