HomeArchivesTagsAbout

断点调试的学习与思考-以一个NodeJS程序调试为例

之前在调试JS代码的时候,经常使用的调试方式就是用console.log跟踪代码执行来确定问题点,但是使用console.log进行调试存在一些问题:

1. 污染代码结构
2. 调试第三方库的时候无能为力

而使用断点调试的原理是根据函数的调用栈信息来确定问题的所在,相比于console.log调试,断点调试针对上述两种情况拥有更好的表现。

由于在周五的工作中遇到了需要针对NodeJS程序进行断点调试的实例,因此我将结合当时使用情况来讲解一下我对断点调试的学习、理解和一些思考。

我使用的编辑器是Visual Studio Code,在讲述相关知识的时候会以VS Code为基础,但在其他的编辑器中情况应该是大同小异的。

断点调试基本知识

首先是基本界面:

UI界面

下面逐一讲解:

1. 变量信息:表示在当前的调用栈下的局部变量、闭包和全局变量,我们在调试的过程中可以通过观察变量的变化来追踪程序的问题
2. 调用堆栈信息:这个比较好理解,表示当前所处的调用栈
3. 已打的断点:表示在调试过程中你已经打过的断点
4. 调试选项配置:可以在这里设置调试程序时的命令和参数
5. 断点:你当前看到的断点
6. log和异常信息:log和异常信息会显示在调试控制台
7. 调试操作栏:调试过程中需要使用到的一些操作的集合,下文将逐一讲解

然后讲解一下调试操作栏中最重要的三个选项:

1. 单步跳过:指直接跳过当前语句的执行流程,叫程序的运行阶段推动到下一条应该执行语句,可以被断点打断。我们可以使用单步跳过来跳过一些对于定位问题没有帮助的函数执行
2. 单步调试:指进入当前函数的调用中,观察函数的执行细节。通过单步调试,我们可以观察到函数的执行细节并追踪到函数执行到某一步时的变量信息等重要信息
3. 单步跳出:指跳出当前函数的执行,叫程序的运行阶段推动到下一跳应该执行语句,可以被断点打断。我们可以用单步跳出配合单步调试来更加细粒度的控制程序的执行

掌握了基本知识基本就可以进行断点调试了,但是在调试NodeJS程序时有一点需要注意:针对.js后缀的文件,NodeJS是不原生支持ES Modules的,需要使用babel进行转译,这会造成我们实际调试的代码并不是我们书写的源代码,从而导致得不到相关信息。

解决办法:如果在代码中使用了ES Modules的语法,将它改成CommonJS Modules,并去掉相关的转译babel。2018/08/19补充:或使用sourcemap。

实例,一个NodeJS程序的断点调试

在周五我遇到的问题是这样的:

1
2
3
4
const actual = transformFileSync(actualPath, {
babelrc: false,
plugins: ['../../../src/index.js']
}).code;

在看到这段代码的时候师兄问了我一个问题:这个程序是怎么通过你plugins属性提供的字符串来把你需要的index.js文件加载进来的?

追踪到transformFileSync函数的定义,发现它是在第三方模块babel-core中引入的,显然,这个问题我们是无法用console.log来回答的。

于是就开始了断点调试的过程,首先在这一行打上断点,然后开始单步调试:

第一个断点

首先我选择通过结合上文提到的三种操作方式跑通一次程序,借此发现加载index.js文件的时候有哪些变量发生了怎样的变化。我发现,在变量信息中,一个plugins属性发生了变化:

plugins状态1

plugins状态2

在仔细分析了这个属性和以及它和正在调试的程序的关联后,我确定就是在这个属性变化的时候真正引入了index.js。

接下来要做的事就是通过单步调试,逐步缩窄范围,最后确定引入index.js文件的具体位置。

下面以缩窄范围的第一步举例:

首先通过单步调试,我发现程序运行到以下代码的第一行:

1
2
3
4
5
6
var file = new _file2.default(opts, this);
return file.wrap(code, function () {
file.addCode(code);
file.parseCode(code);
return file.transform();
});

而在单步调试和单步跳出

file
1
2
3
4
5
6
7
8
9
10
11
12
```这行代码以后,我发现`plugins`属性发生了上文提到的变化,这说明,这次index.js文件的加载一定是发生在```new _file2.default(..., ...)```这个函数的执行过程中,于是我就可以把断点定在这个函数的第一行,然后单步往后调试:

![第一次缩窄范围](http://ashenone.cn/narrow_first20180610.png)

经过几次重复上述的过程,最终定位到了加载index.js文件的代码:

```js
if (pluginLoc) {
plugin = require(pluginLoc);
} else {
throw new ReferenceError(messages.get("pluginUnknown", plugin, loc, i, dirname));
}

总结和思考

我在缩窄范围以定位问题的过程中一共打了6个断点,但是师兄给我演示时候只打了3个,这还是由于我对NodeJS和断点调试的熟练程度不够,以后多学习、运用,应该可以不断提升。

下面谈一点思考:

在这次学习断点调试的过程中,学到了不同的技术有不同适合的场景,比如有些问题可能通过log更好定位,另一些则用断点调试可能会更好。

2018/08/19补充:绝大部分问题用debugger都能更好的解决