Babel 之 ESM 和 CommonJS

吐槽君 分类:javascript

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 模块有两大优点:

  1. 首先,它们的刚性结构使它们能够进行静态分析。这使得,例如,tree-shaking(死代码消除)可以显著减少绑定模块的大小。

  2. 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 那样进行顶级(默认)导出?

原因有二:

  1. 更接近 es6 模块的语义

  2. ❗️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

回复

我来回复
  • 暂无回复内容