esModule + NodeJS + TypeScript 的工程配置

JavaScript的模块化方案经历了长时间的发展,最终于2015年在ES6实现了语言层面的标准化,即esModule(以下简称esm), 现在JS社区开始拥抱esm,很多npm包仅采用esm发布。
而NodeJS一直依赖采用CommonJS的模块化方案,在最近发布的版本中也开始支持ems, 由于巨大的历史包袱,NodeJS并没有抛弃CommonJS,所以在NodeJS中实际支持两种模块化方式,esm和CommonJS.
此外,现在很多前端工程使用TypeScript开发,而TS的模块解析方式也需要进行一些配置,在这3者结合的过程中有很多坑,本文介绍将三者完美结合的最佳实践。
<!--more-->同时引入CommonJS包和ESM包
为了测试我们同时引入lodash的CommonJS版本和ESM版本
npm i lodash-es lodash
在NodeJS中,模块作者除非显示指定使用esm方式加载模块:
- 使用
.mjs - 在
package.json中的type字段指定为module - 使用
--input-type标记
否则默认加载方式为CommonJS。
而上面的例子中lodash-es的package.json中的type就是module。
使用CommonJS包
创建一个common-js.js文件:
const _ = require('lodash');
console.log(_.VERSION); // print '4.17.21'
以上是CommonJS的写法,很习以为常,能够正确打印。
使用esm包
创建一个esm.js文件:
import _ from 'lodash-es';
console.log(_.VERSION);
第1个坑出现了, 报语法错误:SyntaxError: Cannot use import statement outside a module
说无法在模块外部使用import语句,一旦一个文件有顶级(top-level)的import或export,它会被当作一个模块,否则是一个脚本文件。
所以我们需要把这个文件变成esm,两种方式:
- 重命名文件,修改扩展名为
.mjs; - 在
package.json中添加"type": "module"
但如果采用选项2,第2个坑就会出现了, 回去运行common-js.js报错:ReferenceError: require is not defined in ES module scope, you can use import instead。
说require在esm中不支持,此时可以重命名common-js.js后缀为.cjs,告诉模块加载器,此文件以CommonJS加载。
引入Typescript
安装TS:
npm i typescript ts-node -D
ts-node 可以在nodejs中直接运行ts文件而无需编译
然后将刚才的esm.js和common-js.cjs扩展名改为.ts
使用ts-node命令直接运行esm.ts
npx ts-node esm.ts
第3个坑出现了,扩展名不支持:TypeError [ERR_UNKNOWN_FILE_EXTENSION]: Unknown file extension ".ts" for xxx
这是因为esm的默认扩展名是js而不是ts,此时我们需要:
ts-node --esm- 添加
"ts-node": {"esm": true}到tsconfig.json。
如果采用方案1,第4个坑出现了,无法找到模块:error TS7016: Could not find a declaration file for module 'lodash-es'。
此时添加配置: "esModuleInterop": true到tsconfig.json的compilerOptions中。
添加完成后第5个坑出现了,引用错误:ReferenceError: exports is not defined in ES module scope
此时添加配置module": "ESNext"到tsconfig.json的compilerOptions中。
添加完成后第6个坑出现了,找不到模块:ReferenceError: exports is not defined in ES module scope
此时配置模块的解析方式为最新的"moduleResolution": "Node16"。
到此终于可以成功运行esm.ts了。
返回去运行common-js.ts:
npx ts-node common-js.ts
第7个坑出现了,它把我们的CommonJS模块当esm了:ReferenceError: require is not defined in ES module scope, you can use import instead
此时处于鱼和熊掌不可兼得,因为我们设置了module": "ESNext", 所以ts的编译结果会添加export {},所以如果要在纯esm的工程中仍然要使用CommonJS,则使用.cjs作为扩展名即可。