如何使用内置调试器和 Chrome DevTools 调试 Node.js

作者选择了COVID-19 救济基金来接受捐赠,作为Write for DOnations计划的一部分。

介绍

在 Node.js 开发中,将编码错误追溯到其源头可以在项目过程中节省大量时间。但是随着程序复杂性的增加,有效地做到这一点变得越来越困难。为了解决这个问题,开发人员使用调试器之类的工具,该程序允许开发人员在程序运行时对其进行检查。通过逐行重放代码并观察它如何改变程序状态,调试器可以深入了解程序的运行方式,从而更容易找到错误。

程序员用来跟踪代码中错误的常见做法是在程序运行时打印语句。在 Node.js 中,这涉及在其模块中添加额外的console.log()orconsole.debug()语句。虽然这种技术可以快速使用,但它也是手动的,因此可扩展性较差且更容易出错。使用这种方法,可能会错误地将敏感信息记录到控制台,这可能会向恶意代理提供有关客户或您的应用程序的私人信息。另一方面,调试器提供了一种系统的方法来观察程序中发生的事情,而不会将您的程序暴露在安全威胁之下。

调试器的主要功能是观察对象和添加断点通过观察对象,调试器可以在程序员逐步执行程序时帮助跟踪变量的变化。断点是程序员可以放置在他们的代码中以阻止代码继续超出开发人员正在调查的点的标记。

在本文中,您将使用调试器来调试一些示例 Node.js 应用程序。您将首先使用内置的Node.js 调试器工具调试代码,设置观察器和断点,以便您可以找到错误的根本原因。然后,您将使用Google Chrome DevTools作为命令行 Node.js 调试器图形用户界面 (GUI)替代方案。

先决条件

第 1 步 – 在 Node.js 调试器中使用观察器

调试器主要用于两个特性:它们能够观察变量并观察它们在程序运行时如何变化,以及它们能够在称为断点的不同位置停止和启动代码执行在这一步中,我们将介绍如何观察变量以识别代码中的错误。

在我们逐步执行代码时观察变量让我们深入了解变量的值如何随着程序运行而变化。让我们通过一个例子练习观察变量,以帮助我们发现和修复代码中的逻辑错误。

我们首先设置我们的编码环境。在您的终端中,创建一个名为的新文件夹debugging

  • mkdir debugging

现在输入该文件夹:

  • cd debugging

打开一个名为badLoop.js. 我们将nano在终端中使用它:

  • nano badLoop.js

我们的代码将迭代一个数组并将数字添加到总和中,在我们的示例中,它将用于将商店一周内的每日订单数相加。程序将返回数组中所有数字的总和。在编辑器中,输入以下代码:

调试/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存储五个数字数组。然后我们初始化totalOrders0,因为它将存储五个数字的总和。for循环中,我们迭代地将每个值添加orderstotalOrders. 最后,我们在程序结束时打印订单总数。

保存并退出编辑器。现在运行这个程序node

  • node badLoop.js

终端将显示此输出:

Output
NaN

NaN在 JavaScript 中意味着Not a Number鉴于所有输入都是有效数字,这是意外行为。为了找出错误,让我们使用 Node.js 调试器查看for循环中更改的两个变量发生了什么变化totalOrdersi

当我们想在程序上使用内置的 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的的localhost127.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 允许以下命令使用调试器:

  • ccont:继续执行到下一个断点或程序结束。
  • nnext:移至下一行代码。
  • sstep: 进入一个函数。默认情况下,我们只单步调试我们正在调试的块或范围中的代码通过进入一个函数,我们可以检查我们的代码调用的函数的代码,并观察它如何对我们的数据做出反应。
  • o: 跳出一个函数。进入函数后,调试器会在函数返回时返回主文件。我们可以使用这个命令在函数执行完成之前返回到我们正在调试的原始函数。
  • pause: 暂停正在运行的代码。

我们将逐行逐步执行此代码。n转到下一行:

  • n

我们的调试器现在将停留在第三行代码:

Output
break 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在调试控制台中再按一次,我们的调试器将位于代码的第五行:

Output
break in badLoop.js:5 3 let totalOrders = 0; 4 > 5 for (let i = 0; i <= orders.length; i++) { 6 totalOrders += orders[i]; 7 }

我们现在开始我们的循环。如果终端支持颜色,0let i = 0将突出显示。调试器突出显示程序即将执行的代码部分,在for循环中,首先执行计数器初始化。从这里,我们可以观察为什么totalOrders返回NaN而不是数字。在这个循环中,每次迭代都会改变两个变量——totalOrdersi让我们为这两个变量设置观察者。

我们将首先为totalOrders变量添加一个观察者在交互式 shell 中,输入以下内容:

  • watch('totalOrders')

为了观察变量,我们使用watch()带有包含变量名称字符串参数的内置函数当我们按下ENTERwatch()函数时,提示将移动到下一行而不提供反馈,但是当我们将调试器移动到下一行时,监视词将可见。

