前一阵子学习了一点 TypeScript 并用 Vue + Vite 做了一个简单的 Demo,体验了一下 TS 的快感。由此想尝试下使用 TS 开发一下 Node.js 应用,由于前一段时间一直用 Vue.js 便很顺手的用上了 alias 和 import。 没想到事情并没有那么简单……

TL;DR: 请直接看最后一部分和我发布的 repo

问题起因

从网上搜索和参考官方项目使用 TypeScript 开发 Node.js 应用的流程基本上是这样的:

对于开发调试: 使用 nodemon(检测文件变动) + ts-node 自动编译

对于构建生产: 使用 tsc 进行编译

但是,当我们使用 alias(即别名) 时,我们自然的在 tsconfig.json 上配置好 path、alias,却发现这样运行是有问题的:由于 TypeScript 在编译过程中并不会解析与替换我们设置的 alias (即别名),而同时编译后的 js 文件也自然不会被 node 解析,所以我们就会遇到对于 alias 路径的报错。 而这部分工作在 Web 开发中是由 Vite 或 Webpack 这类工具替我们处理的(当然,在这里你也可以引入它们,但是……)。

思路分歧:安能两全?

所以解决这个问题的思路就是找一些相关的 package 以及使用一些 hack 的手段来达成目的——解析 alias 路径。似乎这是很简单的——你可以很容易搜到一些这类的 package 比如:module-aliastsconfig-pathstsc-alias。但是当你想把这一些与 esm 结合时,却又出现了问题。

首先,module-alias 对 esm 并未支持,即使你想调整编译结果为 commonjs ,如果配合 ts-node 使用,仍然会出现一堆问题。

其次是 tsconfig-path,如果我们使用 ts-node 运行一个 esm 文件,我们必须使用 node --loader ts-node/esm src/index.ts,而使用 tsconfig-path 运行时,你则需要 ts-node -r tsconfig-paths/register src/index.ts,那么我们怎么把两者结合呢? node --loader ts-node/esm -r tsconfig-paths/register src/index.ts? 这在我的实际测试中似乎并不管用……最致命的是,他是运行于 运行时(run-time) 的,这意味着它并不会影响你的编译结果,仅仅在你调试的时候生效。

最后是 tsc-alias ,他非常好用,但是与 tsconfig-path 相反,他仅会处理编译后的结果,这也意味着他没有办法很好的与 ts-nodenodemon 直接适用。

另外,我还发现 vite 搞了 vite-node 这个东西,配合 vite.config.ts 在运行时似乎能起到不错的效果,但是目前仍处在初级阶段,有许多问题,所以并不推荐使用。并目前还不支持 build。

开发与生产分开处理

那没有什么很好的办法去解决这个问题了吗?答案是:确实。我没有发现很好的方法去解决这个问题。许多时候你确实要选择性的舍弃一些东西。 比如编译结果要选择为 commonjs 而非 esnext

回顾我们的开发的流程:调试与生产。这两个阶段如果分别适用不同的方法,似乎能达到不同的效果。

由于 tsc-alias 可以直接对 生产后的文件处理 alias ,因此我们完全可以把他设置在 scripts 的 build 中,即 tsc && tsc-alias。 其他部分则分别选择上面的与 nodemon 进行结合(当然不包括 module-alias)

于是我对以上的 package 测试后,发现以下三种方法,你也可以直接去 Github 上看这个 repo

他们的 package.json 基本都是一样的,为了方便我们使用 nodemon.json 来配置执行的指令,scripts 部分参考:

1
2
3
4
"scripts": {
"dev": "nodemon",
"build": "tsc && tsc-alias"
}

第一种:

开发使用:nodemon & ts-node & tsconfig-paths

构建使用:tsc & tsc-alias

这个的缺点就是你必须使用 commonjs 作为编译目标。

nodemon.json

1
2
3
4
5
6
{
"watch": ["src"],
"ext": "ts,json",
"ignore": ["src/**/*.spec.ts"],
"exec": "ts-node -r tsconfig-paths/register src/index.ts"
}

第二种:

开发使用:nodemon & tsc & tsc-alias
构建使用:tsc & tsc-alias

本质上就是使用 nodemon 监测文件变动,编译、解析 alias,并运行编译结果。

缺点是 tsc 要稍微慢点。

nodemon.json

1
2
3
4
5
6
{
"watch": ["src"],
"ext": "ts,json",
"ignore": ["src/**/*.spec.ts"],
"exec": "tsc;tsc-alias;node lib/index.js"
}

第三种:

开发使用:nodemon & vite-node

构建使用:tsc & tsc-alias

缺点就是前面说,vite-node 尚处于早期阶段,并且开启一个 vite 服务也会带来额外功耗。

nodemon.json

1
2
3
4
5
6
{
"watch": ["src"],
"ext": "ts,json",
"ignore": ["src/**/*.spec.ts"],
"exec": "yarn vite-node src/index.ts"
}

具体代码你可以去 Github 上自己 clone。

思考

以上是目前阶段的解决问题,但很明显这不是最终方案。其实问题的本质是 node.js 对于 esm 的支持程度;即由于 commonjs 的 require 和 import 的本质不同导致的。但是随着其发展应该是能够得到优化与解决的。