如何使用内置调试器和ChromeDevTools调试Node.js
作者选择了 COVID-19 Relief Fund 作为 Write for DOnations 计划的一部分来接受捐赠。
介绍
在 Node.js 开发中,将编码错误追溯到其源头可以在项目过程中节省大量时间。 但是随着程序复杂性的增加,要有效地做到这一点变得越来越难。 为了解决这个问题,开发人员使用 调试器 之类的工具,该程序允许开发人员在程序运行时对其进行检查。 通过逐行重放代码并观察它如何改变程序的状态,调试器可以深入了解程序的运行情况,从而更容易发现错误。
程序员用来跟踪代码中错误的一种常见做法是在程序运行时打印语句。 在 Node.js 中,这涉及在其模块中添加额外的 console.log() 或 console.debug() 语句。 虽然这种技术可以快速使用,但它也是手动的,因此它的可扩展性较差并且更容易出错。 使用这种方法,可能会错误地将敏感信息记录到控制台,这可能会为恶意代理提供有关客户或您的应用程序的私人信息。 另一方面,调试器提供了一种系统的方法来观察程序中发生的事情,而不会使您的程序面临安全威胁。
调试器的主要功能是观察对象和添加断点。 通过观察对象,调试器可以帮助在程序员逐步执行程序时跟踪变量的变化。 断点是程序员可以在其代码中放置的标记,以阻止代码继续超出开发人员正在调查的点。
在本文中,您将使用调试器来调试一些示例 Node.js 应用程序。 您将首先使用内置的 Node.js 调试器工具 调试代码,设置观察程序和断点,以便找到错误的根本原因。 然后,您将使用 Google Chrome DevTools 作为 图形用户界面 (GUI) 替代命令行 Node.js 调试器。
先决条件
- 您需要在开发环境中安装 Node.js。 本教程使用版本 10.19.0。 要在 macOS 或 Ubuntu 18.04 上安装它,请按照 如何在 macOS 上安装 Node.js 和创建本地开发环境中的步骤或 的 使用 PPA 部分安装如何在 Ubuntu 18.04 上安装 Node.js。
- 对于本文,我们希望用户熟悉基本的 JavaScript,尤其是创建和使用 函数。 您可以通过阅读我们的 如何在 JavaScript 中编码系列 来了解这些基础知识以及更多内容。
- 要使用 Chrome DevTools 调试器,您需要下载并安装 Google Chrome 网络浏览器 或开源 Chromium 网络浏览器。
第 1 步 — 在 Node.js 调试器中使用 Watchers
调试器主要用于两个功能:它们能够观察变量并观察它们在程序运行时如何变化,以及它们能够在称为断点的不同位置停止和启动代码执行。 在这一步中,我们将介绍如何通过观察变量来识别代码中的错误。
在我们逐步执行代码时观察变量可以让我们深入了解变量的值在程序运行时如何变化。 让我们通过一个示例练习观察变量,以帮助我们查找和修复代码中的逻辑错误。
我们首先设置我们的编码环境。 在您的终端中,创建一个名为 debugging
的新文件夹:
mkdir debugging
现在进入该文件夹:
cd debugging
打开一个名为 badLoop.js
的新文件。 我们将使用 nano
,因为它在终端中可用:
nano badLoop.js
我们的代码将遍历 array 并将数字添加到总和中,在我们的示例中,该总和将用于将商店一周内的每日订单数相加。 该程序将返回数组中所有数字的总和。 在编辑器中,输入以下代码:
调试/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
JavaScript 中的 NaN 表示 不是数字 。 鉴于所有输入都是有效数字,这是意外行为。 为了找到错误,让我们使用 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 浏览器。 请注意,此服务器默认侦听 localhost
(127.0.0.1
) 的端口 :9229
。 出于安全原因,建议避免将此端口暴露给公众。
附加调试器后,调试器输出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
:步入函数。 默认情况下,我们只单步执行我们正在调试的块或 scope 中的代码。 通过进入一个函数,我们可以检查我们的代码调用的函数的代码,并观察它如何对我们的数据做出反应。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 }
我们现在开始我们的循环。 如果终端支持颜色,let i = 0
中的0
会高亮显示。 调试器高亮显示程序即将执行的部分代码,在for
循环中,首先执行计数器初始化。 从这里,我们可以看到为什么 totalOrders
返回 NaN
而不是数字。 在此循环中,每次迭代都会更改两个变量 - totalOrders
和 i
。 让我们为这两个变量设置观察者。
我们将首先为 totalOrders
变量添加一个观察者。 在交互式 shell 中,输入:
watch('totalOrders')
要查看变量,我们使用内置的 watch()
函数和包含变量名称的 string 参数。 当我们在 watch()
函数上按 ENTER
时,提示将移至下一行而不提供反馈,但当我们将调试器移至下一行时,将看到监视字。
现在让我们为变量 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
的值,如输出所示。 每次一行代码更改这些值时,它们都会更新。
此时,调试器正在突出显示 orders.length
中的 length
。 这意味着程序将在执行其块中的代码之前检查条件。 代码执行完毕后,会执行最终表达式i++
。 您可以在我们的 How To Construct For Loops in JavaScript 指南中阅读有关 for
循环及其执行的更多信息。
在控制台输入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 }
初始化后,我们必须四次单步执行代码才能看到变量更新。 像这样单步执行代码可能很乏味; 这个问题将通过 Step 2 中的断点来解决。 但是现在,通过设置我们的观察者,我们已经准备好观察他们的价值观并找到我们的问题。
通过再输入十二次 n
来逐步执行程序,观察输出。 您的控制台将显示以下内容:
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
数组的最后一个值位于索引 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
,我们可以看到我们的循环迭代了六次而不是五次。 当 i
为 5
时,orders[5]
被添加到 totalOrders
。 由于 orders[5]
是 undefined
,将其与数字相加将产生 NaN
。 因此,我们代码的问题在于我们的 for
循环的条件。 与其检查 i
是否小于或等于 orders
数组的长度,我们应该只检查它是否小于长度。
让我们退出调试器,进行更改并再次运行代码。 在调试提示符下,键入退出命令并按 ENTER
:
.exit
现在您已经退出调试器,在您的文本编辑器中打开 badLoop.js
:
nano badLoop.js
更改 for
循环的条件:
调试器/badLoop.js
... 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
接下来,输入以下代码:
调试器/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
中键入以下内容:
调试器/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()
方法将其作为字符串返回。
我们将添加的下一个函数处理一个文本字符串并将其展平为一个包含单词的数组。 将以下代码添加到编辑器中:
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()); 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; };
在这里,我们创建了一个名为 map
的 JavaScript 对象,其中单词作为键,计数作为值。 我们遍历数组,当它是循环的当前元素时,将每个单词的计数加一。 让我们通过导出这些函数来完成这个模块,使它们可用于其他模块:
调试器/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', ''];
停用词是我们在处理文本之前过滤掉的语言中的常用词。 我们可以使用它来找到比英文文本中最流行的单词是 the
或 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
您将看到以下输出:
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()
函数,如下所示:
调试器/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
后,您将在控制台中看到:
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
使其看起来像这样:
调试器/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; }; ...
使用 CTRL+X
保存并退出 nano
。
让我们用这个命令再次启动调试器:
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
为空的键。 接着,当在 map
中查找 whale
时,我们得到 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
键。 whale
键的 map
的值为 NaN
,这突出了我们的问题。 countWords()
中的 if
语句旨在将一个单词的计数设置为如果它是新的,如果它已经存在则添加一个。
罪魁祸首是 if
语句的条件。 如果在 map
中找不到 word
,我们应该将 map[word]
设置为 1
。 现在,如果找到 word
,我们将添加一个。 在循环开始时,map["whale"]
是 undefined
。 在 JavaScript 中,undefined + 1
的计算结果为 NaN
——不是数字。
解决此问题的方法是将 if
语句的条件从 (word in map)
更改为 (!(word in map))
,使用 !
运算符测试 [ X150X] 不在 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
该脚本将输出以下语句:
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 服务器的指南。
在文本编辑器中键入以下代码:
调试器/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
模块。 然后我们设置 host
和 port
变量,我们稍后将使用它们来运行服务器。 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
。
启动调试器后,您会发现以下输出:
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 后,您将看到以下页面:
在 Devices 标题下,单击 Open dedicated DevTools for Node 按钮。 将弹出一个新窗口:
我们现在可以使用 Chrome 调试我们的 Node.js 代码。 如果还没有,请导航到 Sources 选项卡。 在左侧,展开文件树并选择 server.js
:
让我们在代码中添加一个断点。 当服务器选择了一个问候语并即将返回它时,我们希望停止。 点击调试控制台中的行号10。 数字旁边会出现一个红点,右侧面板将指示添加了一个新断点:
现在让我们添加一个监视表达式。 在右侧面板上,单击 Watch 标题旁边的箭头以打开监视词列表,然后单击 +。 输入greeting
,然后按ENTER
,这样我们在处理请求的时候就可以观察它的值了。
接下来,让我们调试我们的代码。 打开一个新的浏览器窗口并导航到 http://localhost:8000
——Node.js 服务器运行的地址。 当按下 ENTER
时,我们不会立即得到响应。 相反,调试窗口将再次弹出。 如果它没有立即成为焦点,请导航到调试窗口以查看:
调试器在我们设置断点的地方暂停服务器的响应。 我们观察到的变量在右侧面板和创建它的代码行中更新。
让我们通过按下右侧面板上的继续按钮来完成响应的执行,就在 Paused on breakpoint 的正上方。 响应完成后,您将在用于与 Node.js 服务器对话的浏览器窗口中看到成功的 JSON 响应:
{"message": "Hello world"}
这样,Chrome DevTools 就不需要更改代码来添加断点。 如果您更喜欢通过命令行使用图形应用程序进行调试,Chrome DevTools 更适合您。
结论
在本文中,我们通过设置观察者来观察应用程序的状态,然后通过添加断点来允许我们在程序执行的各个点暂停执行来调试示例 Node.js 应用程序。 我们使用内置的 CLI 调试器和 Google Chrome 的 DevTools 完成了这项工作。
许多 Node.js 开发人员登录到控制台来调试他们的代码。 虽然这很有用,但它不如暂停执行和观察各种状态变化那么灵活。 因此,使用调试工具通常更有效,并且会在项目开发过程中节省时间。
要了解有关这些调试工具的更多信息,您可以阅读 Node.js 文档 或 Chrome DevTools 文档。 如果您想继续学习 Node.js,您可以返回 如何在 Node.js 中编写代码系列,或者在我们的 Node 主题页面 上浏览编程项目和设置。