Babel 之 ESM 和 CommonJS
1. ES6 modules vs. CommonJS modules
1.1 ECMAScript 6 modules
顶级导出(默认/单一导出):
// lib.js
export default function () {}
// main.js
import lib from './lib';
次级导出(多个导出):
// lib.js
export function foo() {}
export function bar() {}
// main1.js
import * as lib from './lib';
lib.foo();
lib.bar();
// main2.js
import {foo, bar} from './lib';
foo();
bar();
esm 支持两者同时出现,即支持同时含有顶级导出和次级导出。
1.2 CommonJS modules
顶级导出(默认/单一导出):
// lib.js
module.exports = function () {};
// main.js
var lib = require('./lib');
次级导出:
// lib.js
exports.foo = function () {};
exports.bar = function () {};
// main1.js
var lib = require('./lib');
lib.foo();
lib.bar();
// main2.js
var foo = require('./lib').foo;
var bar = require('./lib').bar;
foo();
bar();
**cjs 的两者互斥,即仅支持其中一种方式导出。**若想实现两者兼容效果,你可以这样:
function defaultExport() {}
defaultExport.foo = function () {};
defaultExport.bar = function () {};
module.exports = defaultExport;
1.3 对比两模块
ES6 模块较 CommonJS 模块有两大优点:
-
首先,它们的刚性结构使它们能够进行静态分析。这使得,例如,tree-shaking(死代码消除)可以显著减少绑定模块的大小。
-
ES6 的 import 不会被直接运行,这意味着循环依赖关系是被支持的。在 CommonJS 中,你必须像这样编写代码,这样导出的实体 foo 就可以在以后被填充:
var lib = require('./lib'); lib.foo();
相反,这种不能正常运行(也不能通过module.exports进行默认导出):
var foo = require('./lib').foo; foo();
如果对此段不太理解,建议阅读文章 Support for cyclic dependencies
2. Babel 如何编译 ES6 模块为 CommonJS
举个例子,请看下面 ESM:
export function foo() {};
export default 123;
Babel 会将其编译成如下 commonjs:
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.foo = foo;
function foo() {};
exports.default = 123;
下面的小节可能回答了有关于这个代码你要问的问题:
- 为什么没有像 commonjs 那样进行顶级(默认)导出?
- 为什么通过 __esModule 标记 ES6 模块?
2.1 为什么没有像 commonjs 那样进行顶级(默认)导出?
原因有二:
-
更接近 es6 模块的语义
-
❗️commonjs 不支持同时存在 顶级导出 与 次级导出,所以 babel 采用统一的次级导出方式编译出 commonjs:
es6 code:
export default 'abc'; export var foo = 123;
compiled to es5 code:
exports.default = 'abc'; exports.foo = 123; Object.defineProperty(exports, '__esModule', { value: true });;
即使 esm 仅有一个顶级导出,同样会被编译为 commonjs 的次级导出模式。
2.2 为什么通过 __esModule 标记 ES6 模块?
该标志允许 Babel 将具有顶级导出的 CommonJS 模块视为具有顶级导出的 ES6 模块。如何做到这一点将在下一节中讨论。
3. Babel 如何引用 esm 编译后的 CommonJS
babel 既然编译了 esm 为 commonjs,那么 babel 肯定是有自己的方式配合进行导入。
3.1 顶级(默认)引用
❗️这里是默认导入的情况。
es6 code:
import assert from 'assert';
assert(true);
compiled to es5 code:
'use strict';
function _interopRequireDefault(obj) {
return obj && obj.__esModule
? obj
: { 'default': obj };
}
var _assert = require('assert');
var _assert2 = _interopRequireDefault(_assert);
(0, _assert2['default'])(true); // (A)
解释一下:
-
_interopRequireDefault()
: esm 编译后的 commonjs 模块, exports 必然含有名为default
的属性。而对于原始普通 commonjs 模块主动为其添加一个 default 属性,其值为默认值。这就实现了统一。 -
注意,默认的导出总是像 A 行那样实现的,❌不可使用如下方式:
var assert = _assert2.default;
这是因为像上文说到的那样,来支持循环依赖。
3.2 次级(具名)引用
❗️这里是仅含有具名导入的情况。
es6 code:
import {ok} from 'assert';
ok();
compiled to es5 code:
'use strict';
var _assert = require('assert');
(0, _assert.ok)();
同样,您可以看到,ok() 从不直接访问,总是通过 _assert 访问,这确保了循环依赖正常运行。
3.3 顶级与次级联合引用
❗️这里是默认导入和具名导入同时存在的情况。
es6 code:
import * as assert from 'assert';
assert.ok(true);
compiled to es5 code:
'use strict';
function _interopRequireWildcard(obj) {
if (obj && obj.__esModule) {
return obj;
}
else {
var newObj = {}; // (A)
if (obj != null) {
for (var key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key))
newObj[key] = obj[key];
}
}
newObj.default = obj;
return newObj;
}
}
var _assert = require('assert');
var assert = _interopRequireWildcard(_assert);
assert.ok(true);
上面 es6 的代码首先会被转换成:
import assert, {ok} from `assert`;
然后 A 行创建一个新的对象存储其他命名的变量,因为它不可以修改原始的 obj 对象。
参考
- 原文: Babel and CommonJS modules