微技术

关注技术领域,分享资讯和经验。

使用VSCode调试nodejs

这篇文章是想系统的说一下VSCode的Node.js调试,大部分内容均来自于VSCode的官方文档:在VSCode中调试Node.js ,官方文档里有很多模棱两可的表述,这里则会更详细的讲解。

VSCode中有如下三种方式来触发对Node.js程序的调试。

  • 自动调试(auto attach,直接翻译应该叫“自动附加”)
  • Javacript调试终端
  • 启动配置(launch config)

自动调试(auto attach)

所谓自动调试,意思就是一旦打开这个功能,咱们在VSCode集成终端(命令行)中启动的Node.js程序会自动进入调试状态。为什么叫attach(附加)呢?因为调试功能本身也是一个独立的程序,它和目标程序连接上了,就叫attach,就像是调试程序依附着目标程序在运行,一个目标程序可以连接多个调试程序,它们可以运行在同一台机器上,也可以在不同的机器上。

要想打开此功能,先用mac:shift + command + p(windows:shift+control+p) 开启VSCode的命令调色板,然后在文本框中输入Toggle Auto Attach, 这时文本框下方的下拉列表会把这个唯一的命令筛选出来,如下图所示:

点击该命令或者直接按回车,会出现如下图的四个选项:

每个选项的含义,里面的文字已经解释的很清楚了。
第三个仅带标志,意思是当我们在命令行启动Node.js程序时带上—inspect参数,才会自动进行调试,例如:node —inspect xxx
第四个已禁用选项是选中状态,代表目前我们的状态是已禁用,这也是VSCode的默认状态。

点击上面三个选项的其中一个开启自动调试后,还需要重新打开VSCode的集成终端(命令行),大家可以想象一下,终端肯定是在其启动时读取自动调试的状态来决定是否要加载相关功能的,因此我们打开或者关闭自动调试后,都需要重新启动终端,这一步比较不显眼,容易忘记,大家注意一下。
VSCode怕我们忘记重启集成终端(命令行),当自动调试的状态改变后,集成终端的右上角会出现一个黄色叹号,来提醒你需要重新启动终端,但是这个功能目前还不是很靠谱,不一定每次都有提示。因此大家记得修改完状态后,重新启动一下终端就好了。

这里注意下,重启终端并不是点击终端的关闭按钮,而是点击删除按钮(垃圾桶)或者点击如下图所示的按钮,再点击终止终端

或者也可以新加一个终端,点击右上角的加号按钮。

当开启自动调试后,你在终端中启动node.js程序时,会显示Debugger attached,这时表示调试功能已经成功连接到了你的主程序。

当开启自动调试后,VSCode最下面的状态栏中会显示自动调试的状态自动附加:xxxxxx处可以是始终 智能 带标志。我们可以直接点击该文字来切换状态(该功能有时候只有当前VSCode的实例上生效,有时候所有VSCode实例都被切换,且重启VSCode也不会恢复成之前的状态)。当禁用该功能时,该状态不再显示。

我们也可以对自动调试进行一些个性化的配置,VSCode提供两种不同范围的配置:用户配置和工作空间配置,均可通过command+,直接打开,或者通过点击以下按钮:
macOS:Code -> 首选项(Preference)-> 设置(Settings)
Window:File -> 首选项(Preference) -> 设置(Settings)

打开后在最上方搜索框中输入:terminalOptions,结果如下图所示:

