前言
最近在 typescript 项目上踩了不少坑,打算写几篇文章记录一下。
本篇文章就来梳理一下 ts 的相关技术栈 tsc、ts-node、ts-node-dev 中的路径别名问题,从开发到打包阶,不仅告诉你坑在哪,怎么解决,还会还会告诉你为什么会有这个坑以及坑背后的故事。
开始探索
本篇文章会以一个空项目开始,由浅入深的告诉你会遇到的各种问题,有兴趣的同学可以跟着往下做一下:
首先找一个空文件夹,请出本文的核心嘉宾 ts-node:
npm install ts-node typescript
然后完善一下项目,添加一个相当基础的 tsconfig.json:
{ "compilerOptions": { "outDir": "dist", "skipLibCheck": true, "strict": true, "noEmit": false, "module": "CommonJS", "baseUrl": ".", "paths": { "@/*": ["./*"] } }, "include": [ "**/*.ts"], }
然后在 package.json 里加入我们的 ts-node 执行命令,执行后 ts-node 就会去读 index.ts 并执行:
"scripts": { "dev": "ts-node --files ./index.ts" }
这样一个 typescript 开发环境就准备就绪了,然后我们新建 index.ts
和 utils.ts
:
utils.ts
export const plus = (a: number, b: number): number => a + b;
index.ts
import { plus } from "./utils"; console.log(plus(1, 1))
这些代码很简单,导出了一个加法函数,然后在 index.ts 里引用并打印了出来,现在我们执行 npm run dev
就可以看到如下结果:
没毛病,一切正常,现在我们通过路径别名引入 utils:
// 把 utils 的引入换成了 @ 路径别名 import { plus } from "@/utils";
然后再来执行一下:
出问题了,找不到对应的模块,这个问题其实不在 ts-node,而是因为 tsc 在编译代码时不会去把路径别名替换成对应的相对路径,所以 ts-node 用 tsc 编译完然后转交给 node 执行的时候自然就找不到 @/
这个目录了。
解决起来很简单,只需要安装一个包:
npm install tsconfig-paths
然后在 package.json
里改一下 dev
命令即可,这里的 -r 实际上是 node 的命令行参数,有兴趣的可以去看一下这个:Command-line API | Node.js v16.15.1 Documentation (nodejs.org):
"dev": "ts-node -r tsconfig-paths/register --files ./index.ts"
然后再执行就发现正常了:
ts-node-dev 使用路径别名
很多同学在开发 ts 项目的时候都是使用 nodemon 监听文件变更,然后使用 ts-node 执行代码。比如这样:
"dev": "nodemon -e ts --exec ts-node -r tsconfig-paths/register --files ./index.ts"
不过我们可以用一个更简单方便的工具来完成这个操作,那就是 ts-node-dev
,npm install ts-node-dev
安装好之后,我们就可以把上面这行改写成这样:
"dev": "tsnd -r tsconfig-paths/register --respawn ./index.ts"
官方文档中也提到:
So you just combine node-dev and ts-node options (see docs of those packages)
所以我们可以和 ts-node 一样用相同的方法解决路径别名问题。
而且除了看起来更简洁外,ts-node-dev 还有个好处就是 它会缓存 tsc 的编译过程,所以热更新速度比 nodemon + ts-node 快了很多。非常推荐大家试用一下。
关于路径别名的打包问题
还没完,我们现在只解决了开发时的问题。如果路径别名是 tsc 负责的话,那么打包时也会遇到这个问题。现在我们就来看一下打包时会怎样。
首先,在命令行执行 npx tsc
,然后去 ./dist/index.js
里看一下打包后的成果:
果然,tsc 没有把我们的路径别名转换成实际的相对路径。那么执行 node ./dist/index.js
时肯定会报错找不到 @/utils
,所以该如何解决这个问题呢?
一般来说有两种方法,先说菜一点的,我们刚才提到 tsconfig-paths/register
实际上是用于 node 的一个包,那么这里自然也可以这么用,在 package.json 里添加如下命令(记得安装 cross-env ):
"scripts": { "start": "cross-env TS_NODE_BASEURL=./dist node -r tsconfig-paths/register .\\dist\\index.js" }
然后再执行就可以了:
简单解释一下,这里用环境变量 TS_NODE_BASEURL
覆盖了 tsconfig.json 里的 baseurl
,来让查找路径别名指向的目录时可以找到正确的(编译后)的文件。后面就是正常的 node -r 引用对应包然后执行文件。
但是这种操作属于“运行时”处理,相信很多人都会觉得不舒服。所以更简单的方法是在编译代码时就一劳永逸的替换掉路径别名,就是下面这种方法(推荐):
我们可以安装 npm install tsc-alias
这个包,用于替换路径别名,用法也很简单,跟在 tsc 后面就可以了:
"scripts": { "build": "tsc && tsc-alias" }
现在执行 npm run build
之后就可以看到路径别名已经被替换成相对路径了:
当然,这个只是比较简单的解决方式之一,你也可以根据项目的实际情况来,比如使用 webpack 或者 vite,又或者你可以看下文末参考小节里的第一个链接来了解更多的解决方法。
tsc 为什么不会转换路径别名?
文章写到这里,我们已经解决了 ts 项目中的路径别名问题,但是相信很多人依旧有一个困惑:tsc 为什么不在编译的时候直接把路径别名替换掉?
在网上搜索相关的问题,能找到的大多都是如何解决这个问题,而对这个问题起因的解释却很少,我在搜索解决方法的时候也只看到有人说,这件事啊,说来话长了。
这不由得勾起了我的兴趣,在详细搜索之后,我找到了这篇讨论,却没想到居然是一篇横跨三年的故事。下面我们就大致了解一下这个问题的前世今生,如果你感兴趣的话也可以直接去看一下:Module path maps are not resolved in emitted code · Issue #10866
故事要从 2016 年 9 月说起,当时的 typescript 还在 2.1 开发版。有人在试用了 tsconfig 中的 paths 配置时也遭遇了相同的困惑,他便提交了一个 issue 来询问这个问题(事实上,最早的 issue 记录可以追溯到 2016 年 7 月份,不过问题相同,这里不再赘述 )。
没过两天,有两位 ts 贡献者先后回复到:你有没有同时在用一些 webpack 之类的打包工具?另一位则简单介绍了这么做是 有意为之的,paths 配置项的本意是想为一些第三方包提供特殊的引入渠道,使得 ts 可以获得更多类型信息,所以这不是个问题。
但是很显然,围观者的 👎 表明大家都不是很能接受这个观点。
在解释之后,贡献者给本 issue 添加了 Working as Intended
(按预期工作)标签并把问题标记为完成,同时 issue 作者也表明可以理解,并善意的给后来者介绍了自己已经通过 module-alias 解决了问题。
但是围观者显然没有被说服,大家开始陆续在这个 issue 下评论,一些人在介绍自己使用的解决办法,一些人在挺 ts,但是更多的人在吐槽这个设计太糟糕了,自己实在想不明白,希望有人能解释一下。
这个讨论足足持续了两年多,期间很多人 @ ts 贡献者并表示已经过去两年时间了,希望可以重新考虑下这个令人困惑的设计。甚至有人在 reddit 上发了帖子:Path maps cannot be resolved by tsc / Works as intended - what a joke。
时间来到 2019 年 2 月份,issue 最开始回复的一位贡献者站出来发出了一段长文进行了解释。大致意思就是,ts 在编译中修改路径这些字面值是不切实际的。ts 的工作重点应该是如何更好的和其他更擅长此类工作的模块相互协作,而不是为了支持更灵活的配置来自己实现这些功能,更别提可能会因此生成出“崩坏”的代码。
同一天,ts 组成员 DanielRosenwasser 再次明确表示了:哪怕将来可能会优化这里的开发体验,ts 也不会修改用户写下的路径。随后便锁定了这个问题。
至此,这个故事便画上了句号。
现在了解了事情的前因后果,我们就可以来总结一下这个问题的根本原因了。
tsc 不转换路径别名的根本原因
站在开发者的角度看,我们希望可以用更少的工具完成更多的事情,这无可厚非。但是站在 ts 本身的角度看,typescript 只是开发阶段使用的一个工具。它的核心目的也是优化开发体验,在开发时以强类型对代码进行约束。换句话说就是 ts 表示我是负责开发的,打包工作不是我的强项,我大概编译一下,去掉我的类型,剩下的打包工作交给更专业的工具来做。
所以我们可以把 tsc 编译后的代码看作是一种“中间产物”,typescript 期望用户可以使用更贴近自己需求的工具将这些代码二次编译为实际的生产代码。
现在我们回头看一下 ts hankbook 中关于路径别名的介绍:
可以看到,paths 配置项的本意是为了给一些模块提供特殊的引入渠道。注意,此处引入的包只是你开发时需要的,而不一定是生产时使用的。
例如,我们可以在开发时(使用 ts 开发时)引入一些开发模式的包,而在生产时则使用压缩过的 .min.js 包,更甚者我们可能会选择在生产时使用 cdn 来链接这些包而不是直接引入。
如果 tsc 自己会转换这些路径别名,那么后续的打包工具就无从得知这个包是需要特殊对待的。在这些打包工具眼里这些引入的第三方代码和你自己写的代码是地位相同的。
更危险的情况下,如果 paths 引入的这个包是 只能 用在开发环境下的,那么直接转换路径别名就相当于把开发包编译到代码包然后发布到生产环境里,从而导致程序崩溃的风险。这也是为什么 kitsonk 最后说:
Just because TypeScript can be configured to resolve modules in flexible ways doesn't not mean that TypeScript emits "broken code".
Daniel Rosenwasser 最后也提到,paths 配置项运行用户自己选择引入的是何种格式的包,例如 AMD 或是 systemjs。在开发阶段这无关紧要,而在打包阶段,用哪种格式的包应该交给更专业的打包工具来选择。
换一个角度看这个问题,使用者希望 ts 必须一丝不苟的把自己的 ts 代码翻译成 js 代码,之前执行什么样,转换成 js 代码后执行就得是什么样。而自己替换路径别名这件事就开了个口子,编译器居然把一个根本不可能变化的字面值给改了?
现在好了,如果你是 tsc,现在甲方又想让你保证代码的准确性,又因为懒想让你自己修改代码。你怎么干?你能怎么干?
当然是不干。毕竟霸哥也说过:
我宁愿什么都不做,也不愿犯错。
总结
本文介绍了怎么解决 ts 项目中的路径别名问题以及为什么会有这个问题。本来打算简单写一下的,没想到还是洋洋洒洒几千字出去了。
其实原因上面说了这么多,总结起来也就一句话:路径别名替换这件事我不干也有更专业的人来干,我要干了可能会出问题,出了问题你们还得骂我。
参考
- typescript - tsc - doesn't compile alias paths - Stack Overflow
- Module path maps are not resolved in emitted code · Issue #10866 · microsoft/TypeScript · GitHub
- Path maps cannot be resolved by tsc / Works as intended - what a joke : typescript (reddit.com)
- TypeScript: Documentation - Module Resolution (typescriptlang.org)
- 解决typescript 在 node.js 下使用别名(paths)无效的问题
到此这篇关于typescript路径别名问题的文章就介绍到这了,更多相关typescript路径别名问题内容请搜索阿兔在线工具以前的文章或继续浏览下面的相关文章希望大家以后多多支持阿兔在线工具!