关于javascript:tsconfigjson的esModuleInterop使用场景是怎样的

49次阅读

共计 5793 个字符,预计需要花费 15 分钟才能阅读完成。

  • 问题场景
  • npm 包革新前,仅反对 esm
  • npm 包革新后,既反对 esm,又反对 cjs
  • 为什么革新后,还是会报错?
  • 如何了解 ts 编译配置 esModuleInterop?
  • 总结

问题场景

遇到一个很乏味的场景,cjs 中须要引入原先打包形式为 esm 形式的模块。

也就是想要通过 require(),去引入一个 export 的模块。

my-npm-package 包的裸露形式为:


import foo from "./foo";
import bar from './bar';
export {foo, bar};

反对的形式为

import {foo, bar} from 'my-npm-package';

cjs 中想要应用 esm 形式的包

const {foo} = require("my-npm-package");

会报错:SyntaxError: Cannot use import statement outside a module

那么如何使得原先仅反对 esm 形式的包,革新为既反对 esm 又反对 cjs 呢?
打包形式 commonjs。
这只反对了 cjs,esm 怎么反对呢?
反对 esm 是通过引入包的我的项目的 babel 进行转化进行反对的。

npm 包革新前, 仅反对 esm

tsconfig.json

{
  "compilerOptions": {
    "target": "ES2015",
    "module": "esnext",
  }
}

打包后果:

import foo from "./foo";
import bar from './bar';
export {foo, bar};
//# sourceMappingURL=index.js.map

npm 包革新后,既反对 esm,又反对 cjs

tsconfig.json

{
  "compilerOptions": {
    "target": "ES2015",
    "module": "commonjs"
  }
}

打包后果:

"use strict";
Object.defineProperty(exports, "__esModule", { value: true});
exports.bar = exports.foo = void 0;
const foo_1 = require("./foo");
exports.foo = foo_1.default;
const bar_1 = require("./bar");
exports.bar = bar_1.default;
//# sourceMappingURL=index.js.map

cjs: exports.xxx
esm: Object.defineProperty(exports, “__esModule”, { value: true});

能够“csj 引入原先形式为 esm 包”的起因是什么?

exports.xxx

原先 esm 形式的包,还能够失常应用的起因是什么?

Object.defineProperty(exports, "__esModule", { value: true});

那就是“__esModule”,webpack 会依据__esModule,将模块辨认为 esm,最初通过 babel 转化为 cjs 模块形式引入。

回到咱们的场景:革新 esm 模块为既反对 cjs,又反对 esm,能实现的起因是什么?

第一步:target 从 esm 改为 commonjs,从而反对 cjs
第二步:这一步其实不必做,主我的项目的 babel 曾经做了配置,对于所有 esm 和 cjs 的包,都能够通过 esm 形式引入。

为什么革新后,还是会报错?

先说论断:因为 tsc cjs 形式打包,默认会把 import a from ‘a’, a.method() 的包,转化为 const a_1 = require(‘a’),a_1.default.method()。而有些 npm 包,没有 exports.default。
如何解决:开启 esModuleInterop。

TypeError: Cannot read properties of undefined (reading ‘stringify’)

这是因为,在咱们的 npm 包中,有应用到 query-string 这个依赖。

import queryString from 'query-string';
const query_string_1 = require("query-string");
query_string_1.default.stringify(body) // 这里产生了报错 

通过 tsc 打包后,会转换为为 query_string_1.default。

然而 query-string@7.1.1 的 index.js,并没有裸露 default。

转换后

const query_string_1 = exports;
// query-string@7.1.1
exports.parseUrl
exports.stringifyUrl 
exports.pick
exports.exclude
exports.stringify
exports.extract
exports.parse

那么如何解决这个问题呢?开启 tsconfig.json 中的 esModuleInterop 为 true。
从而将 exports 作为 default 返回。

{
  "compilerOptions": {
    "target": "ES2015",
    "module": "commonjs",
    "esModuleInterop": true
  }
}

打包后果:

// index.js
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {return (mod && mod.__esModule) ? mod : {"default": mod};
};
Object.defineProperty(exports, "__esModule", { value: true});
exports.bar = exports.foo = void 0;
const foo_1 = __importDefault(require("./foo"));
exports.foo = foo_1.default;
const bar_1 = __importDefault(require("./bar"));
exports.bar = bar_1.default;
//# sourceMappingURL=index.js.map

