3

tsconfig.json的esModuleInterop使用场景是怎样的?

 2 years ago
source link: https://www.fly63.com/article/detial/11858
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

更新日期: 2022-07-08阅读: 13标签: json分享

扫一扫分享

遇到一个很有趣的场景,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。

但是[email protected]的index.js,并没有暴露default。

const query_string_1 = exports;
// [email protected]
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属性。例如[email protected]这样的包。
保险起见,建议开启这个配置。

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
来源:https://segmentfault.com/a/1190000042084600

链接: https://www.fly63.com/article/detial/11858


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK