Webpack打包commonjs和esmodule混用模块的产物对比

Webpack 打包 commonjs 和 esmodule 模块的产物对比 继续,这篇文章来测试下 commonjs 模块和 esmodule 混用的情况,也就是 import 导入 commonjs 的模块,require 导入 esomodule 的模块,看一下它们在 Webpack 下的产物。

import 导入 commonjs 模块

commonjs 模块会为我们预设一个 module = {exports: {}} 的对象,导出模块的话我们可以直接给 module.exports.xxx = xxxx 或者 exports.xxx = xxx 加属性,也可以给 module.exports = xxx 赋值为一个新对象或者函数。

下边看下这两种情况的异同:

exports 添加属性

两个文件的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// src/commonjs/add.js
console.log("add开始引入");
const add = (a, b) => {
return a + b;
};

module.exports.add = add;

exports.sub = (a, b) => a - b;

// src/commonjs/index.js
console.log("commonjs开始执行");
import { add } from "./add";
console.log("1+1=", add(1, 1));

如果还记得 Webpack 打包 commonjs 和 esmodule 模块的产物对比 这里总结的,我们的 import 会导入整个对象,然后执行的时候再通过 xxx.add 的形式调用。

1
2
3
4
5
6
var _add__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(
"./src/commonjs/add.js"
);
console.log("commonjs开始执行");

console.log("1+1=", (0, _add__WEBPACK_IMPORTED_MODULE_0__.add)(1, 1));

_add__WEBPACK_IMPORTED_MODULE_0__ 就是我们导出的整个 module.exports 对象。

1
2
3
4
5
6
7
8
9
10
"./src/commonjs/add.js": (module, exports) => {
console.log("add开始引入");
const add = (a, b) => {
return a + b;
};

module.exports.add = add;

exports.sub = (a, b) => a - b;
},

因此这种情况两种模式是完全契合的,不会有问题。

全部代码如下:

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
(() => {
var __webpack_modules__ = {
"./src/commonjs/add.js": (module, exports) => {
console.log("add开始引入");
const add = (a, b) => {
return a + b;
};

module.exports.add = add;

exports.sub = (a, b) => a - b;
},
};

var __webpack_module_cache__ = {};

function __webpack_require__(moduleId) {
var cachedModule = __webpack_module_cache__[moduleId];
if (cachedModule !== undefined) {
return cachedModule.exports;
}

var module = (__webpack_module_cache__[moduleId] = {
exports: {},
});

__webpack_modules__[moduleId](
module,
module.exports,
__webpack_require__
);

return module.exports;
}

(() => {
__webpack_require__.r = (exports) => {
if (typeof Symbol !== "undefined" && Symbol.toStringTag) {
Object.defineProperty(exports, Symbol.toStringTag, {
value: "Module",
});
}
Object.defineProperty(exports, "__esModule", {
value: true,
});
};
})();

var __webpack_exports__ = {};

(() => {
"use strict";

__webpack_require__.r(__webpack_exports__);
var _add__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(
"./src/commonjs/add.js"
);
console.log("commonjs开始执行");

console.log("1+1=", (0, _add__WEBPACK_IMPORTED_MODULE_0__.add)(1, 1));
})();
})();

exports 赋值为新对象

这次我们将 module.exports 整个赋值为一个新对象,这种情况我们一般是直接赋值为一个新的函数。

1
2
3
4
5
6
// src/commonjs/add.js
const add = (a, b) => {
return a + b;
};

module.exports = add;

index.js 我们可以直接导入函数,不需要这样子 import { xxx } from 'yyy' 再进行对象解构。

1
2
3
// src/commonjs/index.js
import add from "./add";
console.log("1+1=", add(1, 1));

我们知道,对于直接 import 导入的话, esmodule 相当于导入 default 属性,事实上 commonjs 并没有导出 default ,但 webpack 帮我们进行了兼容。

1
2
3
4
var _add__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__( "./src/commonjs/add.js");
var _add__WEBPACK_IMPORTED_MODULE_0___default = __webpack_require__.n(_add__WEBPACK_IMPORTED_MODULE_0__);

console.log("1+1=", _add__WEBPACK_IMPORTED_MODULE_0___default()(1, 1));

看一下 __webpack_require__.n 方法

1
2
3
4
5
6
7
__webpack_require__.n = (module) => {
var getter = module && module.__esModule ? // 判断是否为 esmodule 模块
() => (module['default']) :
() => (module); // 直接返回整个模块
__webpack_require__.d(getter, { a: getter }); // 这句没懂
return getter;
};