不仅仅是 index.js 会注入__importDefault,所有通过 tsc 编译的 ts 文件,都会注入__importDefault。

// foo.js
var __importDefault = (this && this.__importDefault) || function (mod) {return (mod && mod.__esModule) ? mod : {"default": mod};
};
const query_string_1 = __importDefault(require("query-string"));

通过__importDefault 转换后,变为

const query_string_1 = __importDefault(exports);

转换后

const query_string_1 = {default: exports};
query_string_1.default.stringify(body) // 这里就没问题了。

如何了解 ts 编译配置 esModuleInterop?

除了默认引入短少 default 的状况,依照 namespace 形式引入的状况,也须要配置 esModuleInterop 去兼容。

先来看看 ts 官网文档:https://www.typescriptlang.or…

默认状况下,esModuleInterop 敞开,ts 依照 CommonJS/AMD/UMD 模块解决为 es6 模块一样去解决。有两种状况下不能这样去解决:

  • ❌ import * as moment from “moment” 当做 const moment = require(“moment”)
  • ❌import moment from “moment” 当做 const moment = require(“moment”).default

开启后能够防止这 2 个问题:

import * as fs from "fs";
import _ from "lodash";
fs.readFileSync("file.txt", "utf8");
_.chunk(["a", "b", "c", "d"], 2);

禁用时 (间接 require):

"use strict";
Object.defineProperty(exports, "__esModule", { value: true});
const fs = require("fs");
const lodash_1 = require("lodash");
fs.readFileSync("file.txt", "utf8");
lodash_1.default.chunk(["a", "b", "c", "d"], 2);

开启时 (辅助导入函数__importStar, __importDefault):

"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;
};
var __importDefault = (this && this.__importDefault) || function (mod) {return (mod && mod.__esModule) ? mod : {"default": mod};
};
Object.defineProperty(exports, "__esModule", { value: true});
const fs = __importStar(require("fs"));
const lodash_1 = __importDefault(require("lodash"));
fs.readFileSync("file.txt", "utf8");
lodash_1.default.chunk(["a", "b", "c", "d"], 2);

再来看一下知乎上一位前端同学的文章:https://zhuanlan.zhihu.com/p/…

esm 引入 cjs 能够 interop(互操作)的核心思想是:esm 有 default,而 cjs 没有,为 cjs 模块减少 default。

援用一段作者的话,很精简:

目前很多罕用的包是基于 cjs / UMD 开发的,而写前端代码个别是写 esm,所以常见的场景是 esm 导入 cjs 的库。然而因为 esm 和 cjs 存在概念上的差别,最大的差别点在于 esm 有 default 的概念而 cjs 没有,所以在 default 上会出问题。TS babel webpack 都有本人的一套解决机制来解决这个兼容问题,核心思想根本都是通过 default 属性的削减和读取

总结

1. 如何将 esm 模块打包为 cjs?

module 改为 commonjs。

2. 为什么 esm 能够通过 import 援用 cjs 的包?

babel 会把 import 转为 require。

3. 如何了解 esModuleInterop?

兼容只有 umd,cjs 形式且没有裸露 deault 属性的包,增加 default 属性,从而使得 import a from “a” 或者 import * as a from “a” 引入的包,不会报没有 default 属性。例如 query-string@7.1.1 这样的包。
保险起见,倡议开启这个配置。

4. 为什么 module 为 esnext 时不会报错?

因为 module 为 esnext 时,代码间接就是 esModule 模式,也就是 import, default 模式,不会被转为 cjs 并带一个尾缀 default 的形式。

能够说,怎么写的,打包进去就是原模原样的。

import webcVCS from "./webcVCS";
import generateAssets from './generateAssets';
export {webcVCS, generateAssets,};
import queryString from 'query-string';

5. 当前打包,module 怎么配置?

  • esnext: 只在 esm 环境应用的包
  • commonjs:纯 cjs 或既在 cjs 又在 esm 环境应用的包(esm 环境应用个别是由安装包的我的项目,联合 webpack,babel 等打包工具反对的)
  • umd: 同 commonjs,且须要同时反对 cjs,amd, cmd

正文完
 0