浅谈在 vscode 中调试 typescript 的小坑

2,254 阅读7分钟

我报名参加金石计划1期挑战——瓜分10万奖池,这是我的第1篇文章,点击查看活动详情

概述

使用 typescript 写 node ,本来以为在 vscode 上调试是件很容易的事情,结果还是踩了一些小坑。下文记录一下踩坑的过程和具体的解决方案。

下文整理使用两套方案在 vscode 中调试 typescript :

  1. 直接使用 tsc 编译 typescript 代码,然后使用 vscode 调试
  2. 使用 ts-node 库协助编译,然后再使用 vscode 调试

PS,vscode、tsc、node 版本如下:

  1. vscode:1.71.2
  2. tsc:4.8.3
  3. node:16.13.1

方案一:tsc

demo 演示项目:demo-ts-debugger,可以 clone 下来然后跟着下文步骤尝试

这个方案是 vscode 官网文档——Debugging TypeScript 中的方案,但官网文档中提到的内容感觉不太全,有些坑和细节没细讲( 也许是官网大大觉得这些都是常识吧 (〒︿〒)

完整效果

如下图 gif,演示使用 tsc 编译 typescript 代码,然后使用 vscode 调试的效果:

达到调试的预期效果:

  1. 直接定位到 ts 源文件,而不是跳去编译出来的 js 文件
    • 在代码中使用 debugger 或者直接在 vscode 中打断点都可以
  2. 支持源文件中使用别名(alias)

launch.json

{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "node",
      "request": "launch",
      "name": "Launch Program",
      "program": "${workspaceFolder}/src/index.ts",
      "preLaunchTask": "tsc: build - tsconfig.json",
      "outFiles": ["${workspaceFolder}/out/**/*.js"]
    }
  ]
}

launch.json 的详细字段说明可以参考:官网文档,部分字段中文含义可以参考:vscode调试学习

挑三个对这次调试任务来说比较重要的字段说明一下:

  1. program
    • “启动调试器时要运行的可执行文件或文件”
    • 我猜:这个应该是调试器加载需要调试代码的入口文件吧
    • 试了把这个字段的值改成 "${workspaceFolder}/out/index.js" 也可以正常运行,可能是有 map 吧,暂时没找到详细解说,希望有了解里面细节的朋友评论区告知一下 🤝
    • 另外,如果把这个字段删掉就有问题,没法正常调试
  2. preLaunchTask
    • “要在调试会话开始之前启动任务,请将此属性设置为 tasks.json 中指定的任务标签(在工作区的 .vscode 文件夹中)。或者,这可以设置 ${defaultBuildTask} 为使用您的默认构建任务”
    • 翻译一下就是在调试启动之前会先执行 tasks.json 指定的任务,至于指定的是哪个任务呢?就看 preLaunchTask 的值和 tasks.jsonlabel 的对应关系,如下图:
      • 这样说明,调试启动之前会先执行 tasks.jsonlabel 标签是 tsc: build - tsconfig.json 的任务
      • PS:这里有一个坑,如果一开始没有 task.json 文件的话,直接启动调试会弹出一个对话框,提示并帮助你生成 task.json 文件,如下图:
        • 但如果 vscode 使用了中文版(中文语言包)的话,自动生成的 task.json 文件中的 label 的值是 "tsc: 构建 - tsconfig.json" ,要手动换成 "tsc: build - tsconfig.json"
        • 反正记住 preLaunchTask 的值和 tasks.jsonlabel 的值向对应就准没错
  3. outFiles
    • 按照官网的说法:Mapping the output location
      • If generated (transpiled) JavaScript files do not live next to their source, you can help the VS Code debugger locate them by setting the outFiles attribute in the launch configuration. Whenever you set a breakpoint in the original source, VS Code tries to find the generated source by searching the files specified by glob patterns in outFiles.
    • 但好像去掉这个字段也是可以正常调试的,暂时不确定详细原因和用法

另外,配置中如:${workspaceFolder} 之类的特殊变量,具体可参考:官方文档 —— variables-reference,这里就不一一翻译里,文档里面还挺详细的 (~ ̄▽ ̄)~

tasks.json

{
  "version": "2.0.0",
  "tasks": [
    {
      "type": "typescript",
      "tsconfig": "tsconfig.json",
      "problemMatcher": [
        "$tsc"
      ],
      "group": "build",
      "label": "tsc: build - tsconfig.json"
    }
  ]
}

关于 vscode 中 tasks 的用法和介绍,可以参考:官方文档 —— tasks

关于 tasks.json 文件中各个字段的解析,可以参考:官方文档 —— tasks-appendix

上述这份 tasks.json 文件中的内容作用就是使用 tsc 命令行工具,将 typescript 代码编译成 javascript 代码

如何在源码中使用“别名”(alias)?

用过 webpack 别名 alias,应该不陌生,可以非常优雅地解决两个问题:

  1. 相对路径太长
  2. 使用相对路径 import 的文件,一旦移动了文件,要重新修改相对路径