如果不是 esmodule 模块的话,我们会将整个模块作为 default 属性返回,但为什么在模块内又加了个 a 属性,这里没太懂,谁知道的话可以和我交流一下哈。

再看下整个的代码:

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
(() => {
var __webpack_modules__ = {
"./src/commonjs/add.js": (module) => {
const add = (a, b) => {
return a + b;
};

module.exports = add;
},
};

var __webpack_module_cache__ = {};

function __webpack_require__(moduleId) {
var cachedModule = __webpack_module_cache__[moduleId];
if (cachedModule !== undefined) {
return cachedModule.exports;
}

var module = (__webpack_module_cache__[moduleId] = {
exports: {},
});

__webpack_modules__[moduleId](
module,
module.exports,
__webpack_require__
);

return module.exports;
}

(() => {
__webpack_require__.n = (module) => {
var getter =
module && module.__esModule
? () => module["default"]
: () => module;
__webpack_require__.d(getter, { a: getter });
return getter;
};
})();

(() => {
__webpack_require__.d = (exports, definition) => {
for (var key in definition) {
if (
__webpack_require__.o(definition, key) &&
!__webpack_require__.o(exports, key)
) {
Object.defineProperty(exports, key, {
enumerable: true,
get: definition[key],
});
}
}
};
})();

(() => {
__webpack_require__.o = (obj, prop) =>
Object.prototype.hasOwnProperty.call(obj, prop);
})();

(() => {
__webpack_require__.r = (exports) => {
if (typeof Symbol !== "undefined" && Symbol.toStringTag) {
Object.defineProperty(exports, Symbol.toStringTag, {
value: "Module",
});
}
Object.defineProperty(exports, "__esModule", { value: true });
};
})();

var __webpack_exports__ = {};

(() => {
"use strict";

__webpack_require__.r(__webpack_exports__);
var _add__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(
"./src/commonjs/add.js"
);
var _add__WEBPACK_IMPORTED_MODULE_0___default = __webpack_require__.n(
_add__WEBPACK_IMPORTED_MODULE_0__
);

console.log("1+1=", _add__WEBPACK_IMPORTED_MODULE_0___default()(1, 1));
})();
})();

require 导入 esmodule 模块

esmodule 模块除了正常的 export ,我们把 export default 也加一下:

1
2
3
4
5
6
7
// src/esmodule/add.js
const add = (a, b) => {
return a + b;
};
export const sub = (a, b) => a - b;

export default add;

然后 index.js 通过 require 来导入:

1
2
3
4
5
// src/esmodule/index.js
const m = require("./add");
console.log(m)
console.log("1+1=", m.default(1, 1));
console.log("1-1=", m.sub(1, 1));

因为 require 是直接导入整个对象,没有专门导入 default 的形式,所以调用 default 方法的时候,我们需要通过 m.default 来调用。

运行起来是没有问题的:

image-20220508085606651

让我们回忆下 Webpack 打包 commonjs 和 esmodule 模块的产物对比 这里介绍的 esmodule 模块的导出产物:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var __webpack_modules__ = {
"./src/esmodule/add.js": (
__unused_webpack_module,
__webpack_exports__,
__webpack_require__
) => {
"use strict";
__webpack_require__.r(__webpack_exports__);
__webpack_require__.d(__webpack_exports__, {
sub: () => sub,
default: () => __WEBPACK_DEFAULT_EXPORT__,
});
const add = (a, b) => {
return a + b;
};
const sub = (a, b) => a - b;

const __WEBPACK_DEFAULT_EXPORT__ = add;
},
};

相当于导出了一个 module.exports 大的对象,包含 subdefault 属性。

然后是 index.js

1
2
3
4
5
6
(() => {
const m = __webpack_require__("./src/esmodule/add.js");
console.log(m);
console.log("1+1=", m.default(1, 1));
console.log("1-1=", m.sub(1, 1));
})();

需要注意的是虽然导出的是整个对象,但对于 index.js 我们不可以通过对象解构来拿到 default 方法。

1
2
3
// src/esmodule/index.js
const { default} = require("./add");
console.log("1+1=", default(1, 1));

default 会在这里被认为是一个关键字,直接抛错:

image-20220508090141724