点击在settings.json中编辑,VSCode会自动列出terminalOptions的可设置项,这里我们选择skipFiles,VSCode会自动将<node_internals>/**,放到skipFiles数组的第一项。意思是告诉调试器直接step over node.js内部JS文件的调试。terminalOptions中可以设置的选项和启动配置(lanuch config)中的选项是一致的,后面会详细讲解这些选项。

如上面所看到的,自动调试有三种模式:始终、智能、仅带标志。这里还需要说明一下智能选项,当设置成智能选项时,VSCode会尽可能的只对开发人员的代码进行调试,而忽略掉build tools,因为build tools的代码一般是我们不太感兴趣的,毕竟大部分时候我们都专注在本次业务代码上。
VSCode主要是通过一系列的glob模式来识别出需要和不需要调试的代码,这些glob需要放到debug.javascript.autoAttachSmartPattern配置项下,它的默认配置如下:

  1. {
  2. "debug.javascript.autoAttachSmartPattern": [
  3. "!**/node_modules/**", // 不调试node_modules下的代码
  4. "**/$KNOWN_TOOLS$/**" // 调试一些常用的工具库
  5. ]
  6. }
复制

前面带叹号”!”表示不调试后面的通配符匹配的代码。后面的glob会覆盖前面的,所以上面的配置的意思时,不调试node_modules下的代码,但是需要调试一些常用的工具库。$KNOWN_TOOLS$会被替换成ts-node,mocha,ava等。
这里需要注意一下的是,每次修改完这些配置项,都需要重新启动集成终端,否则有可能失去调试功能。

以上便是自动调试部分的讲解。自动调试的三种模式,大家可以根据自身的情况进行选择。

Javascript调试终端

这个和自动调试非常类似,我们要做的就是把自动调试里集成终端中运行的代码放到调试终端里运行就可以开启调试模式了。
那么怎么打开Javascript调试终端呢?
方式一:
打开集成终端,mac:command+j,windows:control+j,点击右上角的下拉箭头,点击Javascript Debug Terminal,这时便开启了调试终端。具体如下图所示:

方式二:
点击VSCode左边面板上的调试按钮,如下图所示:

可以看到按钮Javascript Debug Terminal,点击它开启调试终端。
方式三:
鼠标放到package.json里的scripts上,然后点击“调试脚本”,VSCode会自动打开Javascript调试终端并运行scripts中的代码

另外还有一种方式是要打开命令调色板,这种方式咱们就不用看了,需要找到对应的命令再运行,太麻烦了。

同样,我们也可以对调试终端进行配置,其配置方式和自动调试一致,都是需要对debug.javascript.terminalOptions进行配置,并且启动配置launch.json中的配置在这里都适用。启动配置我们后面讲述。

Javascript调试终端就讲到这里了。

启动配置launch.json

启动配置是VSCode传统的调试方式。如果你的VSCode需要针对不同项目采用不同的调试配置时,建议采用这种方式。这种方式也是最正式的方式,如果前面两种方式是游击战,那启动配置方式就是正规军团的阵地战,如果你有一个团队,并希望规范他们的调试行为,则可以对launch.json进行完善的配置,并让整个团队使用同一份配置。在这段我们会更详细的讲解这些配置,以便我们应对更复杂的调试场景。
在讲解启动配置之前,我们先来看一下VSCode的调试面板,之所以现在才开始讲这个调试面板,是因为这个调试面板实在是有些乱,我们新人看到他会有点懵懵的感觉。调试是VSCode的核心功能之一,他内置支持对Node.js运行时的调试,同时通过扩展的形式也能支持其他若干种语言,所以稍微有些乱也是完全可以理解的。
咱们看一下这个运行和调试视图,首次进入该视图没有进行过配置和启动过程序时,会如下图所示:

如果你当前没有已经打开的可调试文件,在最上方会出现打开文件的按钮,让你选择一个文件进行调试。如果当前有打开的可调试文件,该按钮便不会出现,这时如果你点击运行和调试,会对当前打开并处于显示状态的文件进行调试,首先弹出一个下拉框,让你选择调试器,如下图所示:

我们当前打开显示的是一个Javascript文件,可以看到该下拉框中的选项均是和Javascript相关的,这里我们选择Node.js,如果我们已经提前设置了断点,便可以一步一步的调试了。
我们现在再看一下调试界面:

上面的那个下拉框显示,我们当前处在Run Current File状态,Run Current File状态实际上是VSCode的运行程序的方式之一,而它下方的Node.js,实际上是调试器的名字,也就是说,我们现在处在Node.js调试器下的Run Current File启动方式。
我们也可以对Run Current File进行更详细的配置,点击Node.js调试器,会出现选择启动配置的下拉框,如下图:

我们选择启动配置选项后面的齿轮按钮,会跳转到launch.json文件,VSCode为我们自动添加了Run Current File的配置launch.json文件在当前工作目录的.vscode文件夹下,也就是说这个launch.json文件对这整个文件夹的工作区生效。其实实际使用时,我们对单文件进行调试的情况应该很少,因此我们只是想通过对它的讲解,让大家初步了解一下VSCode调试的概念:调试器启动方式launch.json

现在我们删除掉.vscode下面的lauch.json文件,并停止调试,再进入运行和调试,便可以看到,视图又恢复到了刚开始的样子。这里再将这个视图贴出来:

其实这个视图分几部分,它给用户提供了几个选择(如果你用的是英文版,烦请翻译对照):

  • 运行和调试
  • 创建launch.json文件
  • 显示所有的自动调试配置
  • 启动Javascript Debug Terminal(Javascript调试终端)
  • Debug URL

运行和调试:
刚才咱们已经细说了,它就是用来调试当前打开并正在显示的文件的。
创建launch.json文件:
这个也很明确,让我们创建启动配置,点击它,会弹出选择调试器的下拉框。调试器+启动配置(启动方式)便可以开始一次调试了。
显示所有的自动调试配置:
点击之后的下拉框中会显示Node.js调试器下的启动配置,会有Javascript Debug Terminal”、Run Current File,如果你有package.json,并且设置了scripts项,点击Node.js调试器后,package.json的scripts项中所有的script命令都会被列出来,点击可以进行调试。
启动Javascript Debug Terminal(Javascript调试终端):
这个比较好理解,点击之后,集成终端右侧的列表中会多出该调试终端,在里面启动的Node.js进程都会进入调试状态,这个咱门已经讲过了。
Debug URL:
该按钮实际上对应的是该命令:Debug:Open Link,mac:shift+command+p windows:shift+control+p,开启命令调色板,然后输入该命令和点击Debug URL按钮的效果是一致的。会弹出一个文本框让输入要调试页面的URL。浏览器应用的调试不在我们这次要讨论的Node.js调试范围内,就不细说了。

可以看到虽然VSCode的运行和调试视图里按钮不少,但是大部分都是非常简单,我们只需要明白这几个概念:调试器、启动方式、launch.json,所有按钮功能都是围绕这几个概念设计的。

现在我们开始真正讨论一下启动配置launch.json。

其实其他几种方式都是在力求对调试功能傻瓜化,而要想对调试功能进行精细的控制,那就需要用好launch.json。
launch.json文件位于你工作空间的.vscode文件夹下,它有一大堆的属性可以配置,这里我们只列出Node.js相关的部分。这部分才是VSCode调试最复杂的部分,文档说得很模糊,而各种配置的组合很难记住,还好VSCode为我们提供了配置工具,打开launch.json,右下角有一个添加配置的按钮,点击后会打开各种常见场景配置的组合列表,由于VSCode的配置较多,为了方便开发人员,VSCode便把各种常用场景的配置都提前写好了,大家选一个和自己当前场景匹配的场景,然后再在这个基础上修改一下就可以了。如下图所示:

这里我们简单解释一下各个常用场景的意义,具体的还需要各位自己体验一下,体验一下可能比看这个文章更快,只是你体验到不理解的地方时,来这里查一下就可以了:

  • Node: Launch Program
    这种方式下,咱们通过program选项指定要执行的Javascript文件,执行的命令是node
  • Node: Launch via npm
    这种方式下,通过”runtimeArgs”传入要执行的npm script的名称,默认名称是debug
  • Node: Attach
    这种方式表示VSCode不会去启动目标程序,而是直接将调试功能附加到一个已经开启了调试服务的进程上去,要理解attach,可以看后续即将出炉的文章:Node.js的调试方式和其原理。
  • Node: Attach to Remote Program
    Attach的原理一致,只是要调试的程序不在本机,需要通过网络连接到远程的目标程序,对目标程序进行调试。
  • Node: Attach by Process ID
    填上目标Node.js程序的进程号,VSCode便可以对其进行调试
  • Node: Nodemon Setup
    调试通过Nodemon启动的程序,runtimeExecutable选项的值是nodemon, program是要执行的Nodejs程序。你需要在全局安装一个Nodemon,另外注意,我们终止调试并不会终止Nodemon程序,需要在集成终端里control+c来终止Nodemon程序。下面的示例配置由VSCode自动生成:
    1. {
    2. "console": "integratedTerminal",
    3. "internalConsoleOptions": "neverOpen",
    4. "name": "nodemon",
    5. "program": "${workspaceFolder}/app.js",
    6. "request": "launch",
    7. "restart": true,
    8. "runtimeExecutable": "nodemon",
    9. "skipFiles": [
    10. "<node_internals>/**"
    11. ],
    12. "type": "node"
    13. }
    复制
    另外还有其他几种配置类型:Mocha TestYeoman generatorGulp taskElectron Main,它们都大同小异,只需要确保相关的库成功安装就可以了。

下面再来说一下launch.json中具体的配置选项,这里我们把每一个选项进行详尽的解释,供大家参考。
这些配置项的名字有一部分比较古怪,让人很难仅通过名字来猜到它的意义,且有些配置之间是相互冲突的,不应该同时存在,这里我们不准备细讲配置之间的组合,因为VSCode的launch.json里智能感知会帮我们判断,没有必要话心思去记这个。
launch.json的configurations属性是一个数组,所以可以配置多种启动配置,这些启动配置最终会列在下拉框中,供你启动程序时选择。如下图所示:

我先来说几个核心配置:

  • type
    type项共有三个可能选项:node node-terminal pwa-node
    • node
      代表要对Node.js程序进行调试
    • node-terminal
      这个实际上就是上面说的Javascript调试终端,通过调试终端启动的Node.js程序都可以被调试。当用该选项时,点击开始调试,一个调试终端会被打开,command选项中的命令会被在调试终端中输入并运行,从而开始调试
    • pwa-node
      新版VSCode(近几年之内的版本)用的Javascript调试器刚开始时叫“pwa debugger”,是一个全新调试器,功能强大。之所以有pwa前缀也是因为它支持对PWA的调试。为了让开发人员尝鲜,VSCode允许通过设置pwa-node来使用该新功能的调试器,它指向的是这个调试器的nightly版本。现如今这款新的调试器早已取代了老的调试功能,成了内置的调试器,因此pwa-node也改成了node,不过pwa-node在VSCode中仍然生效,主要是为了向后兼容,它和“node”是一样的,算是它的别名。某些情况下VSCode自动生成的配置中还会有pwa-node出现,只是因为还没有改干净。
  • request
    共有两个可能的选项:launch attach
    launch代表VSCode要做两件事:启动程序并连接上(attach)它对其进行调试。
    attach代表VSCode只是启动调试功能,并不会启动目标程序,这时需要告诉的VSCode的选项一般都是端口号、进程ID之类的,用于连接目标程序。
  • runtimeExecutable
    要运行的可执行程序名称(只要是通过PATH环境变量能找到的程序都可以),不包括参数部分,一般都是node npm nodemon等,如果typenode-terminal,则不需要配置该选项,咱门也不用太担心配置冲突,VSCode会给我们提示。
  • runtimeArgs
    一个字符串数组,这个选项和runtimeExecutable是一起的,用于作为runtimeExecutable的参数,例如如果runtimeExecutablenpm,那runtimeArgs则可能是[“run”, “debug”],实际等于npm run debug
  • name
    该配置组合的名字,当我们要按照某个配置组合运行时,通过名字选择来告诉VSCode要按照哪个配置项运行,如下图所示:
  • program
    字符串,想调试程序的绝对路径。VSCode中可以用${workspaceFolder}表示当前工作空间的路径,因此如果代码文件在工作空间内,则可以像下面这样写:${workspaceFolder}/app.js,这样写意思是对工作空间内的app.js文件进行调试。
  • command
    typenode-terminal时,command选项用来设置要运行的命令,例如“command”: “npm run debug”,这种方式的可读性要比runtimeExecutable强。

以上就是一些核心选项。

下面是一些可以让你进行精细配置的选项:

  • outFiles
    一个glob模式的数组,用于指定告诉VSCode哪些是生成的代码文件,VSCode会根据这些文件找到sourcemap文件,从而找到对应的源文件,以便于在源文件上进行调试。
    例子:
    1. {
    2. "version": "0.2.0",
    3. "configurations": [
    4. {
    5. "name": "Launch TypeScript",
    6. "type": "node",
    7. "request": "launch",
    8. "program": "app.ts",
    9. "outFiles": ["${workspaceFolder}/bin/**/*.js"]
    10. }
    11. ]
    12. }
    复制
  • resolveSourceMapLocations
    一个glob模式的数组。
    默认情况下,仅仅outFiles指定的目录下的sourcemap会被解析。这时为了防止某些依赖模块会干扰我们调试,某些依赖模块的sourcemap会错误的指向到我们自己的源代码,从而导致调试混乱,而一般情况下依赖的代码是不需要调试的。我们可以通过resolveSourceMapLocations来改变这个默认行为。如果resolveSourceMapLocations设置成null,所有的sourcemap文件都会被解析,也可以设置成如下代码所示的样子,除了out目录下的,node_modules/some-dependency下的sourcemap也会被解析。
  1. "resolveSourceMapLocations": [
  2. "out/**/*.js",
  3. "node_modules/some-dependency/**/*.js",
  4. ]
