目录
- 前言
- 开始探索
- ts-node-dev 使用路径别名
- 关于路径别名的打包问题
- tsc 为什么不会转换路径别名?
- tsc 不转换路径别名的根本原因
- 总结
- 参考
前言
最近在 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 可以获得更多类型信息,所以这不是个问题。
但是很显然,围观者的