来看下整体代码:

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
(() => {
var __webpack_modules__ = {
"./src/esmodule/add.js": (
__unused_webpack_module,
__webpack_exports__,
__webpack_require__
) => {
"use strict";
__webpack_require__.r(__webpack_exports__);
__webpack_require__.d(__webpack_exports__, {
sub: () => sub,
default: () => __WEBPACK_DEFAULT_EXPORT__,
});
const add = (a, b) => {
return a + b;
};
const sub = (a, b) => a - b;

const __WEBPACK_DEFAULT_EXPORT__ = add;
},
};

var __webpack_module_cache__ = {};

function __webpack_require__(moduleId) {
var cachedModule = __webpack_module_cache__[moduleId];
if (cachedModule !== undefined) {
return cachedModule.exports;
}

var module = (__webpack_module_cache__[moduleId] = {
exports: {},
});

__webpack_modules__[moduleId](
module,
module.exports,
__webpack_require__
);

return module.exports;
}

(() => {
__webpack_require__.d = (exports, definition) => {
for (var key in definition) {
if (
__webpack_require__.o(definition, key) &&
!__webpack_require__.o(exports, key)
) {
Object.defineProperty(exports, key, {
enumerable: true,
get: definition[key],
});
}
}
};
})();

(() => {
__webpack_require__.o = (obj, prop) =>
Object.prototype.hasOwnProperty.call(obj, prop);
})();

(() => {
__webpack_require__.r = (exports) => {
if (typeof Symbol !== "undefined" && Symbol.toStringTag) {
Object.defineProperty(exports, Symbol.toStringTag, {
value: "Module",
});
}
Object.defineProperty(exports, "__esModule", { value: true });
};
})();

var __webpack_exports__ = {};

(() => {
const m = __webpack_require__("./src/esmodule/add.js");
console.log(m);
console.log("1+1=", m.default(1, 1));
console.log("1-1=", m.sub(1, 1));
})();
})();

同时导出 commonjs 和 esmodule

我们在一个文件同时使用 module.exportsexport

1
2
3
4
5
6
7
const add = (a, b) => {
return a + b;
};
module.exports.add = add;
export const sub = (a, b) => {
return a + b;
};

浏览器会直接抛错:

image-20220508092644764

如果直接重写 module.exports 呢?

1
2
3
4
5
6
7
8
9
const add = (a, b) => {
return a + b;
};
module.exports = add;


export const sub = (a, b) => {
return a + b;
};

image-20220508092737165

同样会抛错,让我们看一下 webpack 的产物。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
"./src/commonjs/add.js": (
module,
__webpack_exports__,
__webpack_require__
) => {
__webpack_require__.r(__webpack_exports__);
__webpack_require__.d(__webpack_exports__, {
sub: () => sub,
});
module = __webpack_require__.hmd(module);

const add = (a, b) => {
return a + b;
};
module.exports.add = add;

const sub = (a, b) => {
return a + b;
};
},

调用了 __webpack_require__.hmd 方法拿到 module

1
2
3
4
5
6
7
8
9
10
11
12
13
14
__webpack_require__.hmd = (module) => {
module = Object.create(module);
if (!module.children) module.children = [];
Object.defineProperty(module, "exports", {
enumerable: true,
set: () => {
throw new Error(
"ES Modules may not assign module.exports or exports.*, Use ESM export syntax, instead: " +
module.id
);
},
});
return module;
};

hmd 方法重新定义了 exports 属性,没有定义 get 属性,所以 module.exports 返回的是 undefinedmodule.exports.add 就直接抛错了。

重新定义了 set 函数,所以 module.exports = xxx ,重新赋值属性的时候走到 set 后直接抛错。

同时导入 commonjs 和 esmodule

定义一个 commonjs 模块:

1
2
3
4
5
// src/commonjs/add.js
const add = (a, b) => {
return a + b;
};
module.exports = add;

定义一个 esmodule 模块:

1
2
3
4
5
6
// src/esmodule/sub.js
const sub = (a, b) => {
return a - b;
};

export default sub;

然后在 index.js 同时引入:

1
2
3
4
5
const add = require("./add") ;
console.log("1+1=", add(1, 1));

import sub from '../esmodule/sub'
console.log("1-1=", sub(1, 1));

没什么问题,正常运行:

image-20220508094440422