复制
  • timeout
    当我们用nodemon来让我们的程序修改后自动重启时,如果这时代码有错误重启失败,VSCode会持续的尝试连接到Node.js,默认是持续10秒,我们可以通过timeout来修改该持续时间,单位是毫秒。
  • stopOnEntry
    程序启动后立即断点中断,这个应该很好理解了,程序会停在第一句话,等着你调试。但是该选项并不适合所有的场景,当type为node-terminal时,该选项无效,VSCode会给出提示,告诉你没有该选项。而且有这个选项时,也不一定能生效,目前看当我们设置了通过program命令来启动时是生效的,其他的通过npm启动的程序是不生效的。
  • localRoot
    本地VSCode工作空间的根目录,一般就是你VSCode打开程序的根目录。当你用VSCode进行远程开发调试时,这个选项有用,这时需要指定远程程序的根目录(remoteRoot)是什么,本地程序的根目录是什么。
  • remoteRoot
    远程VSCode工作空间的根目录,已经和上面的localRoot一起解释了。
  • smartStep
    布尔值,设置为true后,调试时会尝试自动step over没有被sourcemap覆盖的生成的代码。
  • skipFiles
    一个glob数组,指定的文件在单步调试的时候会被调试器直接运行过去,不会在里面停留,当你输入skipFiles时,VSCode会自动给你加上/**,代表当程序在单步执行Node.js的内部Javascript代码时,不会停留。后面在跳过不感兴趣的代码一节会详细讲skipFiles
  • trace
    布尔值,默认为false,允许诊断性的日志输出

  • args
    参数数组。不同于runtimeArgsargsprogram配套使用,用于给program指定的程序传参数。

  • cwd
    程序启动时,使用这个选项作为当前工作目录,一般设置成${workspaceFolder}
  • runtimeVersion
    版本字符串。如果本机是用nvm来做Node.js的版本管理的话,该选项可以用来设置要使用的Node.js版本,例如runtimeVersion:14,本机内的14大版本里的最新版会被使用。如果是用的nvs,还可以指定:特殊版本的Node.js(基于chackracore引擎的Node.js)、版本、cpu架构,例如runtimeVersion:chackracore/8.9.4/x64,
  • env
    json键值对的形式。用来设置环境变量,例如:{env: “production”}
  • envFile
    文件路径字符串。用来设置环境变量文件的路径,例如${workspaceFolder}/.env,文件中的环境变量是这样的格式:

    1. USER=doe
    2. PASSWORD=abc123
    3. empty=
    4. # new lines expanded in quoted strings:
    5. lines="foo\nbar"
    复制
  • console
    字符串。默认是internalConsole,用于指定调试控制台。由于internalConsole不能进行用户输入,如果要调试需要用户输入的程序,则不能满足需求,console选项可以让开发人员改用其他终端,可选值有:integratedTerminal externalTerminalintegratedTerminal就是VSCode的集成终端,externalTerminal可以在VSCode的用户设置(mac:command+, window:control+,)中指定,windows:terminal.external.windowsExec macOS:terminal.external.osxExec linux:terminal.external.linuxExec,VSCode已经给了默认值
  • outputCapture
    可选值:console或者std。默认情况下,调试控制台只会显示console.** API打印出来的信息,并不会把标准输出和标准错误输出的内容显示出来,因为调试控制台实际上是调试程序的一部分,而调试程序和目标程序是分开的,调试控制台并不是一个输入输出终端,目标程序发送给标准输入输出的数据自然不会跑到控制台。而outputCapture设置成std时,则可以捕获住标准输入输出std的信息,这样的话,有些程序没有用console.*的API而是用的process.stdout process.stderr*,也可以在调试控制台显示。
  • restart
    布尔值或者json格式:{“delay”: 1000, “maxAttempts”: 10}。调试程序和目标程序是分开运行的,目标程序损坏或者其他任何有可能的原因都会导致两个程序之间的连接中断,restart:true则告诉调试程序,连接中断后需要不断尝试连接目标程序,间隔是每隔1秒尝试一次,一直尝试知道连接成功。也可以以键值对的形式delay自定义间隔时间,单位为毫秒,并且设置尝试次数maxAttempts
  • protocol
    该选项在新的VSCode版本中已经被废弃了,但是文档还没有更新。之前可以设置成legacy或者inspectorlegacy作为V8老的调试协议已经被废弃了,所以现在只有inspector,这样的话protocol选项也就不需要了。
  • port
    TCP/IP端口号,number类型。默认值是9229。Nodejs8以后的调试本质上是都是基于V8-inspector,它默认会监听TCP的9229端口,通过websocket协议和调试客户端通信。也可以指定监听其他端口,如果指定了其他端口,那我们这里的port选项也需要设置成一致的端口。只有选项requestattach的模式下才有可能需要指定端口,在launch模式下程序由VSCode启动,由于都是通过VSCode启动,VSCode自己就搞定了,也就没有人为指定端口的必要了。
  • address
    TCP/IP的地址,字符串。和port同理,因为是通过websocket连接,而websocket基于TCP/IP,因此肯定需要一个IP地址,这个地址可以是IP地址,也可以是localhost
  • processId
    目标程序的进程Id,或者${command:PickProcess},字符串。当目标Node.js程序没有以调试模式启动时,这时要想对目标程序进行调试,可以向目标程序发送一个信号:USR1,目标Node.js程序接收到该信号后,便会启动调试模式,这时VSCode就可以通过websocket对目标程序进行调试了,由于这种方式会使用默认的端口号,不能人为指定,所以如果设置了processId,就不要再设置port选项了。然而这里说的这个USR1信号只适用于与POSIX兼容的操作系统:Linux、macOS,不适用于Windows,如果是Windows,则是通过process._debugProcess(processId)来通知目标进程启动调试服务的。由于要获取进程Id还挺麻烦的,VScode提供了UI供我们可视化的选择当前运行的Node.js进程,只要我们这样设置:processId:”${command:PickProcess}”,当开始使用该配置运行时,VSCode会弹出下拉框,让你选择一个Node.js进程进行调试。
  • continueOnAttach
    布尔值。当选项requestattach时有效。如果目标Node.js程序是断点暂停状态,当VSCode调试器连接上时,是否让程序恢复运行。
  • sourcemaps
    布尔值,true表示会通过sourcemap进行源码调试,false表示即使有sourcemap也不会通过sourcemap来对源码进行调试。

好了,就上面这些了。其实launch.json还有其他一些配置,大部分情况下,我们通过编辑器的智能提示都可以找到这些配置选项,当把鼠标放到选项上时,还会有用途解释。如果大家对某个选项不理解,可以向我们提问,我们将给你一个满意的答案。

关于停止调试

如果是attach模式或者我们是从命令行中启动的调试程序(也可以认为是attach模式),我们调试面板上的停止按钮变成了断开调试连接按钮,该按钮不会停止目标程序。如下图所示:

如果是lauch模式,点击停止按钮会做一下事情:

  • 第一次点击停止按钮,目标Node.js程序会收到SIGINT的信号。然后目标程序收到信号可能会做一些回收资源之类操作,如果这些操作里的代码没有断点,程序最终会终止。
  • 如果关闭功能相关的代码有断点或者相关代码抛出异常,无法正常关闭,则整个调试功能不会结束,需要再次点击“停止”按钮来强制目标程序和它的子进程退出,这次的信号是SIGKILL。
    所以如果按一次“停止”按钮没有停止调试的话,则再按一次便可以强制停止了。
    另外,在Windows操作系统上,只要按一下停止,就会强制退出调试,目标程序也会停止。

launch模式下的停止按钮如下图:

关于Source Map

当我们在运行经过Babel或者Typescript生成的代码时,如果要对代码进行调试,需要有sourcemap文件,这样才能在源码上进行调试。VSCode对sourcemap有很好的支持。如果生成的代码没有对应的sourcemap,这时如果我在源代码上打断点,当我们运行调试时,该断点会变成一个灰色的空心圈,表示该断点无效,因此sourcemap是我们对这种转译代码调试的关键。
上面也已经说过了,launch.json中,通过sourcemaps选项可以对是否用sourcemap进行调试进行开关。

处理Sourcemap相关问题的一些秘诀

当用sourcemap进行调试时,很常见的一个问题是,你设置了一个断点,它是灰色的,灰色一般代表着不可用。如果你把鼠标放在上面,你会看到提示信息:“Breakpoint ignored because generated code not found (source map problem?)”,意思是说“无法找到和源码的断点位置对应的生成代码”,这个有很多原因可以导致这个问题的发生。
首先我们快速解释一下Javascript调试器是怎么处理sourcemap的。
当你在app.ts文件中设置断点时,调试器需要找到app.js(生成的文件)的文件路径,app.js是真正在Node.js中运行的代码。但是并没有一个方法可以直接从.ts文件找到.js文件。相反,调试器通过launch.json中的“outFiles”配置选项来找到所有的.js文件,然后找到sourcemap(有可能是内联在.js文件中,也有可能是独立的文件),sourcemap文件中包含了源文件.ts文件的路径。
当你编译app.ts文件成app.js文件时,如果开启了sourcemap开关,那会生成一个app.js.map文件,或者sourcemap以base64编码的形式作为注释内联在app.js文件的底部。要想通过app.js找到app.ts的文件位置的话,调试器会查看sourcemap中的两个属性:“sources”和“sourceRoot”,“sourceRoot”是所有源文件的根路径,可以为空,“sources”是所有源文件的路径,通过这两个属性可以拿到源文件绝对或者相对路径的数组,相对路径是相对于sourcemap文件的路径。
最终调试器找到app.ts文件,这时app.ts和app.js便建立了对应的联系。app.ts找到了对应的app.js,当给app.ts加断点时,通过sourcemap中的行列map信息,调试器能够知道该断点在app.js中的位置,当app.js运行到断点位置时,调试器的UI会在app.ts对应的位置停住。但是如果调试没有找到app.ts文件,联系便不能建立,断点会变成灰色。

当断点变成灰色时,可以尝试一下下面几个方法来解决这个问题:

  • 当调试时,mac:shift+command+p,windows:shift+control+p唤起命令调色板,输入Debug:Diagnose Breakpoint Problems,然后回车运行,这个命令会唤起一个工具,这个工具能提供一些线索,帮你解决问题。
  • 一定要确保sourcemap已经生成。
  • 看一下sourcemap中的“sourceRoot”和“sources”中的路径是否正确,是否指向了正确的源文件。
  • 如果是通过命令行打开VSCode,看一下文件夹字母的大小写是否正确。
  • 尝试在断点处用debugger语句,如果debugger能够起作用,断点却不行,那就说明是VSCode的问题了,可以在Github中反馈。

远程调试

VSCode具有远程调试的能力。只要目标Node.js程序开启了调试,并且指定端口TCP/IP连接通畅,不管和VSCode是否在同一个机器上,我们都可以用VSCode对它进行调试。
launch.json中加上如下配置,便可以对位于192.168.148.2上的开启了9229端口的Node.js程序进行调试:

  1. {
  2. "type": "node",
  3. "request": "attach",
  4. "name": "Attach to remote",
  5. "address": "192.168.148.2", // <- remote address here
  6. "port": 9229
  7. }
复制

默认情况下,远程代码会在VSCode只读的编辑器中显示。你可以单步调试,但是不能修改代码。如果你想修改代码,可以在launch.json中添加localRootremoteRoot选项,分别代表两边代码的根目录。这样便可以修改代码了。举例如下:

  1. {
  2. "type": "node",
  3. "request": "attach",
  4. "name": "Attach to remote",
  5. "address": "TCP/IP address of process to be debugged",
  6. "port": 9229,
  7. "localRoot": "${workspaceFolder}",
  8. "remoteRoot": "C:\\Users\\username\\project\\server"
  9. }
复制

访问已载入的代码

有些代码并不在我们的工作空间中,如果我们想访问查看这部分代码,可以通过“已载入的脚本”视图进行查看,该视图的位置如下图所示:

这部分没什么好说的,大家打开VSCode自己看一下就清楚了,比文字描述要形象多了。

代码修改后,重新启动调试会话

launch.json中的restart配置选项用来控制调试会话中断后是否要自动重启会话。当我们使用nodemon重启Node.js时,restart功能就显得很有用了。nodemon的特点是在开发时业务代码被开发人修改后,nodemon会自动重新启动Node.js,以便重新加载新的代码。重启必然要停止之前的进程,这就会导致之前的调试会话中断,如果不重新建立会话,则需要手动去启动调试,“restart”设置为“true”后,不需要手动启动调试,调试会话会自动重新建立。这里再次贴一下VSCode自动添加的Nodemon启动配置:

  1. {
  2. "console": "integratedTerminal",
  3. "internalConsoleOptions": "neverOpen",
  4. "name": "nodemon",
  5. "program": "${workspaceFolder}/app.js",
  6. "request": "launch",
  7. "restart": true,
  8. "runtimeExecutable": "nodemon",
  9. "skipFiles": [
  10. "<node_internals>/**"
  11. ],
  12. "type": "node"
  13. }
复制

另外还是像上面说的,当使用nodemon时,点击停止调试按钮只会让调试会话停止,但是nodemon启动的程序还会继续运行,如果要停止nodemon,必须从命令行按control+c来kill掉它。
如果程序代码有语法错误,nodemon不能成功启动Node.js,VSCode会持续尝试连接到Node.js,最长持续10秒。我也可以通过timeout配置选项来调整该时间,timeout的单位是毫秒。

重启栈帧(frame)

VSCode调试器支持重启栈帧执行功能。当我们调试时发现了代码的一个问题,想修改一下入参后重新运行一下这段代码,重新启动整个程序再运行到当前位置可能比较费时,重启栈帧功能可以让你在改变当前函数的入参后只重新运行当前函数,演示如下:

鼠标放到CALL STACK的函数上,右键点击Restart Frame,中文版点击重启框架(这个翻译确实不是很准确)。
当鼠标放到每个调用堆栈的后面时,重启框架的按钮也会出现。
重启栈帧不会回滚对函数外面变量的修改,所以重启可能不一定会完全按照预想的去运行,函数外面的变量值可能会因为重复运行函数而变得错误。

断点

条件断点

条件断点意思就是,只有达到某个条件时,才会触发断点。在代码行号前右键,会出来几个选项添加断点添加条件断点添加记录点运行到行(当在调试状态时,此选项出现)。点击添加条件断点,会弹出如下下拉和文本框:

下拉中有表达式命中次数日志消息。每一项在文本框中都会有解释,解释都比较明了,这里把它们的解释列出来:

  • 表达式
    在表达式结果为真时中断。按Enter键确认,Esc键取消。
  • 命中次数
    在命中次数条件满足时中断。按Enter键确认,Esc键取消。
  • 日志消息
    断点命中时记录的消息。{}内的表达式将被替换。按Enter键确认,Esc键取消。

命中次数中可以设置一个数值比较类的条件或者是一个整型数字或者%n,数值比较支持的比较操作符有:<、 <=、 ==、 >、 >=,这些都比较好理解。%n不太好理解,实际上它表示:time%n === 0,这样大家应该好理解了,命中次数除以n的余数必须是0,%2就表示第2次第4次第6次以此类推一个隔着一个的都可以命中,命中即断点生效。

日志消息也可以通过在代码行号前右键,然后点击添加记录点来触发。它支持插值功能,在{}中输入表达式,日志中表达式会被替换成当前的值,就像这样:myVariable.property is {myVariable.property},消息会在调试控制台打印出来。

断点验证

考虑到性能,Node.js并不会在启动时对所有的函数进行解析处理,它只处理启动时必要的函数,以便加快启动速度,不必要的函数被延后处理了。
这个特性叫做懒加载,英文lazy load。但是由于这些函数没有得到解析,那VSCode设置在这些函数内的断点也没有办法被Node.js确认为一个有效的断点。
因此VSCode在启动调试时自动地向Node.js传入了—nolazy参数,这样Node.js便不会进行延后处理,不过也可能让Node.js的启动变慢了,具体慢多少和我们的代码量有关。

我们可以强行通过launch.json的runtimeArgs配置选项传入—lazy参数,让Node.js进行懒加载启动。这样做之后,你会发现一些断点没有停留在你设定的位置,而是“跳”到了一个已经被解析过的代码行。为了避免开发人员困惑,VSCode总是在Node.js认为的断点位置显示断点(这样VSCode的UI才能和Node.js的行为一致)。在VSCode的断点面板中,这类断点的后面会是这样显示:设置断点的行号->Node.js反馈的实际断点的行号,下面的图片来自于VScode文档:

emitter.js后面的6->10,就是这种情况,开发人员在第6行设置了断点,Node.js认为的实际断点在第10行。
当Node.js开始解析或者运行到被延迟解析的函数后,我们可以在断点面板上点击右键,点击重新应用所有断点,这时断点又会跳回到开发者设定的行上。不同VSCode的版本的具体操作会有所不同,之前的版本中,在断点面板右上方有一个刷新按钮,可以重新刷新断点。

跳过不感兴趣的代码

通过设置launch.json中的skipFiles配置选项,可以跳过不感兴趣的代码,示例如下:

  1. "skipFiles": [
  2. "${workspaceFolder}/node_modules/**/*.js",
  3. "${workspaceFolder}/lib/**/*.js"
  4. ]
