作者选择了COVID-19 救济基金来接受捐赠,作为Write for DOnations计划的一部分。
介绍
在 Node.js 开发中,将编码错误追溯到其源头可以在项目过程中节省大量时间。但是随着程序复杂性的增加,有效地做到这一点变得越来越困难。为了解决这个问题,开发人员使用调试器之类的工具,该程序允许开发人员在程序运行时对其进行检查。通过逐行重放代码并观察它如何改变程序状态,调试器可以深入了解程序的运行方式,从而更容易找到错误。
程序员用来跟踪代码中错误的常见做法是在程序运行时打印语句。在 Node.js 中,这涉及在其模块中添加额外的console.log()
orconsole.debug()
语句。虽然这种技术可以快速使用,但它也是手动的,因此可扩展性较差且更容易出错。使用这种方法,可能会错误地将敏感信息记录到控制台,这可能会向恶意代理提供有关客户或您的应用程序的私人信息。另一方面,调试器提供了一种系统的方法来观察程序中发生的事情,而不会将您的程序暴露在安全威胁之下。
调试器的主要功能是观察对象和添加断点。通过观察对象,调试器可以在程序员逐步执行程序时帮助跟踪变量的变化。断点是程序员可以放置在他们的代码中以阻止代码继续超出开发人员正在调查的点的标记。
在本文中,您将使用调试器来调试一些示例 Node.js 应用程序。您将首先使用内置的Node.js 调试器工具调试代码,设置观察器和断点,以便您可以找到错误的根本原因。然后,您将使用Google Chrome DevTools作为命令行 Node.js 调试器的图形用户界面 (GUI)替代方案。
先决条件
- 您需要在开发环境中安装 Node.js。本教程使用版本 10.19.0。要在 macOS 或 Ubuntu 18.04 上安装它,请按照如何在 macOS 上安装 Node.js 和创建本地开发环境或如何在 Ubuntu 18.04 上安装 Node.js 的使用 PPA 安装部分中的步骤进行操作。
- 对于本文,我们希望用户熟悉基本的 JavaScript,尤其是创建和使用函数。您可以通过阅读我们的如何在 JavaScript 中编码系列来了解这些基础知识以及更多内容。
- 要使用 Chrome DevTools 调试器,您需要下载并安装Google Chrome 网络浏览器或开源Chromium 网络浏览器。
第 1 步 – 在 Node.js 调试器中使用观察器
调试器主要用于两个特性:它们能够观察变量并观察它们在程序运行时如何变化,以及它们能够在称为断点的不同位置停止和启动代码执行。在这一步中,我们将介绍如何观察变量以识别代码中的错误。
在我们逐步执行代码时观察变量让我们深入了解变量的值如何随着程序运行而变化。让我们通过一个例子练习观察变量,以帮助我们发现和修复代码中的逻辑错误。
我们首先设置我们的编码环境。在您的终端中,创建一个名为的新文件夹debugging
:
- mkdir debugging
现在输入该文件夹:
- cd debugging
打开一个名为badLoop.js
. 我们将nano
在终端中使用它:
- nano badLoop.js
我们的代码将迭代一个数组并将数字添加到总和中,在我们的示例中,它将用于将商店一周内的每日订单数相加。程序将返回数组中所有数字的总和。在编辑器中,输入以下代码:
let orders = [341, 454, 198, 264, 307];
let totalOrders = 0;
for (let i = 0; i <= orders.length; i++) {
totalOrders += orders[i];
}
console.log(totalOrders);
我们首先创建orders
存储五个数字的数组。然后我们初始化totalOrders
为0
,因为它将存储五个数字的总和。在for
循环中,我们迭代地将每个值添加orders
到totalOrders
. 最后,我们在程序结束时打印订单总数。
保存并退出编辑器。现在运行这个程序node
:
- node badLoop.js
终端将显示此输出:
OutputNaN
NaN
在 JavaScript 中意味着Not a Number。鉴于所有输入都是有效数字,这是意外行为。为了找出错误,让我们使用 Node.js 调试器查看for
循环中更改的两个变量发生了什么变化:totalOrders
和i
。
当我们想在程序上使用内置的 Node.js 调试器时,我们inspect
在文件名之前包含。在您的终端中,node
使用此调试器选项运行命令,如下所示:
- node inspect badLoop.js
当您启动调试器时,您会发现如下输出:
Output< Debugger listening on ws://127.0.0.1:9229/e1ebba25-04b8-410b-811e-8a0c0902717a
< For help, see: https://nodejs.org/en/docs/inspector
< Debugger attached.
Break on start in badLoop.js:1
> 1 let orders = [341, 454, 198, 264, 307];
2
3 let totalOrders = 0;
第一行显示了我们调试服务器的 URL。当我们想要使用外部客户端进行调试时会使用它,例如我们稍后将看到的 Web 浏览器。请注意,此服务器的端口侦听:9229
的的localhost
(127.0.0.1
)默认情况下。出于安全原因,建议避免将此端口暴露给公众。
连接调试器后,调试器输出Break on start in badLoop.js:1
.
断点是我们希望停止执行的代码中的位置。默认情况下,Node.js 的调试器在文件开头停止执行。
然后调试器向我们展示了一段代码,然后是一个特殊的debug
提示:
Output...
> 1 let orders = [341, 454, 198, 264, 307];
2
3 let totalOrders = 0;
debug>
在>
旁边的1
指示,我们已经在我们的执行力达到了哪条线路,并及时是我们将在我们的赞扬到调试器类型。当此输出出现时,调试器已准备好接受命令。
使用调试器时,我们通过告诉调试器转到程序将执行的下一行来单步执行代码。Node.js 允许以下命令使用调试器:
c
或cont
:继续执行到下一个断点或程序结束。n
或next
:移至下一行代码。s
或step
: 进入一个函数。默认情况下,我们只单步调试我们正在调试的块或范围中的代码。通过进入一个函数,我们可以检查我们的代码调用的函数的代码,并观察它如何对我们的数据做出反应。o
: 跳出一个函数。进入函数后,调试器会在函数返回时返回主文件。我们可以使用这个命令在函数执行完成之前返回到我们正在调试的原始函数。pause
: 暂停正在运行的代码。
我们将逐行逐步执行此代码。按n
转到下一行:
- n
我们的调试器现在将停留在第三行代码:
Outputbreak in badLoop.js:3
1 let orders = [341, 454, 198, 264, 307];
2
> 3 let totalOrders = 0;
4
5 for (let i = 0; i <= orders.length; i++) {
为方便起见,跳过空行。如果我们n
在调试控制台中再按一次,我们的调试器将位于代码的第五行:
Outputbreak in badLoop.js:5
3 let totalOrders = 0;
4
> 5 for (let i = 0; i <= orders.length; i++) {
6 totalOrders += orders[i];
7 }
我们现在开始我们的循环。如果终端支持颜色,0
在let i = 0
将突出显示。调试器突出显示程序即将执行的代码部分,在for
循环中,首先执行计数器初始化。从这里,我们可以观察为什么totalOrders
返回NaN
而不是数字。在这个循环中,每次迭代都会改变两个变量——totalOrders
和i
。让我们为这两个变量设置观察者。
我们将首先为totalOrders
变量添加一个观察者。在交互式 shell 中,输入以下内容:
- watch('totalOrders')
为了观察变量,我们使用watch()
带有包含变量名称的字符串参数的内置函数。当我们按下ENTER
该watch()
函数时,提示将移动到下一行而不提供反馈,但是当我们将调试器移动到下一行时,监视词将可见。
现在让我们为变量添加一个观察者i
:
- watch('i')
现在我们可以看到我们的观察者在行动。按n
进入下一步。调试控制台将显示:
Outputbreak in badLoop.js:5
Watchers:
0: totalOrders = 0
1: i = 0
3 let totalOrders = 0;
4
> 5 for (let i = 0; i <= orders.length; i++) {
6 totalOrders += orders[i];
7 }
调试器现在显示totalOrders
和i
之前显示代码行的值,如输出所示。每当一行代码更改它们时,这些值都会更新。
此时,调试器length
在orders.length
. 这意味着程序将在执行其块内的代码之前检查条件。代码执行完毕后,i++
将执行最终的表达式。您可以for
在我们的How To Construct For Loops in JavaScript指南中阅读有关循环及其执行的更多信息。
进入n
控制台进入for
循环体:
Outputbreak in badLoop.js:6
Watchers:
0: totalOrders = 0
1: i = 0
4
5 for (let i = 0; i <= orders.length; i++) {
> 6 totalOrders += orders[i];
7 }
8
此步骤更新totalOrders
变量。因此,在这一步完成后,我们的变量和观察者将被更新。
按n
确认。你会看到这个:
OutputWatchers:
0: totalOrders = 341
1: i = 0
3 let totalOrders = 0;
4
> 5 for (let i = 0; i <= orders.length; i++) {
6 totalOrders += orders[i];
7 }
正如突出显示的,totalOrders
现在具有第一个订单的值:341
。
我们的调试器即将处理循环的最终条件。输入n
所以我们执行这一行并更新i
:
Outputbreak in badLoop.js:5
Watchers:
0: totalOrders = 341
1: i = 1
3 let totalOrders = 0;
4
> 5 for (let i = 0; i <= orders.length; i++) {
6 totalOrders += orders[i];
7 }
初始化后,我们不得不单步执行代码四次才能看到变量更新。像这样单步执行代码可能很乏味;这个问题将通过步骤 2 中的断点解决。但是现在,通过设置我们的观察者,我们已经准备好观察他们的价值观并找到我们的问题。
通过再输入n
12 次来逐步执行程序,观察输出。您的控制台将显示:
Outputbreak in badLoop.js:5
Watchers:
0: totalOrders = 1564
1: i = 5
3 let totalOrders = 0;
4
> 5 for (let i = 0; i <= orders.length; i++) {
6 totalOrders += orders[i];
7 }
回想一下,我们的orders
数组有五个项目,i
现在位于 位置5
。但由于i
用作数组的索引,因此在orders[5]
;处没有值。orders
数组的最后一个值在 index 处4
。这意味着orders[5]
将有一个值undefined
。
键入n
在控制台,你会看到,在循环中的代码被执行:
Outputbreak in badLoop.js:6
Watchers:
0: totalOrders = 1564
1: i = 5
4
5 for (let i = 0; i <= orders.length; i++) {
> 6 totalOrders += orders[i];
7 }
8
n
再次键入会显示totalOrders
迭代后的值:
Outputbreak in badLoop.js:5
Watchers:
0: totalOrders = NaN
1: i = 5
3 let totalOrders = 0;
4
> 5 for (let i = 0; i <= orders.length; i++) {
6 totalOrders += orders[i];
7 }
通过调试和看totalOrders
和i
,我们可以看到,我们的循环迭代六次而不是5。当i
是 时5
,orders[5]
被添加到totalOrders
。既然orders[5]
是undefined
,把它加到一个数字上就会产生NaN
。因此,我们代码的问题在于我们的for
循环条件。而不是检查是否i
小于或等于orders
数组的长度,我们应该只检查它是否小于长度。
让我们退出调试器,进行更改并再次运行代码。在调试提示中,键入 exit 命令并按ENTER
:
- .exit
现在您已经退出调试器,badLoop.js
在文本编辑器中打开:
- nano badLoop.js
改变for
循环的条件:
...
for (let i = 0; i < orders.length; i++) {
...
保存并退出nano
。现在让我们像这样执行我们的脚本:
- node badLoop.js
完成后,将打印正确的结果:
Output1564
在本节中,我们使用调试器的watch
命令来查找代码中的错误,修复它,并观察它是否按预期工作。
现在我们对调试器观察变量的基本使用有了一些经验,让我们看看我们如何使用断点,以便我们可以在不从程序开始时单步调试所有代码行的情况下进行调试。
第 2 步 – 在 Node.js 调试器中使用断点
Node.js 项目通常由许多相互连接的模块组成。逐行调试每个模块将非常耗时,尤其是当应用程序的复杂性扩展时。为了解决这个问题,断点允许我们跳转到我们想要暂停执行并检查程序的代码行。
在 Node.js 中调试时,我们通过debugger
直接在代码中添加关键字来添加断点。然后我们可以通过按下c
调试器控制台而不是 来从一个断点转到下一个断点n
。在每个断点处,我们可以为兴趣表达设置观察者。
让我们用一个例子来看看。在这一步中,我们将设置一个程序来读取句子列表并确定所有文本中最常用的单词。我们的示例代码将返回出现次数最多的第一个单词。
在本练习中,我们将创建三个文件。第一个文件 ,sentences.txt
将包含我们的程序将处理的原始数据。我们将添加大英百科全书关于鲸鲨的文章的开头文本作为示例数据,并删除标点符号。
在文本编辑器中打开文件:
- nano sentences.txt
接下来,输入以下代码:
Whale shark Rhincodon typus gigantic but harmless shark family Rhincodontidae that is the largest living fish
Whale sharks are found in marine environments worldwide but mainly in tropical oceans
They make up the only species of the genus Rhincodon and are classified within the order Orectolobiformes a group containing the carpet sharks
The whale shark is enormous and reportedly capable of reaching a maximum length of about 18 metres 59 feet
Most specimens that have been studied however weighed about 15 tons about 14 metric tons and averaged about 12 metres 39 feet in length
The body coloration is distinctive
Light vertical and horizontal stripes form a checkerboard pattern on a dark background and light spots mark the fins and dark areas of the body
保存并退出文件。
现在让我们将代码添加到textHelper.js
. 这个模块将包含一些我们将用来处理文本文件的方便的函数,从而更容易确定最流行的单词。textHelper.js
在文本编辑器中打开:
- nano textHelper.js
我们将创建三个函数来处理sentences.txt
. 首先是读取文件。在 中键入以下内容textHelper.js
:
const fs = require('fs');
const readFile = () => {
let data = fs.readFileSync('sentences.txt');
let sentences = data.toString();
return sentences;
};
首先,我们导入fs
Node.js 库,以便我们可以读取文件。然后readFile()
,我们创建用于readFileSync()
从对象加载数据sentences.txt
的Buffer
函数以及将toString()
其作为字符串返回的方法。
我们将添加的下一个函数处理一串文本并将其展平为一个包含单词的数组。将以下代码添加到编辑器中:
...
const getWords = (text) => {
let allSentences = text.split('\n');
let flatSentence = allSentences.join(' ');
let words = flatSentence.split(' ');
words = words.map((word) => word.trim().toLowerCase());
return words;
};
在该代码中,我们使用的方法中split()
,join()
以及map()
操纵串入的各个单词的阵列。该函数还将每个单词小写以方便计数。
需要的最后一个函数返回字符串数组中不同单词的计数。像这样添加最后一个函数:
...
const countWords = (words) => {
let map = {};
words.forEach((word) => {
if (word in map) {
map[word] = 1;
} else {
map[word] += 1;
}
});
return map;
};
在这里,我们创建一个JavaScript对象称为map
具有单词作为其键和它们的值计数。我们遍历数组,当它是循环的当前元素时,将每个单词的计数加一。让我们通过导出这些函数来完成这个模块,使它们可用于其他模块:
...
module.exports = { readFile, getWords, countWords };
保存并退出。
我们将用于本练习的第三个也是最后一个文件将使用该textHelper.js
模块在我们的文本中查找最流行的单词。index.js
用你的文本编辑器打开:
- nano index.js
我们通过导入textHelpers.js
模块开始我们的代码:
const textHelper = require('./textHelper');
继续创建一个包含停用词的新数组:
...
const stopwords = ['i', 'me', 'my', 'myself', 'we', 'our', 'ours', 'ourselves', 'you', 'your', 'yours', 'yourself', 'yourselves', 'he', 'him', 'his', 'himself', 'she', 'her', 'hers', 'herself', 'it', 'its', 'itself', 'they', 'them', 'their', 'theirs', 'themselves', 'what', 'which', 'who', 'whom', 'this', 'that', 'these', 'those', 'am', 'is', 'are', 'was', 'were', 'be', 'been', 'being', 'have', 'has', 'had', 'having', 'do', 'does', 'did', 'doing', 'a', 'an', 'the', 'and', 'but', 'if', 'or', 'because', 'as', 'until', 'while', 'of', 'at', 'by', 'for', 'with', 'about', 'against', 'between', 'into', 'through', 'during', 'before', 'after', 'above', 'below', 'to', 'from', 'up', 'down', 'in', 'out', 'on', 'off', 'over', 'under', 'again', 'further', 'then', 'once', 'here', 'there', 'when', 'where', 'why', 'how', 'all', 'any', 'both', 'each', 'few', 'more', 'most', 'other', 'some', 'such', 'no', 'nor', 'not', 'only', 'own', 'same', 'so', 'than', 'too', 'very', 's', 't', 'can', 'will', 'just', 'don', 'should', 'now', ''];
停用词是我们在处理文本之前过滤掉的语言中的常用词。我们可以使用它来找到比英语文本中最受欢迎的词是the
or的结果更有意义的数据a
。
继续使用textHelper.js
模块函数来获取包含单词及其计数的 JavaScript 对象:
...
let sentences = textHelper.readFile();
let words = textHelper.getWords(sentences);
let wordCounts = textHelper.countWords(words);
然后我们可以通过确定频率最高的单词来完成这个模块。为此,我们将使用单词计数遍历对象的每个键,并将其计数与先前存储的最大值进行比较。如果单词的计数更高,它将成为新的最大值。
添加以下代码行以计算最流行的单词:
...
let max = -Infinity;
let mostPopular = '';
Object.entries(wordCounts).forEach(([word, count]) => {
if (stopwords.indexOf(word) === -1) {
if (count > max) {
max = count;
mostPopular = word;
}
}
});
console.log(`The most popular word in the text is "${mostPopular}" with ${max} occurrences`);
在这段代码中,我们使用Object.entries()
将wordCounts
对象中的键值对转换为单个数组,所有这些数组都嵌套在一个更大的数组中。然后我们使用该forEach()
方法和一些条件语句来测试每个单词的计数并存储最高数字。
保存并退出文件。
现在让我们运行这个文件来查看它的运行情况。在您的终端中输入以下命令:
- node index.js
您将看到以下输出:
OutputThe most popular word in the text is "whale" with 1 occurrences
通过阅读文本,我们可以看出答案是错误的。快速搜索sentences.txt
会突出显示该词whale
出现多次。
我们有很多函数会导致这个错误:我们可能没有读取整个文件,或者我们可能没有正确地将文本处理到数组和 JavaScript 对象中。我们寻找最大单词的算法也可能不正确。找出问题的最好方法是使用调试器。
即使没有庞大的代码库,我们也不想花时间逐行执行每一行代码来观察情况何时发生变化。相反,我们可以使用断点在函数返回之前转到那些关键时刻并观察输出。
让我们在textHelper.js
模块中的每个函数中添加断点。为此,我们需要将关键字添加debugger
到我们的代码中。
textHelper.js
在文本编辑器中打开文件。我们将nano
再次使用:
- nano textHelper.js
首先,我们将断点添加到readFile()
函数中,如下所示:
...
const readFile = () => {
let data = fs.readFileSync('sentences.txt');
let sentences = data.toString();
debugger;
return sentences;
};
...
接下来,我们将为该getWords()
函数添加另一个断点:
...
const getWords = (text) => {
let allSentences = text.split('\n');
let flatSentence = allSentences.join(' ');
let words = flatSentence.split(' ');
words = words.map((word) => word.trim().toLowerCase());
debugger;
return words;
};
...
最后,我们将为该countWords()
函数添加一个断点:
...
const countWords = (words) => {
let map = {};
words.forEach((word) => {
if (word in map) {
map[word] = 1;
} else {
map[word] += 1;
}
});
debugger;
return map;
};
...
保存并退出textHelper.js
。
让我们开始调试过程。尽管断点在 中textHelpers.js
,但我们正在调试应用程序的主要入口点:index.js
。通过在 shell 中输入以下命令来启动调试会话:
- node inspect index.js
输入命令后,我们将看到以下输出:
Output< Debugger listening on ws://127.0.0.1:9229/b2d3ce0e-3a64-4836-bdbf-84b6083d6d30
< For help, see: https://nodejs.org/en/docs/inspector
< Debugger attached.
Break on start in index.js:1
> 1 const textHelper = require('./textHelper');
2
3 const stopwords = ['i', 'me', 'my', 'myself', 'we', 'our', 'ours', 'ourselves', 'you', 'your', 'yours', 'yourself', 'yourselves', 'he', 'him', 'his', 'himself', 'she', 'her', 'hers', 'herself', 'it', 'its', 'itself', 'they', 'them', 'their', 'theirs', 'themselves', 'what', 'which', 'who', 'whom', 'this', 'that', 'these', 'those', 'am', 'is', 'are', 'was', 'were', 'be', 'been', 'being', 'have', 'has', 'had', 'having', 'do', 'does', 'did', 'doing', 'a', 'an', 'the', 'and', 'but', 'if', 'or', 'because', 'as', 'until', 'while', 'of', 'at', 'by', 'for', 'with', 'about', 'against', 'between', 'into', 'through', 'during', 'before', 'after', 'above', 'below', 'to', 'from', 'up', 'down', 'in', 'out', 'on', 'off', 'over', 'under', 'again', 'further', 'then', 'once', 'here', 'there', 'when', 'where', 'why', 'how', 'all', 'any', 'both', 'each', 'few', 'more', 'most', 'other', 'some', 'such', 'no', 'nor', 'not', 'only', 'own', 'same', 'so', 'than', 'too', 'very', 's', 't', 'can', 'will', 'just', 'don', 'should', 'now', ''];
这一次,进入c
交互式调试器。提醒一下,c
是 continue 的缩写。这会将调试器跳转到代码中的下一个断点。按下c
并输入后ENTER
,您将在控制台中看到:
Outputbreak in textHelper.js:6
4 let data = fs.readFileSync('sentences.txt');
5 let sentences = data.toString();
> 6 debugger;
7 return sentences;
8 };
我们现在通过直接转到断点节省了一些调试时间。
在这个函数中,我们希望确保文件中的所有文本都被返回。为sentences
变量添加一个观察者,以便我们可以看到返回的内容:
- watch('sentences')
按n
移至下一行代码,以便我们观察sentences
. 您将看到以下输出:
Outputbreak in textHelper.js:7
Watchers:
0: sentences =
'Whale shark Rhincodon typus gigantic but harmless shark family Rhincodontidae that is the largest living fish\n' +
'Whale sharks are found in marine environments worldwide but mainly in tropical oceans\n' +
'They make up the only species of the genus Rhincodon and are classified within the order Orectolobiformes a group containing the carpet sharks\n' +
'The whale shark is enormous and reportedly capable of reaching a maximum length of about 18 metres 59 feet\n' +
'Most specimens that have been studied however weighed about 15 tons about 14 metric tons and averaged about 12 metres 39 feet in length\n' +
'The body coloration is distinctive\n' +
'Light vertical and horizontal stripes form a checkerboard pattern on a dark background and light spots mark the fins and dark areas of the body\n'
5 let sentences = data.toString();
6 debugger;
> 7 return sentences;
8 };
9
似乎我们在读取文件时没有任何问题;问题一定出在我们代码的其他地方。让我们再按c
一次移到下一个断点。当你这样做时,你会看到这个输出:
Outputbreak in textHelper.js:15
Watchers:
0: sentences =
ReferenceError: sentences is not defined
at eval (eval at getWords (your_file_path/debugger/textHelper.js:15:3), <anonymous>:1:1)
at Object.getWords (your_file_path/debugger/textHelper.js:15:3)
at Object.<anonymous> (your_file_path/debugger/index.js:7:24)
at Module._compile (internal/modules/cjs/loader.js:1125:14)
at Object.Module._extensions..js (internal/modules/cjs/loader.js:1167:10)
at Module.load (internal/modules/cjs/loader.js:983:32)
at Function.Module._load (internal/modules/cjs/loader.js:891:14)
at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:71:12)
at internal/main/run_main_module.js:17:47
13 let words = flatSentence.split(' ');
14 words = words.map((word) => word.trim().toLowerCase());
>15 debugger;
16 return words;
17 };
我们收到此错误消息是因为我们为该sentences
变量设置了一个观察者,但该变量在我们当前的函数作用域中不存在。一个观察者持续整个调试会话,所以只要我们继续观察sentences
它没有定义的地方,我们就会继续看到这个错误。
我们可以使用unwatch()
命令停止观察变量。让我们取消观察,sentences
这样我们就不再需要在每次调试器打印输出时看到此错误消息。在交互式提示中,输入以下命令:
- unwatch('sentences')
当您取消监视变量时,调试器不会输出任何内容。
回到getWords()
函数中,我们希望确保返回的单词列表是从我们之前加载的文本中提取的。让我们观察words
变量的值:
- watch('words')
然后回车进入n
调试器的下一行,这样我们就可以看到words
. 调试器将显示以下内容:
Outputbreak in textHelper.js:16
Watchers:
0: words =
[ 'whale',
'shark',
'rhincodon',
'typus',
'gigantic',
'but',
'harmless',
...
'metres',
'39',
'feet',
'in',
'length',
'',
'the',
'body',
'coloration',
... ]
14 words = words.map((word) => word.trim().toLowerCase());
15 debugger;
>16 return words;
17 };
18
调试器不会打印出整个数组,因为它很长并且会使输出更难阅读。但是,输出符合我们对应该存储什么的期望:将文本sentences
拆分为小写字符串。似乎getWords()
运行正常。
让我们继续观察countWords()
函数。首先,取消观察words
数组,以便在下一个断点时不会导致任何调试器错误。在命令提示符下,输入:
- unwatch('words')
接下来,c
在提示中输入。在最后一个断点处,我们将在 shell 中看到:
Outputbreak in textHelper.js:29
27 });
28
>29 debugger;
30 return map;
31 };
在这个函数中,我们希望确保map
变量正确包含我们句子中每个单词的计数。首先,让我们告诉调试器观察map
变量:
- watch('map')
按n
移至下一行。然后调试器将显示:
Outputbreak in textHelper.js:30
Watchers:
0: map =
{ 12: NaN,
14: NaN,
15: NaN,
18: NaN,
39: NaN,
59: NaN,
whale: 1,
shark: 1,
rhincodon: 1,
typus: NaN,
gigantic: NaN,
... }
28
29 debugger;
>30 return map;
31 };
32
这看起来不正确。似乎计算单词的方法产生了错误的结果。我们不知道为什么要输入这些值,所以下一步是调试words
数组上使用的循环中发生的事情。为此,我们需要对放置断点的位置进行一些更改。
首先,退出调试控制台:
- .exit
textHelper.js
在您的文本编辑器中打开,以便我们可以编辑断点:
- nano textHelper.js
首先,知道readFile()
并且getWords()
正在工作,我们将删除它们的断点。然后我们想countWords()
从函数的末尾删除断点,并在forEach()
块的开头和结尾添加两个新断点。
编辑textHelper.js
它看起来像这样:
...
const readFile = () => {
let data = fs.readFileSync('sentences.txt');
let sentences = data.toString();
return sentences;
};
const getWords = (text) => {
let allSentences = text.split('\n');
let flatSentence = allSentences.join(' ');
let words = flatSentence.split(' ');
words = words.map((word) => word.trim().toLowerCase());
return words;
};
const countWords = (words) => {
let map = {};
words.forEach((word) => {
debugger;
if (word in map) {
map[word] = 1;
} else {
map[word] += 1;
}
debugger;
});
return map;
};
...
保存并退出nano
用CTRL+X
。
让我们用这个命令再次启动调试器:
- node inspect index.js
为了深入了解正在发生的事情,我们想要调试循环中的一些事情。首先,让我们为 设置一个观察者word
,forEach()
循环中使用的参数包含循环当前正在查看的字符串。在调试提示中,输入:
- watch('word')
到目前为止,我们只观察了变量。但手表不限于变量。我们可以观察在我们的代码中使用的任何有效的 JavaScript 表达式。
实际上,我们可以为条件添加一个观察者word in map
,它决定了我们如何计算数字。在调试提示中,创建这个观察者:
- watch('word in map')
让我们还为map
变量中正在修改的值添加一个观察者:
- watch('map[word]')
观察者甚至可以是我们代码中未使用但可以使用我们拥有的代码进行评估的表达式。让我们通过为word
变量的长度添加一个观察者来看看这是如何工作的:
- watch('word.length')
现在我们已经设置了所有的观察者,让我们进入c
调试器提示,这样我们就可以看到循环中的第一个元素是如何countWords()
被评估的。调试器将打印此输出:
Outputbreak in textHelper.js:20
Watchers:
0: word = 'whale'
1: word in map = false
2: map[word] = undefined
3: word.length = 5
18 let map = {};
19 words.forEach((word) => {
>20 debugger;
21 if (word in map) {
22 map[word] = 1;
循环中的第一个单词是whale
。此时,map
对象没有键,whale
其为空。从下面仰望的时候whale
在map
,我们得到undefined
。最后,长度whale
为5
。这并不能帮助我们调试问题,但它确实验证了我们可以在调试时观察任何可以用代码计算的表达式。
再按c
一次以查看循环结束时发生了什么变化。调试器将显示:
Outputbreak in textHelper.js:26
Watchers:
0: word = 'whale'
1: word in map = true
2: map[word] = NaN
3: word.length = 5
24 map[word] += 1;
25 }
>26 debugger;
27 });
28
在循环结束时,word in map
现在为真,因为map
变量包含一个whale
键。价值map
的whale
关键是NaN
,这凸显了我们的问题。该if
声明countWords()
是为了一个字的计数设为一个,如果是新的,并添加一个,如果它已经存在。
罪魁祸首是if
语句的条件。我们应该设置map[word]
到1
如果word
不找到map
。现在,我们正在添加一个 if word
。在循环的开始,map["whale"]
是undefined
。在 JavaScript 中,undefined + 1
计算结果为NaN
– 不是数字。
对此的解决方法是将if
语句的条件从to 更改(word in map)
为(!(word in map))
,使用!
运算符来测试 if word
is not in map
。让我们在countWords()
函数中进行更改,看看会发生什么。
首先,退出调试器:
- .exit
现在textHelper.js
用你的文本编辑器打开文件:
- nano textHelper.js
修改countWords()
函数如下:
...
const countWords = (words) => {
let map = {};
words.forEach((word) => {
if (!(word in map)) {
map[word] = 1;
} else {
map[word] += 1;
}
});
return map;
};
...
保存并关闭编辑器。
现在让我们在没有调试器的情况下运行这个文件。在终端中,输入:
- node index.js
该脚本将输出以下句子:
OutputThe most popular word in the text is "whale" with 3 occurrences
这个输出似乎比我们之前收到的更有可能。使用调试器,我们找出哪个函数导致了问题,哪些函数没有。
我们使用内置的 CLI 调试器调试了两个不同的 Node.js 程序。我们现在可以使用debugger
关键字设置断点并创建各种观察者来观察内部状态的变化。但有时,可以更有效地从 GUI 应用程序调试代码。
在下一节中,我们将使用 Google Chrome 的 DevTools 中的调试器。我们将在 Node.js 中启动调试器,导航到 Google Chrome 中的专用调试页面,并使用 GUI 设置断点和观察器。
第 3 步 – 使用 Chrome DevTools 调试 Node.js
Chrome DevTools 是在 Web 浏览器中调试 Node.js 的流行选择。由于 Node.js 使用与Chrome相同的V8 JavaScript 引擎,因此调试体验比其他调试器更加集成。
对于本练习,我们将创建一个新的 Node.js 应用程序,该应用程序运行 HTTP 服务器并返回JSON 响应。然后,我们将使用调试器设置断点并深入了解为请求生成的响应。
让我们创建一个名为的新文件server.js
,用于存储我们的服务器代码。在文本编辑器中打开文件:
- nano server.js
此应用程序将返回带有Hello World
问候语的 JSON 。它将有一系列不同语言的消息。当收到一个请求时,它会随机选择一个问候语并在一个 JSON 正文中返回它。
此应用程序将在我们的localhost
服务器上的端口上运行:8000
。如果您想了解有关使用 Node.js 创建 HTTP 服务器的更多信息,请阅读我们关于如何使用 HTTP 模块在 Node.js 中创建 Web 服务器的指南。
在文本编辑器中输入以下代码:
const http = require("http");
const host = 'localhost';
const port = 8000;
const greetings = ["Hello world", "Hola mundo", "Bonjour le monde", "Hallo Welt", "Salve mundi"];
const getGreeting = function () {
let greeting = greetings[Math.floor(Math.random() * greetings.length)];
return greeting
}
我们首先导入http
创建 HTTP 服务器所需的模块。然后我们设置稍后将用于运行服务器的host
和port
变量。该greetings
数组包含我们的服务器可以返回的所有可能的问候语。该getGreeting()
函数随机选择一个问候语并返回它。
让我们添加处理 HTTP 请求的请求侦听器并添加代码来运行我们的服务器。通过键入以下内容继续编辑 Node.js 模块:
...
const requestListener = function (req, res) {
let message = getGreeting();
res.setHeader("Content-Type", "application/json");
res.writeHead(200);
res.end(`{"message": "${message}"}`);
};
const server = http.createServer(requestListener);
server.listen(port, host, () => {
console.log(`Server is running on http://${host}:${port}`);
});
我们的服务器现在可以使用了,所以让我们设置 Chrome 调试器。
我们可以使用以下命令启动 Chrome 调试器:
- node --inspect server.js
注意:请记住 CLI 调试器和 Chrome 调试器命令之间的区别。使用 CLI 时,您使用inspect
. 使用 Chrome 时,您使用--inspect
.
启动调试器后,您会发现以下输出:
OutputDebugger listening on ws://127.0.0.1:9229/996cfbaf-78ca-4ebd-9fd5-893888efe8b3
For help, see: https://nodejs.org/en/docs/inspector
Server is running on http://localhost:8000
现在打开 Google Chrome 或Chromium并chrome://inspect
在地址栏中输入。Microsoft Edge还使用 V8 JavaScript 引擎,因此可以使用相同的调试器。如果您使用的是 Microsoft Edge,请导航至edge://inspect
.
导航到 URL 后,您将看到以下页面:
Under the Devices header, click the Open dedicated DevTools for Node button. A new window will pop up:
We’re now able to debug our Node.js code with Chrome. Navigate to the Sources tab if not already there. On the left-hand side, expand the file tree and select server.js
:
Let’s add a breakpoint to our code. We want to stop when the server has selected a greeting and is about to return it. Click on the line number 10 in the debug console. A red dot will appear next to the number and the right-hand panel will indicate a new breakpoint was added:
Now let’s add a watch expression. On the right panel, click the arrow next to the Watch header to open the watch words list, then click +. Enter greeting
and press ENTER
so that we can observe its value when processing a request.
Next, let’s debug our code. Open a new browser window and navigate to http://localhost:8000
—the address the Node.js server is running on. When pressing ENTER
, we will not immediately get a response. Instead, the debug window will pop up once again. If it does not immediately come into focus, navigate to the debug window to see this:
The debugger pauses the server’s response where we set our breakpoint. The variables that we watch are updated in the right panel and also in the line of code that created it.
Let’s complete the response’s execution by pressing the continue button at the right panel, right above Paused on breakpoint. When the response is complete, you will see a successful JSON response in the browser window used to speak with the Node.js server:
{"message": "Hello world"}
In this way, Chrome DevTools does not require changes to the code to add breakpoints. If you prefer to use graphical applications over the command line to debug, the Chrome DevTools are more suitable for you.
Conclusion
In this article, we debugged sample Node.js applications by setting up watchers to observe the state of our application, and then by adding breakpoints to allow us to pause execution at various points in our program’s execution. We accomplished this using both the built-in CLI debugger and Google Chrome’s DevTools.
许多 Node.js 开发人员登录到控制台来调试他们的代码。虽然这很有用,但它不如能够暂停执行和观察各种状态变化那么灵活。因此,使用调试工具通常更有效,并且可以在开发项目的过程中节省时间。
要了解有关这些调试工具的更多信息,您可以阅读Node.js 文档或Chrome DevTools 文档。如果您想继续学习 Node.js,可以返回到如何在 Node.js 中编码系列,或在我们的Node 主题页面浏览编程项目和设置。