【填坑】babel 和 ts 对 ES Module 和 CommonJS 的处理

简介

ES Module 导入导出的语法我们其实已经用了很久了,但是通常我们都是通过一些工具将我们的代码转换到 cjs 模块再去执行,比如 babel,但是不同的工具,甚至 babel 的不同版本的行为也是不一致的,这会给我们的使用带来很多困扰。这篇文章就简单介绍一下 ESM 和 CJS 两者之间相互导入导出存在的问题,各工具实现标准不一致的原因,以及现阶段可能的最佳实践

ES Module

// 导出
// deafult 导出,可以导出任何普通常量或者对象,只允许有一个 default 导出
// file1.js
// export default const a = 1
// export default class {}
export default {a:1, b:2}
// 普通导出,可以有任意多个
export const c = 3
const d = 4
const e = 5
export {d, e}

// 导入
import x from './file1' // { a: 1, b: 2 }
import * as x from './file1' // { default: { a: 1, b: 2 }, c: 3, d: 4, e: 5 }
import {d} from './file1' // 4

CJS

// CJS 比较简单,每个模块只有一个导出 module.exports
// file2.js
// 导出
module.exports = {a:1, b:2}

// 导入
const x = require('./file2')  // { a: 1, b: 2 }

两者的相互导入

由于没有标准的定义两者相互导入的行为,这就导致了不同工具不一致的行为,二罪魁祸首就是 default 导出这个行为。如果 ESM 没有 default 导出,那么
import x from './file1' 就会返回 undefined,而对于 cjs 的模块,默认导入这一行为不同的工具有可能会有不一致的行为了。

在最初尝试将项目从 babel 迁移到 typescript 上时,就遇到了这个问题。对于 babel 而言,会把 cjs 模块作为 default 导入。参考下面这个例子

// cjs.js
module.exports = {
  a:1,
  b:2
}
// esm.js
export default {a:1,b:2}
export const c = 3
const d = 4
const e = 5
export {d, e}

// index.js
import c from './cjs'
import {a} from './cjs'
import es from './esm'
import * as es2 from './esm'
import {d} from './esm'
console.log(a)
console.log(c)
console.log(es)
console.log(es2)
console.log(d)

经过 babel 转译后的代码如下:

// index.js
var _cjs = require('./cjs');
var _cjs2 = _interopRequireDefault(_cjs);
var _esm = require('./esm');
var es2 = _interopRequireWildcard(_esm);
function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } }
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
console.log(_cjs2.default);
console.log(_cjs.a);
console.log(es2.default);
console.log(es2);
console.log(_esm.d);

// esm.js
"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.default = { a: 1, b: 2 };
var c = exports.c = 3;
var d = 4;
var e = 5;
exports.d = d;
exports.e = e;

对于 ESM 文件, babel 添加了 _esModule 属性表示该模块为 ES Module, 然后在 exports 上绑定了 default 输出

对于 index.js 文件,可以发现在 导入 cjs 文件时,首先 require 该文件,然后将其所有导出的字段绑定到了 default 字段。

然而对于 Typescript,最初它是和 babel 这样的导入形式不兼容的。
import React from 'react' 这样会报错。

TypeScript 的出现远早于 ES2015 定稿。而 ES 规范中的标准化模块系统是从 ES 2015 中才开始出现的,所以最初 TS 使用的模块系统叫做 Namespace。后来由于 ESM 的标准定稿, TS 也逐渐放弃了自己的模块系统转而支持 ESM 的规范。相关的讨论可以看下面的 PR
https://github.com/Microsoft/TypeScript/pull/2460

TS 把 CJS 模块作为一个 Namespace 导入,所以,为了解决上面提到的报错,需要这样导入 CJS 模块,以及任何没有 default 导出的模块:
import * as React from 'react'

这样子的代码,如果从 babel 迁移到 TS 就需要大幅的改动代码,不过 TS 也注意到了这个问题,添加了一个 compile option 支持 babel 的这种写法 esModuleInterop, PR 在下面
https://github.com/Microsoft/TypeScript/pull/19675

// tsconfig.json
{
    "esModuleInterop": true
}

最后

由于历史遗留问题, ESM 和 CJS及其他现存的模块系统之间的交互总是会或多或少遇到一些坑,这样的讨论也到处可见,所以团队中还是应该遵循统一的 模块导入导出 标准,能基于约定形成一套标准在之后需要改变迁移的时候可能也会更加方便一些。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容