复制

node_moduleslib下的所有js文件的代码都会被跳过。
如果要想跳过Node.js内部的Javascript代码,可以这样设置:

  1. "skipFiles": [
  2. "<node_internals>/**/*.js"
  3. ]
复制

具体的跳过规则是这样的:

  • 如果你step in入一个被跳过的文件,调试器不会在那里停留,会一直执行直到停留在后面的不在跳过之列的文件代码上。
  • 如果你设置了在抛异常的地方停住,而这个抛异常的地方在被跳过的代码文件里,那这个异常不会停住,只能在里面接住该异常,然后在外面没有被跳过的代码中抛异常,这样才可以停住。
  • 如果你在跳过的代码设置了断点,调试器会在运行到该代码时停住,然后可以在里面单步执行。
  • 跳过代码里的console日志显示到调试控制台时,在调试控制台右边的代码位置处显示的是“跳过”的代码后面首个非跳过代码的位置。这个还是挺迷惑人的。

跳过的代码在调用栈面板中会显示成暗淡的灰色,图示如下:

我们也可以在调用堆栈面板里的执行文件上点击右键,再点击Toggle skipping this file,来临时的跳过某个文件的调试。

VSCode支持的类Node的运行环境

当前的VSCode调试器支持Node.js8和8以后的版本,最近的Chrome,最近的Edge,这里的最近实际上是一个相对概念,在目前该文章发表时看,近一年内的Node.js Chrome Edge版本应该是没有问题的。但是如果后续如果调试协议有大调整,那可能就会涉及VSCode与目标运行环境的版本兼容问题。

就是这些了。