CommonJS与ES6模块导出行为差异

668 阅读3分钟

CommonJS

如以下代码:cjs导出name,changeName()main导入cjs并打印name

//cjs.js
let name = 'cjs'
function changeName() {
    name += ' changed'
}
setTimeout(() => {
    console.log('3:', name);
})
module.exports = {
    name,
    changeName
}

// main.js
let cjs = require('./cjs')
console.log('1:', cjs.name)
cjs.changeName()
console.log('2:',cjs.name)

// result
1: cjs
2: cjs
3: cjs changed

image.png

简单解释:当遇到require('./cjs')时,首先查看require.cache有没有该模块的记录,如果有直接返回记录的Module.exports,没有就进行模块解析(CommonJS的解析方式)来查找模块,找到模块就创建一个Module对象放入require.cache中:注意创建的Module对象中的exports{},接下来就去执行cjs,执行完将模块的module.exports返回给main。 当main调用changeName()时,因为闭包的因素,实际改变的是cjs中的变量。

ES6

// esm.js
let name = 'esm'
let age = 2015
function changeName() {
    name += ' changed'
}
function changeAge() {
    age += 7
}
setTimeout(() => {
    console.log('3:', name)
    console.log('3:', age)
})
export { name, changeAge }
export default {
    age,
    changeName
}

// main.js
import * as esm from './esm.js'
console.log('1:', esm.name);
console.log('1:', esm.default.age);
esm.default.changeName()
esm.changeAge()
console.log('2:', esm.name);
console.log('2:', esm.default.age);

// result
1: esm
1: 2015
2: esm changed
2: 2015
3: esm changed
3: 2022

image.png

简单解释:export中所有标识符(name, changeAge, default)都是模块内部变量的引用,遇到import再生成一个只读引用。

TS

在TypeScript中,当配置为"module" : "CommonJS"时,ts可以同时使用CommonJSES6两种导入导出语法:

//cjs:
import x = require('./x')
export = { x }

// ems:
export { x, y }
import { x, y } from './x'

export default { x }
import X from './x'
  1. 当用cjs语法引入cjs模块时:tsc不对导入导出做额外处理,并且导出的行为和CommonJS一致。
    cjs2cjs.png

  2. 当用esm语法引入esm模块时:tsc会将esm转为cjs,上述代码被转为:

// esm.js
"use strict";
exports.__esModule = true;
exports.changeAge = exports.name = void 0;
var name = 'esm';
exports.name = name;
var age = 2015;
function changeName() {
    exports.name = name += ' changed';
}
function changeAge() {
    age += 7;
}
exports.changeAge = changeAge;
setTimeout(function () {
    console.log('3:', name);
    console.log('3:', age);
});
exports["default"] = {
    age: age,
    changeName: changeName
};

// main.js
"use strict";
exports.__esModule = true;
var esm = require("./esmt");
console.log('1:', esm.name);
console.log('1:', esm["default"].age);
esm["default"].changeName();
esm.changeAge();
console.log('2:', esm.name);
console.log('2:', esm["default"].age);

esm2esm.png

  • export转为exports,export default转为exports.default
    import { x }exportsimport *exportsexports.defaultimport Xexports.default
  • 为了保证转换后的export的行为和esm一致,做了如exports.name = name += ' changed';处理。
  1. 当用cjs语法引入esm模块时:tsc同样将esm转为cjs,导出的行为和esm一致。 esm2cjs.png

  2. 当用esm语法引入cjs模块时:

  • 如果"esModuleInterop": false,ts不进行交叉导入处理,那么就会出现错误: cjs2esm.png
    import X会去找exports.default,但根本就没有exports.default,就会出现undefined
    import { x }import *虽然会报错,但实际生成的js是可以运行的。
  • 如果"esModuleInterop": true,ts会对交叉导入进行额外处理:
  1. import X:再包裹一层default
// main.ts
import cjs from './cjst'
console.log(cjs.name);

// 转化为main.js
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
exports.__esModule = true;
var cjst_1 = __importDefault(require("./cjst"));
console.log(cjst_1["default"].name);
  1. import { x }:无需处理
// main.ts
import { name } from './cjst'
console.log(name);

// 转换成main.js
"use strict";
exports.__esModule = true;
var cjst_1 = require("./cjst");
console.log(cjst_1.name);
  1. import * :再包裹一层default,并打平对象
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
    if (mod && mod.__esModule) return mod;
    var result = {};
    if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
    __setModuleDefault(result, mod);
    return result;
};
exports.__esModule = true;
var cjs = __importStar(require("./cjst"));
console.log(cjs.name);

cjs2esm fix.png

这么多处理,谁能记得住,所以ts就规定了其中一种方式:import X为正统方式,使用其他方法都会出现:只能通过启用 "esModuleInterop" 标志并引用其默认导出,使用 ECMAScript 导入/导出来引用此模块。ts(2497)即:This module can only be referenced with ECMAScript imports/exports by turning on the 'esModuleInterop' flag and referencing its default export.ts(2497)
当然,导出的行为和CommonJS一致。

__esModule

常见于exports.__esModule = true;用来告知导入方,我是用esm的语法导出的。ts不写导出默认是esm导出。CommonJS导出则没有这个标识。

webpack

__webpack_require__大杀四方,直接使用js解决各种模块形式。

  1. cjs引用cjs:行为和CommonJS一致。
  2. esm引用esm:行为和esm一致。
  3. cjs引用esm:行为和esm一致。
  4. esm引用cjs:行为和CommonJS一致。

总之:导入后的行为只与模块的导出方式有关。