因为导入的话它们是互不影响的,各自导入自己的即可,可以看下完整代码:

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
92
93
94
95
96
97
98
(() => {
var __webpack_modules__ = {
"./src/commonjs/add.js": (module) => {
const add = (a, b) => {
return a + b;
};
module.exports = add;
},

"./src/esmodule/sub.js": (
__unused_webpack_module,
__webpack_exports__,
__webpack_require__
) => {
"use strict";
__webpack_require__.r(__webpack_exports__);
__webpack_require__.d(__webpack_exports__, {
default: () => __WEBPACK_DEFAULT_EXPORT__,
});
const sub = (a, b) => {
return a - b;
};

const __WEBPACK_DEFAULT_EXPORT__ = sub;
},
};

var __webpack_module_cache__ = {};

function __webpack_require__(moduleId) {
var cachedModule = __webpack_module_cache__[moduleId];
if (cachedModule !== undefined) {
return cachedModule.exports;
}

var module = (__webpack_module_cache__[moduleId] = {
exports: {},
});

__webpack_modules__[moduleId](
module,
module.exports,
__webpack_require__
);

return module.exports;
}

(() => {
__webpack_require__.d = (exports, definition) => {
for (var key in definition) {
if (
__webpack_require__.o(definition, key) &&
!__webpack_require__.o(exports, key)
) {
Object.defineProperty(exports, key, {
enumerable: true,
get: definition[key],
});
}
}
};
})();

(() => {
__webpack_require__.o = (obj, prop) =>
Object.prototype.hasOwnProperty.call(obj, prop);
})();

(() => {
__webpack_require__.r = (exports) => {
if (typeof Symbol !== "undefined" && Symbol.toStringTag) {
Object.defineProperty(exports, Symbol.toStringTag, {
value: "Module",
});
}
Object.defineProperty(exports, "__esModule", { value: true });
};
})();

var __webpack_exports__ = {};

(() => {
"use strict";

__webpack_require__.r(__webpack_exports__);
var _esmodule_sub__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(
"./src/esmodule/sub.js"
);
const add = __webpack_require__("./src/commonjs/add.js");
console.log("1+1=", add(1, 1));

console.log(
"1-1=",
(0, _esmodule_sub__WEBPACK_IMPORTED_MODULE_0__["default"])(1, 1)
);
})();
})();

不管是 esmodule 还是 commonjs 模块,最终都转换成了 module = {exports: {}} 形式的模块,所以它们之间的混用成为了可能。

import commonjs 模块的话,import 拿到的就是整个 module.exports 对象,正常使用即可。如果我们直接改写 module.exports 对象,webpack 会认为等同于 export default ,进行兼容处理。

require esmodule 模块的话,如果之前 esmodule 模块中有 export default ,那么使用的时候需要显示的调用 xxx.default ,对于其他的 export 正常使用即可。

虽然可以混用,但一般情况下能不混用就不混用,以免遇到未知问题,目前更推荐 esmodule 模块。

如果遇到奇怪问题的话,可以考虑直接去查看 webpack 的产物,能更快的排查出问题。

讨论

对产物其实有两个问题,然后去请教了下 Tecvan ,杰哥。

第一个问题就是上边提到,当使用 imports 导入 commonjs 模块的时候,会调用 n 方法。

1
2
3
4
5
6
7
8
__webpack_require__.n = (module) => {
var getter =
module && module.__esModule
? () => module["default"]
: () => module;
__webpack_require__.d(getter, { a: getter });
return getter;
};

这里会挂一个 a 属性,原因的话如下:

image-20220508095931458

主要是兼容 webpack 混用的情况,场景可能如下:

image-20220508100718016

第二个问题,还是 import 导入 commonjs 模块的时候,打包产物如下:

1
2
3
4
5
6
7
8
var _add__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(
"./src/commonjs/add.js"
);
var _add__WEBPACK_IMPORTED_MODULE_0___default = __webpack_require__.n(
_add__WEBPACK_IMPORTED_MODULE_0__
);

console.log("1+1=", _add__WEBPACK_IMPORTED_MODULE_0___default()(1, 1));

这里会触发 __webpack_require__.n 方法去生成 _add__WEBPACK_IMPORTED_MODULE_0___default 变量。触发这个逻辑的原因并不是因为我们使用了 import xxx from 'yyy' 的格式,而是因为导出 commonjs 模块的时候直接使用 module.exports = xxx 进行了覆盖,这种情况 webpack 就会认为等效于 export default 的情况。

image-20220508101552792

但对于代码,因为 _add__WEBPACK_IMPORTED_MODULE_0___add__WEBPACK_IMPORTED_MODULE_0___default 是同一个值,我们不处理 default 逻辑其实也是通的:

image-20220508101628186

webpack 为什么会这样处理,具体原因就不知道了,欢迎大家一起来讨论,下边是杰哥的猜测:

image-20220508101715288

后记

算上这篇,总结了三篇 webpack 的产物的文章 Webpack 打包 commonjs 和 esmodule 模块的产物对比Webpack 打包 commonjs 和 esmodule 动态引入模块的产物对比,可以加深平常开发中对于模块之间的理解。

大家如果还对 Webpack 原理感兴趣的话,可以去看杰哥的 Webpack 原理系列。目前我没有总结这个系列的计划了,哈哈。

windliang wechat