Open FrankKai opened 2 years ago
遇到一个很有趣的场景,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进行转化进行支持的。
tsconfig.json
{ "compilerOptions": { "target": "ES2015", "module": "esnext", } }
打包结果:
import foo from "./foo"; import bar from './bar'; export { foo, bar }; //# sourceMappingURL=index.js.map
{ "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 });
exports.xxx
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) // 这里就没问题了。
除了默认引入缺少default的情况,按照namespace方式引入的情况,也需要配置esModuleInterop去兼容。
先来看看ts官方文档:https://www.typescriptlang.org/tsconfig#esModuleInterop
默认情况下,esModuleInterop关闭,ts按照CommonJS/AMD/UMD模块处理为es6模块一样去处理。有两种情况下不能这样去处理:
开启后可以避免这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/148081795
esm引入cjs可以interop(互操作)的核心思想是:esm有default,而cjs没有,为cjs模块增加default。
引用一段作者的话,很精简:
目前很多常用的包是基于 cjs / UMD 开发的,而写前端代码一般是写 esm,所以常见的场景是 esm 导入 cjs 的库。但是由于 esm 和 cjs 存在概念上的差异,最大的差异点在于 esm 有 default 的概念而 cjs 没有,所以在 default 上会出问题。TS babel webpack 都有自己的一套处理机制来处理这个兼容问题,核心思想基本都是通过 default 属性的增添和读取
antd@3.26.20
Object.defineProperty(exports, "__esModule", { value: true }); Object.defineProperty(exports, "Affix", { enumerable: true, get: function get() { return _affix["default"]; } }); var _affix = _interopRequireDefault(require("./affix")); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }
uuid@7.0.3
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); Object.defineProperty(exports, "v1", { enumerable: true, get: function () { return _v.default; } }); var _v = _interopRequireDefault(require("./v1.js")); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
axios@0.19.2比较直接,直接为module.exports增加了default属性。 还加了一行贴心的注释,哈哈哈。
module.exports = axios; // Allow use of default import syntax in TypeScript module.exports.default = axios;
Tuya开发的微前端框架medusa,也是通过tsc进行打包的,同样开启了tsconfig.json中的esModuleInterop选项。
Object.defineProperty(exports, "__esModule", { value: true }); var router_1 = require("./router"); Object.defineProperty(exports, "Router", { enumerable: true, get: function () { return __importDefault(router_1).default; } });
开源地址:https://github.com/tuya/medusa 配置地址:https://github.com/tuya/medusa/blob/main/packages/medusa/tsconfig.json
module改为commonjs。
babel会把import转为require。
兼容只有umd,cjs方式且没有暴露deault属性的包,添加default属性,从而使得import a from "a"或者import * as a from "a"引入的包,不会报没有default属性。例如query-string@7.1.1这样的包。 保险起见,建议开启这个配置。
因为module为esnext时,代码直接就是esModule模式,也就是import, default模式,不会被转为cjs并带一个尾缀default的方式。
可以说,怎么写的,打包出来就是原模原样的。
import webcVCS from "./webcVCS"; import generateAssets from './generateAssets'; export { webcVCS, generateAssets, };
esnext: 只在esm环境使用的包 commonjs:纯cjs或既在cjs又在esm环境使用的包(esm环境使用一般是由安装包的项目,结合webpack,babel等打包工具支持的) umd: 同commonjs,且需要同时支持cjs,amd, cmd
问题场景
遇到一个很有趣的场景,cjs中需要引入原先打包方式为esm方式的模块。
也就是想要通过require(),去引入一个export的模块。
my-npm-package包的暴露方式为:
支持的方式为
cjs中想要使用esm方式的包
会报错:SyntaxError: Cannot use import statement outside a module
那么如何使得原先仅支持esm方式的包,改造为既支持esm又支持cjs呢? 打包方式commonjs。 这只支持了cjs,esm怎么支持呢? 支持esm是通过引入包的项目的babel进行转化进行支持的。
npm包改造前,仅支持esm
tsconfig.json
打包结果:
npm包改造后,既支持esm,又支持cjs
tsconfig.json
打包结果:
cjs: exports.xxx esm: Object.defineProperty(exports, "__esModule", { value: true });
可以“csj引入原先方式为esm包”的原因是什么?
原先esm方式的包,还可以正常使用的原因是什么?
那就是“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这个依赖。
经过tsc打包后,会转换为为query_string_1.default。
但是query-string@7.1.1的index.js,并没有暴露default。
转换后
那么如何解决这个问题呢?开启tsconfig.json中的esModuleInterop为true。 从而将exports作为default返回。
打包结果:
不仅仅是index.js会注入importDefault ,所有经过tsc编译的ts文件,都会注入importDefault。
经过__importDefault 转换后,变为
转换后
如何理解ts编译配置esModuleInterop?
除了默认引入缺少default的情况,按照namespace方式引入的情况,也需要配置esModuleInterop去兼容。
先来看看ts官方文档:https://www.typescriptlang.org/tsconfig#esModuleInterop
默认情况下,esModuleInterop关闭,ts按照CommonJS/AMD/UMD模块处理为es6模块一样去处理。有两种情况下不能这样去处理:
开启后可以避免这2个问题:
禁用时(直接require):
开启时(辅助导入函数importStar, importDefault):
再来看一下知乎上一位前端同学的文章:https://zhuanlan.zhihu.com/p/148081795
esm引入cjs可以interop(互操作)的核心思想是:esm有default,而cjs没有,为cjs模块增加default。
引用一段作者的话,很精简:
其它npm包中类似__importDefault的处理
antd和uuid(_interopRequireDefault)
antd@3.26.20
uuid@7.0.3
axios(module.exports.default)
axios@0.19.2比较直接,直接为module.exports增加了default属性。 还加了一行贴心的注释,哈哈哈。
tuya微前端框架medusa(也是__importDefault)
Tuya开发的微前端框架medusa,也是通过tsc进行打包的,同样开启了tsconfig.json中的esModuleInterop选项。
开源地址:https://github.com/tuya/medusa 配置地址:https://github.com/tuya/medusa/blob/main/packages/medusa/tsconfig.json
有哪些包是要开启esModuleInterop后,在esm环境中引入的?
总结
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的方式。
可以说,怎么写的,打包出来就是原模原样的。
5.以后打包,module怎么配置?
esnext: 只在esm环境使用的包 commonjs:纯cjs或既在cjs又在esm环境使用的包(esm环境使用一般是由安装包的项目,结合webpack,babel等打包工具支持的) umd: 同commonjs,且需要同时支持cjs,amd, cmd