本篇我们要说的是ts项目中的一个很大的gap,esm模块依赖问题。一开始我遇到这个问题的时候以为没什么。华人社区还是老样子,只能找几个字。于是去外语社区投诉。凹陷的脚,扁平的脚,让每个人都非常痛苦。真没想到后面的水这么深。
所以本文将从头到尾梳理一下我遇到的所有问题,把我平时在节点模块化方案上找到的知识点串起来,希望能让大家少走弯路。
开始探索
首先,让我们创建一个小演示并找到一个空文件夹来安装必要的依赖项:
npm 安装 ts-node typescript nanoid复制代码
然后增强项目并添加一个非常简单的tsconfig.json:
{ “编译器选项”: { “outDir”: “距离”, “跳过库检查”: 是正确的, “严格”: 是正确的, “无发射”: 不正确, “目标”: “ES5” }, “包含装有”: [ “**/*.ts”],}复制代码
然后在package.json中加入我们的ts-node执行命令,执行后ts-node读取index.ts并执行代码:
“脚本”: { “开发商”: “ts-node --files ./index.ts”, “积聚”: “CH”}复制代码
现在环境已经准备好了,让我们创建一个新的。索引.ts
:
直到更多 = (一:数字,b:数字):数字=>一个 + b;安慰.协议(最多(1个,1个))复制代码
很简单,实现一个加法函数。现在我们跑npm 运行开发
您可以看到以下结果:
ok,应该可以,所以我们重现了这个问题,一开始我们安装了一个比较通用的包纳米化物
🇧🇷这个包很简单,应该会生成一个类似于uuid的字符串,
纳米化物()// 赛达 V1StGXR8_Z5jdHi6B-myT复制代码
安装后,让我们从 index.ts 中调用它:
导入{纳米}为一个 “纳米”;安慰.协议(纳米化物());直到更多 = (一:数字,b:数字):数字=>一个 + b;安慰.协议(最多(1个,1个));复制代码
所以跑npm 运行开发
:
乙:\learn\ts-esm-juejin\node_modules\ts-node\dist\index.js:842 退货alt(m, 文件名); 🇧🇷错误[ERR_REQUIRE_ESM]:要求()冯ES模块E:\learn\ts-esm-juejin\node_modules\nanoid\index.js为一个E:\learn\ts-esm-juejin\index.ts不支持。相反,更改要求冯索引.js里面E:\learn\ts-esm-juejin\index.ts为了一个动态的 import() 那这是无障碍里面所有 CommonJS-Module bei目的.require.extensions.<计算> [像.js] (E:\learn\ts-esm-juejin\node_modules\ts-node\dist\index.js:842:20) 不目的.<anônimo> (E:\learn\ts-esm-juejin\index.ts:3个:16) 不模块.m._compile(E:\learn\ts-esm-juejin\node_modules\ts-node\dist\index.js:848:29) 不目的.require.extensions.<计算> [像.ts] (E:\learn\ts-esm-juejin\node_modules\ts-node\dist\index.js:850:16) 不异步Promise.all(索引0) {代码:'ERR_REQUIRE_ESM'} 终端进程“C:\WINDOWS\System32\WindowsPowerShell\v1.0\powershell.exe -Befehl npm run dev”总结通讯 出口代码:1个.复制代码
它是什么index.ts 中的 Require() 不能用于引用包 es nanoid
🇧🇷我必须在哪里写?
原因是ts节点是先把你的代码编译成js代码,然后提交给node执行(不太了解的可以看之前的文章typepath别名问题详解及前世今生)。
所以我们可以先跑npx tsc
只是看看(如果你在开发 ts 时遇到问题,你可以运行 tsc。如果构建成功,说明节点运行有问题。如果没有,说明ts编译有问题。):
如您所见,编译后的代码使用需要('纳米')
,使得节点在重新运行后立即报告错误。
下面我们再深挖一下,为什么使用require引入Nanoid会报错呢?原因是最新版本的nanoid只支持esm导出格式🇧🇷这意味着我们只能用 import 导入这个包。与 nanoid 类似的还有 Node-Fetch 和 Chalk,后两者的最新版本也宣布它们现在只支持 esm 导出格式。
那么问题就出现了,因为现在打包的目的是通用JS
,所以现在有两个选择:
- 首先是将当前项目的打包格式从 CommonJS 升级为 ESM。
- 另一种是回去将依赖的第三方包降级到仍然支持 CommonJS 的版本。
两种方式都可以,先说最简单的吧。
依赖降级
如果您在处理企业项目时遇到此问题,最简单的方法是降级所需的依赖项。
具体方法也很简单:打开相关包的npm页面,找到版本选择之前的大版本或者安装程序多的版本:
由于只有 ESM 支持是破坏性更新,继续语义版本包基本直接升级到大版本。
将项目更新为esm
这条路上的坑洼比较多。在到达终点之前,我绊倒了,摔断了头。请系好安全带。
首先我们知道编译是通过tsc完成的,所以可以通过改变tsconfig.json来调整build product模式。那么我们可以修改编译器选项
在下面模块
、目标
编译代码直接使用import引入依赖的字段。
{ “编译器选项”: { // ... “模块”: “ES2022”, “目标”: “16号”, }, // ...}复制代码
至于两者的区别,我们下面再说。现在我们npm 运行开发
只是看看:
D:\project\ts-esm-juejin\node_modules\ts-node\src\index.ts:843 退货 纽 TS错误(诊断文本、诊断代码、诊断); ^TSError: ⨯ TypeScript:index.ts 无法编译:1个:24- 错误 TS2792:找不到模块'纳米'🇧🇷你想设置它吗?'模块分辨率'选项'它',或者添加昵称'方法'可能性?1个导入{纳米}为一个 “纳米”; ~~~~~~~~em错误(丁:\project\ts-esm-juejin\node_modules\ts-node\src\index.TS:843:12) 不TS错误报告(丁:\project\ts-esm-juejin\node_modules\ts-node\src\index.TS:847:19) 不得到输出(丁:\project\ts-esm-juejin\node_modules\ts-node\src\index.TS:1057:36) 不反对。编译(丁:\project\ts-esm-juejin\node_modules\ts-node\src\index.TS:1411:41) em Module.m。_编译(丁:\project\ts-esm-juejin\node_modules\ts-node\src\index.TS:1596:30) em Module._extensions..js(它:内部/模块/cjs/谷仓:1153:10) 不反对。要求.extensions.<计算> [像.ts] (D:\project\ts-esm-juejin\node_modules\ts-node\src\index.ts:1600:12) 即时取模负担(它:内部/模块/cjs/谷仓:981:32) em 函数.模块。_负担(它:内部/模块/cjs/谷仓:822:12) bei Function.executeUserEntryPoint [像runMain](或:interno/modules/run_main:81:12) { 诊断代码: [2792]}终端进程“C:\WINDOWS\System32\WindowsPowerShell\v1.0\powershell.exe -Befehl npm run dev”结束了出口代码:1个.复制代码
好的,错误报告已更改。你可以看到它说在提示符下指定模块解析:节点
可以解决此问题。这是一口井, 正确的做法是把它放在模块分辨率:Node16
。
这里解释一下很多人比较困惑的地方,moduleResolution、module和target的区别:
- moduleResolution 用于指定typescript如何查找代码中引入的文件
- module 表示代码使用的模块化方案
- target 表示最终代码将在哪个版本中编译
单说这个可能有点乱,我们先从最简单的说起:
目标领域
该字段表示对应于我们的压缩代码的 js 版本,因此 target 选项只是 ES 版本号:
但是,最终的结果代码并不一定与版本匹配。 destination字段标示了打包的大体方向,一些细节会受到其他参数的影响,比如module字段,下面会讲到。
莫德尔费尔德
当前项目使用的模块化方案,也就是它的options,都是js历史上出现过的模块化方案:
不熟悉的同学可能有点疑惑,为什么会有这么多ESXXX?模块化的Node方案不就是CommonJS、ES6等一些老方案吗?
咦,这个你不知道吗,随着ES版本的变化,ES6的模块化方案也在不断演进。比如ES2020增加动态导入
电子导入.meta
🇧🇷 Node16 及以上版本的 NodeNext/ESNext 改进了 esm 解决方案的兼容性并使其成为使用 CommonJS 解决方案的模块可以本地导入🇧🇷可以参考TypeScript:TSConfig 参考明白就好。
还,虽然所有这些版本的 ES 都使用 import/export,但每个版本都有细微差别,因此它实际上是一种不同的模块化方案。
言归正传,module参数优先级高于target,所以压缩module导入相关代码时,会优先使用module指定的solution,否则由target寻找匹配的solution,例如目的地:ES3
将使用通用JS
🇧🇷这些在文档中有说明:
模块分辨率
最后说一下这个moduleResolution是什么如何查找导入的文件,所以可以看到选项集中在几个差异比较大的方案上。
里面,Node 表示 CommonJS 解决方案,Node16 和 NodeNext 是 ESM 解决方案🇧🇷经典解决方案是 TypeScript 错误。初始阶段(1.x 版)实施的导入逻辑与当前的 ESM 解决方案不同。因此,该选项是为了向后兼容,不能用于新项目。
这个选项的主要目的是为了应对越来越复杂的js模块化方案。一个项目中可以同时存在几种不同的模块化方案。而这个选项代表了一个“逃生通道”。比如我想在最后生成ESM代码,但在编译时使用commonjs解决方案引入其他模块。
探索更多
好吧,回到主题,我们当前的 tsconfig.json 应该是这样的:
{ “编译器选项”: { “outDir”: “距离”, “跳过库检查”: 是正确的, “严格”: 是正确的, “无发射”: 不正确, “目标”: “ES2022”, “模块”: “16号”, “模块分辨率”: “16号” }, “包含装有”: [ “**/*.ts”]}复制代码
好的,现在继续npm 运行开发
:
D:\Projekt\ts-esm-juejin\node_modules\ts-node\源\索引.ts:843返回新的 TSError(DiagnosticText、DiagnosticCodes、Diagnostics); ^TSError: ⨯ 无法编译 TypeScript:index.ts:1:24 - 错误 TS1471:无法使用此构建导入模块“nanoid”。 identifier 只能解析为 ES 模块,不能同步导入。请改用动态导入。1 import { nanoid }为一个“纳米化物"; ~~~~~~~~不 错误(丁:\project\ts-esm-juejin\node_modules\ts-node\src\index.TS:843:12)不 TS错误报告(丁:\project\ts-esm-juejin\node_modules\ts-node\src\index.TS:847:19)不 得到输出(丁:\project\ts-esm-juejin\node_modules\ts-node\src\index.TS:1057:36)不 目的。编译(丁:\project\ts-esm-juejin\node_modules\ts-node\src\index.TS:1411:41)不 模块.m。_编译(丁:\project\ts-esm-juejin\node_modules\ts-node\src\index.TS:1596:30)不 模块._扩展..js(它:内部/模块/cjs/谷仓:1153:10)不 目的.需要.扩展名.<计算的>[作为.ts](丁:\project\ts-esm-juejin\node_modules\ts-node\src\index.TS:1600:12)不 模块。负担(它:内部/模块/cjs/谷仓:981:32)不 职业。模块。_负担(它:内部/模块/cjs/谷仓:822:12)不 职业.executeUserEntryPoint [作为 runMain](它:实习生/模块/run_main:81:12) { 诊断代码: [ 1471 ]}这是 终端 过程“C:\窗户\系统32\WindowsPowerShell\v1.0\电源外壳。可执行程序 -命令 npm 正确的 开发商“总结 通讯 出口 代码:1个.复制代码
Face Slapping 这又是一招,不过不用担心,我们的esm项目改造还没有完成。查看错误消息,它说只能在ES模块中使用import引入nanoid模块
🇧🇷节点是否认为当前项目不是esm?
是的,确实是这个原因。由于 esm 和 commonJS 解决方案之间的巨大差异。 Node 需要一些东西来告诉 npm 安装的包使用哪种方案,即包.json
冯类型
建立:
可以看到,它只有两个值,commonjs是type字段的默认值。所以我们必须明确指定类型:模块
这样节点就知道这个项目是一个ESM模块。
好的,我们添加后再回来npm 运行开发
试试看,这次成功了!
类型错误 [ERR_UNKNOWN_FILE_EXTENSION]:未知 文件 装修“.ts“为了 丁:\项目\ts-esm-绝劲\指数.ts 不 纽 没有的错误(它:实习生/错误:371:5个)不 目的。文件: (它:内部/模块/esm/获取格式:72:十五)不 默认获取格式(它:内部/模块/esm/获取格式:85:38)不 默认加载(它:内部/模块/esm/负担:13:42)不 ESMLoader通用名称。负担(它:内部/模块/esm/谷仓:303:26)不 ESMLoader通用名称.moduleProvider(它:内部/模块/esm/谷仓:230:58)不 纽 模块作业(它:内部/模块/esm/模块作业:63:26)不 ESMLoader通用名称.getModuleJob(它:内部/模块/esm/谷仓:244:11)不 异步 承诺。不(指数0)不 异步 ESMLoader通用名称.导入(它:内部/模块/esm/谷仓:281:24){代码:'ERR_UNKNOWN_FILE_EXTENSION'}这是 终端 过程“C:\窗户\系统32\WindowsPowerShell\v1.0\电源外壳。可执行程序 -命令 npm 正确的 开发商“总结 通讯 出口 代码:1个.复制代码
又是一个新bug,那个无法识别的ts后缀是什么鬼?我完全不知道。事实上,如果你只是打赌模块分辨率
定义像它
也报这个错误。
在这个地方卡了几个小时,通过stackoverflow搜索,终于发现这个问题是ts-node引起的,在ts节点文档看:
原生 ECMAScript 模块
ts-node 的 ESM 支持尽可能稳定,但它取决于 node 可以做的 API还会打破新版本的节点。因此,不建议用于生产。
你必须调整“类型”:“模块”里面
包.json
电子“模块”:“ESNext”里面tsconfig.json文件
.您还需要确保节点已通过
- 充电器
🇧🇷 ts-node-CLI 使用我们的自动执行此操作ESM
可能性。
注意最后一句,因为ts-node默认使用commonjs的方案来解决依赖;所以,如果采用esm方案,就不能使用ts-node原有的调用方式了。这里就不重复了,我选择下面的方案,修改一下包.json
, 用 ts-node-esm 替换 ts-node:
“脚本”: { “开发商”: “ts-node-esm --files ./index.ts”}复制代码
然后我们跑npm 运行开发
:
感谢上帝!我们终于成功地重现了代码!
但你以为是这样吗?不不不,可怕的还在后头,还记得我们一开始写的加法函数吗最多
现在让我们将它移动到另一个文件工具.ts
在,所以在索引.ts
引用他的话:
工具.ts
出口 直到更多 = (一:数字,b:数字):数字=>一个 + b;复制代码
索引.ts
导入{纳米}为一个 “纳米”;导入{ 最多 }为一个 '。/有用'安慰.协议(纳米化物());安慰.协议(最多(1个,1个));复制代码
这很正常,不是吗?现在做npm 运行开发
:
D:\project\ts-esm-juejin\node_modules\ts-node\src\index.ts:843 退货 纽 TS错误(诊断文本、诊断代码、诊断); ^TSError: ⨯ TypeScript:index.ts 无法编译:2个:22- 错误 TS2835:相对导入路径在 EcmaScript 导入时需要显式文件扩展名'--moduleResolution'这是'no16' 或者 '节点文本'🇧🇷你的意思'./utils.js'?2个导入{更多}为一个 '。/有用'~~~~~~~~~em错误(丁:\project\ts-esm-juejin\node_modules\ts-node\src\index.TS:843:12) 不TS错误报告(丁:\project\ts-esm-juejin\node_modules\ts-node\src\index.TS:847:19) 不得到输出(丁:\project\ts-esm-juejin\node_modules\ts-node\src\index.TS:1057:36) 不反对。编译(丁:\project\ts-esm-juejin\node_modules\ts-node\src\index.TS:1411:41) 不转换源(丁:\project\ts-esm-juejin\node_modules\ts-node\src\esm.TS:400:37) em D:\project\ts-esm-juejin\node_modules\ts-node\src\esm.ts:278:53在异步添加短路标志(丁:\project\ts-esm-juejin\node_modules\ts-node\src\esm.TS:409:十五) 与异步 ESMLoader。负担(它:内部/模块/esm/谷仓:303:20) 与异步 ESMLoader。模块供应商(它:内部/模块/esm/谷仓:230:47) 异步捷径(它:内部/模块/esm/模块作业:67:21) { 诊断代码: [2835]}终端进程“C:\WINDOWS\System32\WindowsPowerShell\v1.0\powershell.exe -Befehl npm run dev”结束了出口代码:1个.复制代码
新错误!不过好在最后给出了解决方案,就是在utils导入文件中添加一个.js文件名:
导入{纳米}为一个 “纳米”;导入{ 最多 }为一个 './utils.js';安慰.协议(纳米化物());安慰.协议(最多(1个,1个));复制代码
这很可耻,不是吗?在一个ts项目中,代码写在ts文件中,但是现在导入的时候需要加上js后缀🇧🇷那是一种什么样的态度。
但就是这样,让我们继续模块:ECMAScript 模块 | Node.js v16.15.1 文档 (nodejs.org),其中模块介绍的介绍是这样写的:
使用相对路径时,需要文件扩展名🇧🇷另外,使用原生esm解决方案时,非常方便的功能“导入文件夹时默认访问index.ts文件”不可用;这里就不细说了,有兴趣的可以自己去试试。
那种扭曲的感觉真的很影响开发体验,如果你觉得好就好,我们只是多了一个问题需要解决。
最后一题:esm import cjs 模块
一开始我们提到如果一个模块只支持esm,那么只能用import引入。另一方面,如果一个模块只支持 commonjs,我们还可以导入它吗?
答案是肯定的,但不完全是。我们会尝试。下面以我们非常熟悉的lodash为例。先安装依赖:
npm 安装 lodash @types/lodash复制代码
然后将 map 函数导入 index.ts 并调用:
导入{纳米}为一个 “纳米”;导入{ 最多 }为一个 './utils.js';导入地图为一个 'Lodash/Karte';安慰.协议(纳米化物());安慰.协议(最多(1个,1个));安慰.协议(地图([1个,2个,3个,4个],(X) =>X*2个))复制代码
然后运行npm 运行开发
您可以看到以下错误:
D:\project\ts-esm-juejin\node_modules\ts-node\src\index.ts:843 退货 纽 TS错误(诊断文本、诊断代码、诊断); ^TSError: ⨯ TypeScript:index.ts 无法编译:3个:17- 错误 TS2307:找不到模块'Lodash/Karte' 或者他们相应的类型声明。3个导入地图为一个 'Lodash/Karte'; ~~~~~~~~~~~~em错误(丁:\project\ts-esm-juejin\node_modules\ts-node\src\index.TS:843:12) 不TS错误报告(丁:\project\ts-esm-juejin\node_modules\ts-node\src\index.TS:847:19) 不得到输出(丁:\project\ts-esm-juejin\node_modules\ts-node\src\index.TS:1057:36) 不反对。编译(丁:\project\ts-esm-juejin\node_modules\ts-node\src\index.TS:1411:41) 不转换源(丁:\project\ts-esm-juejin\node_modules\ts-node\src\esm.TS:400:37) em D:\project\ts-esm-juejin\node_modules\ts-node\src\esm.ts:278:53在异步添加短路标志(丁:\project\ts-esm-juejin\node_modules\ts-node\src\esm.TS:409:十五) 与异步 ESMLoader。负担(它:内部/模块/esm/谷仓:303:20) 与异步 ESMLoader。模块供应商(它:内部/模块/esm/谷仓:230:47) 异步捷径(它:内部/模块/esm/模块作业:67:21) { 诊断代码: [2307]}终端进程“C:\WINDOWS\System32\WindowsPowerShell\v1.0\powershell.exe -Befehl npm run dev”结束了出口代码:1个.复制代码
找不到错误消息Lodash/卡丁车
为什么这个模块是这样的,这不是一种很常见的按需加载形式吗?你也可以尝试不同的经典拼写从'lodash'导入{地图}
,也报错。
原因是,Lodash/卡丁车
是一个经典的commonjs代码拆分方案,当我们访问这个模块时,我们实际上是在寻找node_modules/lodash/map.js
这个文件🇧🇷而 lodash 只在 package.json 中指定“主要”:“lodash.js”
,所以我们采用的esm解决方案,如果不借助其他打包工具,是找不到这个js文件的,更不用说分析内部代码了。
那么如何解决这个问题呢?其实很简单,我们在第一次设置的时候在tsconfig.json中设置模块分辨率:Node16
🇧🇷为什么选择 Node16?这是因为在这个版本中,esm 提供了直接导入原生 commonjs 包的能力!
正如我们在节点文档中看到的那样:
如果你用import引用cjs模块,你的modulel.exports导出的对象会被解析成esm出口标准
🇧🇷而且这个方法非常靠谱。
事实上,为了提高兼容性,esm系统也会尝试解析exports中的named export,寻找合适的import-deconstruct write方法,但不能保证完全可靠。我建议你阅读详细内容模块:ECMAScript 模块 |节点.js v16.16.0中性的与 CommonJS 的互操作性引用上述内容的部分。
所以我们只需要重写上面的代码就可以直接导入所有的Lodash:
导入_为一个 'lodash'安慰.协议(_.地图([1个,2个,3个,4个],(X:数字) =>X*2个))复制代码
再次运行,你会看到它给出了正确的结果,所以我不再重复。
当然,这确实介绍了 Lodash 的所有内容,我们可以在 esm 包提供的 Lodash README 中找到更简单的方法:
寻找用 ES6 或更小的包编写的 Lodash 模块?钱lodash-es.
我们只需要安装npm 安装 lodash-es @types/lodash-es
, 那么你可以使用从 'lodash-es' 导入 { map }
直接引入相应的小模块,对减小封装体积有很大帮助。
但并不是所有的 CJS 包都有 ESM 支持,比如加密JS不是。然后你可以只导入整个包来使用它。
说到这里可能有同学会有疑惑,不对啊,我记得导入包很容易,没遇到过你上面说的各种问题。是的,主要是因为我们通常的打包目标是cjs,大多数包都是为了支持多种模块化解决方案而打包的。仍然有一些像 nanoid 这样的包只支持 esm,但随着时间的推移,这些包会越来越多,而不是越来越少。
如果您对与多模块解决方案的兼容性感兴趣,请查看此工具微包,它是一个基于 rollup 的零配置打包器,它根据 package.json 文件中 exports 字段的配置值生成合适的包。
顺便说一下,package.json 中的 exports 字段也是 Node 标准的一部分,从 Node 12 开始引入,文档在这里:模块:包 | Node.js v12.22.12 文档 (nodejs.org)
恢复
本文简单介绍下ts项目中的esm模块问题以及直接使用之前版本或者更激进自己升级esm的原因。
可以看出,从上到下的七种不同的错误信息,理解了背后的原理其实并不难,但是每当你认为找到了解决问题的办法时,马上又会出现新的错误信息,非常强的。
而且在网上找解决办法的时候,也有直接删除一些字段,求助于cjs的方法。如果你使用 dazed,你会发现,对吧?这个问题不是已经解决了吗?怎么又走了?比如下面这个类似于271的打墙武士(那些):
事实上,由于工具链的复杂性,我们在开发前端项目时对ESM的理解往往仅限于了解import/export的使用方式。但是在node的非前端部分,模块依赖问题立马暴露出来,尤其是在使用了ts之后。
所以想要对nodejs有更深入的了解,还是需要不断的开阔自己的视野,而不是只写前端的页面,互相鼓励。
一个警告
- 模块:包 | Node.js v15.14.0 文档 (nodejs.org)
- 错误:未知文件扩展名 .ts Issue #1062 TypeStrong/ts-node (github.com)
- 《推荐合集|坑》如何在NodeJs中使用模块化ESM规范🐣 - 掘金(juejin.cn)
- 无法使用 ts-node/esm 加载 TypeScript ESM 文件 Issue #7482 dotansimha/graphql-code-generator (github.com)
- CommonJS 模块对比原生 ECMAScript | ts 节点(typestrong.org)
- 模块:ECMAScript 模块 | Node.js v18.4.0 文档 (nodejs.org)
- 构建兼容 TS 的 Node Runtime - 简书(jianshu.com)
- 使用ts-node运行ts脚本以及你踩过的坑-掘金(juejin.cn)