现在让我们为变量添加一个观察者i

  • watch('i')

现在我们可以看到我们的观察者在行动。n进入下一步。调试控制台将显示:

Output
break 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 }

调试器现在显示totalOrdersi之前显示代码行的值,如输出所示。每当一行代码更改它们时,这些值都会更新。

此时,调试器lengthorders.length. 这意味着程序将在执行其块内的代码之前检查条件。代码执行完毕后,i++将执行最终的表达式您可以for在我们的How To Construct For Loops in JavaScript指南中阅读有关循环及其执行的更多信息

进入n控制台进入for循环体:

Output
break 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确认。你会看到这个:

Output
Watchers: 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

Output
break 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 中的断点解决但是现在,通过设置我们的观察者,我们已经准备好观察他们的价值观并找到我们的问题。

通过再输入n12 次来逐步执行程序,观察输出。您的控制台将显示:

Output
break 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在控制台,你会看到,在循环中的代码被执行:

Output
break 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迭代后的值

Output
break 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 }

通过调试和看totalOrdersi,我们可以看到,我们的循环迭代六次而不是5。i是 时5orders[5]被添加到totalOrders既然orders[5]undefined,把它加到一个数字上就会产生NaN因此,我们代码的问题在于我们的for循环条件。而不是检查是否i小于或等于orders数组的长度,我们应该只检查它是否小于长度。

让我们退出调试器,进行更改并再次运行代码。在调试提示中,键入 exit 命令并按ENTER

  • .exit

现在您已经退出调试器,badLoop.js在文本编辑器中打开

  • nano badLoop.js

改变for循环的条件:

调试器/badLoop.js
...
for (let i = 0; i < orders.length; i++) {
...

保存并退出nano现在让我们像这样执行我们的脚本:

  • node badLoop.js

完成后,将打印正确的结果:

Output
1564

在本节中,我们使用调试器的watch命令来查找代码中的错误,修复它,并观察它是否按预期工作。

现在我们对调试器观察变量的基本使用有了一些经验,让我们看看我们如何使用断点,以便我们可以在不从程序开始时单步调试所有代码行的情况下进行调试。

第 2 步 – 在 Node.js 调试器中使用断点

Node.js 项目通常由许多相互连接的模块组成逐行调试每个模块将非常耗时,尤其是当应用程序的复杂性扩展时。为了解决这个问题,断点允许我们跳转到我们想要暂停执行并检查程序的代码行。

在 Node.js 中调试时,我们通过debugger直接在代码中添加关键字来添加断点然后我们可以通过按下c调试器控制台而不是 来从一个断点转到下一个断点n在每个断点处,我们可以为兴趣表达设置观察者。

让我们用一个例子来看看。在这一步中,我们将设置一个程序来读取句子列表并确定所有文本中最常用的单词。我们的示例代码将返回出现次数最多的第一个单词。

在本练习中,我们将创建三个文件。第一个文件 ,sentences.txt将包含我们的程序将处理的原始数据。我们将添加大英百科全书关于鲸鲨的文章的开头文本作为示例数据,并删除标点符号。

在文本编辑器中打开文件:

  • nano sentences.txt

接下来,输入以下代码:

调试器/句子.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

调试器/textHelper.js
const fs = require('fs');

const readFile = () => {
  let data = fs.readFileSync('sentences.txt');
  let sentences = data.toString();
  return sentences;
};

首先,我们导入fsNode.js 库,以便我们可以读取文件。然后readFile()我们创建用于readFileSync()对象加载数据sentences.txtBuffer函数以及将toString()其作为字符串返回方法。

我们将添加的下一个函数处理一串文本并将其展平为一个包含单词的数组。将以下代码添加到编辑器中:

文本助手.js
...

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()操纵串入的各个单词的阵列。该函数还将每个单词小写以方便计数。

需要的最后一个函数返回字符串数组中不同单词的计数。像这样添加最后一个函数:

调试器/textHelper.js
...

const countWords = (words) => {
  let map = {};
  words.forEach((word) => {
    if (word in map) {
      map[word] = 1;
    } else {
      map[word] += 1;
    }
  });

  return map;
};

在这里,我们创建一个JavaScript对象称为map具有单词作为其键和它们的值计数。我们遍历数组,当它是循环的当前元素时,将每个单词的计数加一。让我们通过导出这些函数来完成这个模块,使它们可用于其他模块:

调试器/textHelper.js
...

module.exports = { readFile, getWords, countWords };

保存并退出。

我们将用于本练习的第三个也是最后一个文件将使用该textHelper.js模块在我们的文本中查找最流行的单词。index.js用你的文本编辑器打开

  • nano index.js

我们通过导入textHelpers.js模块开始我们的代码

调试器/index.js
const textHelper = require('./textHelper');

继续创建一个包含停用词的新数组

调试器/index.js
...

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', ''];

停用词是我们在处理文本之前过滤掉的语言中的常用词。我们可以使用它来找到比英语文本中最受欢迎的词是theor的结果更有意义的数据a

继续使用textHelper.js模块函数来获取包含单词及其计数的 JavaScript 对象:

调试器/index.js
...

let sentences = textHelper.readFile();
let words = textHelper.getWords(sentences);
let wordCounts = textHelper.countWords(words);

然后我们可以通过确定频率最高的单词来完成这个模块。为此,我们将使用单词计数遍历对象的每个键,并将其计数与先前存储的最大值进行比较。如果单词的计数更高,它将成为新的最大值。

添加以下代码行以计算最流行的单词:

调试器/index.js
...

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

您将看到以下输出:

Output
The 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()函数中,如下所示:

调试器/textHelper.js
...

const readFile = () => {
  let data = fs.readFileSync('sentences.txt');
  let sentences = data.toString();
  debugger;
  return sentences;
};

...

接下来,我们将为该getWords()函数添加另一个断点

调试器/textHelper.js
...

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()函数添加一个断点

调试器/textHelper.js
...

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,您将在控制台中看到:

Output
break 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. 您将看到以下输出:

Output
break 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一次移到下一个断点当你这样做时,你会看到这个输出:

Output
break 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. 调试器将显示以下内容:

Output
break 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 中看到:

Output
break in textHelper.js:29 27 }); 28 >29 debugger; 30 return map; 31 };