比如,在文档: webpack 别名 alias 中的案例,定义了 src/utilities/ 路径的别名为:Utilities 后,就可以使用 import Utility from 'Utilities/utility'; 替换原本的相对路径的写法:import Utility from '../../utilities/utility'; ,灰常方便

tsc 并不支持这种做法,需要借助第三方库:module-alias 才能实现上述效果。

要想使用 module-alias 一共有三步:

  1. tsconfig.json 文件中添加 baseUrlpaths 两个字段
    • baseUrl 字段指代当前根目录
    • paths 里面则用于定义自己想定义的别名(alias)
      • 如上图,我们把 src 目录下的所有路径都起一个别名,叫: @
      • 这样,后续 demo 源码import sum from './utils/sum' 的写法就可以替换成 import sum from '@/utils/sum'
  2. 安装 module-alias 依赖,并且在 package.json 文件中定义 _moduleAliases 字段
    • 细心的朋友可能已经发现,为什么 _moduleAliases 中定义的别名跟 tsconfig.json 文件中的 paths 不一样?下面我们一起看看编译出来的 out 目录下的 js 文件内容就知道:
      • 可以看到 import sum from '@/utils/sum' 编译出来的内容是 var sum_1 = require("@/utils/sum")
        • 即:在编译阶段,tsc 并没有因为多了 module-alias 就把 @ 这个自定义别名“翻译”出来。实际上我猜 module-alias 应该是在 js 运行时才完成的这步“翻译”操作
  3. 在项目的入口文件中引入 import 'module-alias/register';

失败记录

一开始查找 tsc 文档时,发现了这个编译选项:--paths (在 compiler-options 可以查看所有 tsc 的编译选项)

一开始以为可以实现预期的别名效果,但实际上并不行,会报一下错误:

error TS6064: Option 'paths' can only be specified in 'tsconfig.json' file or set to 'null' on command line.

暂时没搞懂,就没走这条路了,换成了上述成功的道路

方案二:ts-node

demo 演示项目:demo-father-ts-debugger,可以 clone 下来然后跟着下文步骤尝试

这个方案是使用 ts-node 库协助编译,然后再使用 vscode 调试

另外,使用了 Umifather 作为项目的脚手架

完整效果

如下图 gif,演示使用 ts-node 库协助编译,然后再使用 vscode 调试

达到调试的预期效果:(跟上面 tsc 的方案的预期效果一致)

  1. 直接定位到 ts 源文件,而不是跳去编译出来的 js 文件
    • 在代码中使用 debugger 或者直接在 vscode 中打断点都可以
  2. 支持源文件中使用别名(alias)

安装 ts-node 和 tsconfig-paths

这个方案依赖 ts-nodetsconfig-paths 这两个第三方库

  1. ts-node,用来将 ts 编译成 js
  2. tsconfig-paths,用来实现上述别名(alias)的效果

launch.json

launch.json 文件内容字段就不赘述了,详细可以参考上述 “方案一:tsc” 小节的内容

这份配置基本是直接照抄: tsconfig-paths 中的使用文档

对于这个方案来说,最核心的应该就是上图中的 runtimeArgsargs 两个字段

我猜:(对又是合理性猜测)

  1. 上面 runtimeArgs 中的内容,是传给启动调试程序时的参数,表示使用 ts-node 编译以及使用 tsconfig-paths “翻译” 别名
  2. 上面 args 中的内容,表示从 "${workspaceFolder}/src/index.ts" 文件作为入口开始编译 ts 代码

为什么会有上述猜想?可以看看下图:

这个是启动调试程序后,vscode 中 “调试控制台” 打印出来的内容

/Users/nicholas/.nvm/versions/node/v16.13.1/bin/node -r /Users/nicholas/Nicholas/my-pro/demo/demo-father-ts-debugger/node_modules/ts-node/register -r /Users/nicholas/Nicholas/my-pro/demo/demo-father-ts-debugger/node_modules/tsconfig-paths/register /Users/nicholas/Nicholas/my-pro/demo/demo-father-ts-debugger/src/index.ts

很明显,是调用了一个 node 程序的命令,其他内容都作为命令的参数传了进去

如何在源码中使用“别名”(alias)?

这个方案使用别名(alias)的方法比较简单,只需要修改 tsconfig.json 文件即可

相对来说,这个方案简单一下,需要修改的地方也不多

One More Thing

按理来说,“方案一:tsc” 应该是可以扩展的,比如不用 tsc 编译代码之类的

比如,在 “方案二:ts-node” 中使用了 Umifather 作为项目的脚手架,那么,应该是可以修改“方案一:tsc”中的 tasks.json ,不用 tsc 任务,换成是 father 的命令行工具,执行编译任务,应该也是可以的,各位看官有兴趣可以尝试一下 (~ ̄▽ ̄)~