在这个函数中,我们希望确保map变量正确包含我们句子中每个单词的计数。首先,让我们告诉调试器观察map变量:

  • watch('map')

n移至下一行。然后调试器将显示:

Output
break 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它看起来像这样:

调试器/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;
};

...

保存并退出nanoCTRL+X

让我们用这个命令再次启动调试器:

  • node inspect index.js

为了深入了解正在发生的事情,我们想要调试循环中的一些事情。首先,让我们为 设置一个观察者wordforEach()循环中使用的参数包含循环当前正在查看的字符串。在调试提示中,输入:

  • watch('word')

到目前为止,我们只观察了变量。但手表不限于变量。我们可以观察在我们的代码中使用的任何有效的 JavaScript 表达式。

实际上,我们可以为条件添加一个观察者word in map,它决定了我们如何计算数字。在调试提示中,创建这个观察者:

  • watch('word in map')

让我们还为map变量中正在修改的值添加一个观察者

  • watch('map[word]')

观察者甚至可以是我们代码中未使用但可以使用我们拥有的代码进行评估的表达式。让我们通过为word变量的长度添加一个观察者来看看这是如何工作的

  • watch('word.length')

现在我们已经设置了所有的观察者,让我们进入c调试器提示,这样我们就可以看到循环中的第一个元素是如何countWords()被评估的。调试器将打印此输出:

Output
break 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其为空。从下面仰望的时候whalemap,我们得到undefined最后,长度whale5这并不能帮助我们调试问题,但它确实验证了我们可以在调试时观察任何可以用代码计算的表达式。

再按c一次以查看循环结束时发生了什么变化。调试器将显示:

Output
break 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键。价值mapwhale关键是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 wordis not in map让我们在countWords()函数中进行更改,看看会发生什么。

首先,退出调试器:

  • .exit

现在textHelper.js用你的文本编辑器打开文件:

  • nano textHelper.js

修改countWords()函数如下:

调试器/textHelper.js
...

const countWords = (words) => {
  let map = {};
  words.forEach((word) => {
    if (!(word in map)) {
      map[word] = 1;
    } else {
      map[word] += 1;
    }
  });

  return map;
};

...

保存并关闭编辑器。

现在让我们在没有调试器的情况下运行这个文件。在终端中,输入:

  • node index.js

该脚本将输出以下句子:

Output
The 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 服务器的指南

在文本编辑器中输入以下代码:

调试器/server.js
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 服务器所需模块。然后我们设置稍后将用于运行服务器hostport变量。greetings数组包含我们的服务器可以返回的所有可能的问候语。getGreeting()函数随机选择一个问候语并返回它。

让我们添加处理 HTTP 请求的请求侦听器并添加代码来运行我们的服务器。通过键入以下内容继续编辑 Node.js 模块:

调试器/server.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.

启动调试器后,您会发现以下输出:

Output
Debugger 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 或Chromiumchrome://inspect在地址栏中输入Microsoft Edge还使用 V8 JavaScript 引擎,因此可以使用相同的调试器。如果您使用的是 Microsoft Edge,请导航至edge://inspect.

导航到 URL 后,您将看到以下页面:

Screenshot of Google Chome's "inspect" page

Under the Devices header, click the Open dedicated DevTools for Node button. A new window will pop up:

Screenshot of debug window

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:

Screenshot of debugger window's Sources tab

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:

Screenshot of adding a breakpoint in the Chrome debugger

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:

Screenshot of the program's execution paused in Chrome

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 主题页面浏览编程项目和设置

觉得文章有用?

点个广告表达一下你的爱意吧 !😁