NodeJS-开发学习手册-二-

47 阅读51分钟

NodeJS 开发学习手册(二)

原文:zh.annas-archive.org/md5/551AEEE166502AE00C0784F70639ECDF

译者:飞龙

协议:CC BY-NC-SA 4.0

第三章:Node 基础知识-第二部分

在这一章中,我们将继续讨论一些更多的 node 基础知识。我们将探讨 yargs,并看看如何使用process.argv和 yargs 来解析命令行参数。之后,我们将探讨 JSON。JSON 实际上就是一个看起来有点像 JavaScript 对象的字符串,与之不同的是它使用双引号而不是单引号,并且所有的属性名称,比如nameage,在这种情况下都需要用引号括起来。我们将探讨如何将对象转换为字符串,然后定义该字符串,使用它,并将其转换回对象。

在我们完成了这些之后,我们将填写addNote函数。最后,我们将进行重构,将功能移入单独的函数并测试功能。

更具体地,我们将讨论以下主题:

  • yargs

  • JSON

  • 添加注释

  • 重构

yargs

在本节中,我们将使用 yargs,一个第三方的 npm 模块,来使解析过程更加容易。它将让我们访问诸如标题和正文信息之类的东西,而无需编写手动解析器。这是一个很好的例子,说明何时应该寻找一个 npm 模块。如果我们不使用模块,对于我们的 Node 应用程序来说,使用经过测试和彻底审查的第三方模块会更加高效。

首先,我们将安装该模块,然后将其添加到项目中,解析诸如标题和正文之类的内容,并调用在notes.js中定义的所有函数。如果命令是add,我们将调用add note

安装 yargs

现在,让我们查看 yargs 的文档页面。了解自己将要涉足的领域总是一个好主意。如果你在 Google 上搜索yargs,你应该会发现 GitHub 页面是你的第一个搜索结果。如下截图所示,我们有 yargs 库的 GitHub 页面:

现在,yargs 是一个非常复杂的库。它有很多功能来验证各种输入,并且有不同的方式来格式化输入。我们将从一个非常基本的例子开始,尽管在本章中我们将介绍更复杂的例子。

如果你想查看我们在本章中没有讨论的任何其他功能,或者你只是想看看我们讨论过的某些功能是如何工作的,你可以在yarg 文档中找到它。

现在,我们将进入终端,在我们的应用程序中安装这个模块。为了做到这一点,我们将使用npm install,然后是模块名称yargs,在这种情况下,我将使用@符号来指定我想要使用的模块的特定版本,即 11.0.0,这是我写作时最新的版本。接下来,我将添加save标志,正如我们所知,这会更新package.json文件:

npm install yargs@11.0.0 --save

如果我不加save标志,yargs 将被安装到node_modules文件夹中,但如果我们稍后清空该node_modules文件夹并运行npm install,yargs 将不会被重新安装,因为它没有列在package.json文件中。这就是为什么我们要使用save标志的原因。

运行 yargs

现在我们已经安装了 yargs,我们可以进入 Atom,在app.js中开始使用它。yargs 的基础,它的功能集的核心,非常简单易用。我们要做的第一件事就是像我们在上一章中使用fslodash一样,将其require进来。让我们创建一个常量并将其命名为yargs,将其设置为require('yargs'),如下所示:

console.log('Starting app.js');

const fs = require('fs');
const _ = require('lodash');
const yargs = require('yargs');

const notes = require('./notes.js');

var command = process.argv[2];
console.log('Command:', command);
console.log(process.argv);

if (command === 'add') {
  console.log('Adding new note');
} else if (command === 'list') {
  console.log('Listing all notes');
} else if (command === 'read') {
  console.log('Reading note');
} else if (command === 'remove') {
  console.log('Removing note');
} else {
  console.log('Command not recognized');
}

从这里,我们可以获取 yargs 解析的参数。它将获取我们在上一章中讨论过的process.argv数组,但它在后台解析它,给我们比 Node 给我们的更有用的东西。就在command变量的上面,我们可以创建一个名为argvconst变量,将其设置为yargs.argv,如下所示:

console.log('Starting app.js');

const fs = require('fs');
const _ = require('lodash');
const yargs = require('yargs');

const notes = require('./notes.js');

const argv = yargs.argv;
var command = process.argv[2];
console.log('Command:', command);
console.log(process.argv);

if (command === 'add') {
  console.log('Adding new note');
} else if (command === 'list') {
  console.log('Listing all notes');
} else if (command === 'read') {
  console.log('Reading note');
} else if (command === 'remove') {
  console.log('Removing note');
} else {
  console.log('Command not recognized');
}

yargs.argv模块是 yargs 库存储应用程序运行的参数版本的地方。现在我们可以使用console.log打印它,这将让我们查看process.argvyargs.argv变量;我们还可以比较它们,看看 yargs 的不同之处。对于使用console.log打印process.argv的命令,我将把第一个参数命名为Process,这样我们就可以在终端中区分它。我们将再次调用console.log。第一个参数将是Yargs字符串,第二个参数将是来自 yargs 的实际argv变量:

console.log('Starting app.js');

const fs = require('fs');
const _ = require('lodash');
const yargs = require('yargs');

const notes = require('./notes.js');

const argv = yargs.argv;
var command = process.argv[2];
console.log('Command:', command);
console.log('Process', process.argv);
console.log('Yargs', argv);

if (command === 'add') {
  console.log('Adding new note');
} else if (command === 'list') {
  console.log('Listing all notes');
} else if (command === 'read') {
  console.log('Reading note');
} else if (command === 'remove') {
  console.log('Removing note');
} else {
  console.log('Command not recognized');
}

现在我们可以以几种不同的方式运行我们的应用程序(参考前面的代码块),看看这两个console.log语句的区别。

首先,我们将使用add命令运行node app.js,我们可以运行这个非常基本的例子:

node app.js add

我们已经从上一章知道了process.argv数组的样子。有用的信息是数组中的第三个字符串,即'add'。在第四个字符串中,Yargs 给了我们一个看起来非常不同的对象:

如前面的代码输出所示,首先是下划线属性,然后存储了 add 等命令。

如果我要添加另一个命令,比如add,然后我要添加一个修饰符,比如encrypted,你会看到add是第一个参数,encrypted是第二个参数,如下所示:

node app.js add encrypted

到目前为止,yargs 并没有表现出色。这并不比我们在上一个例子中拥有的更有用。它真正发挥作用的地方是当我们开始传递键值对时,比如我们在Node 基础知识-第一部分获取输入部分中使用的标题示例中。我可以将我的title标志设置为secrets,按enter,这一次,我们得到了更有用的东西:

node app.js add --title=secrets

在下面的代码输出中,我们有第三个字符串,我们需要解析以获取值和键,而在第四个字符串中,我们实际上有一个带有值为 secrets 的标题属性:

此外,yargs 已经内置了对您可能指定的所有不同方式的解析。

我们可以在title后面插入一个空格,它仍然会像以前一样工作;我们可以在secrets周围添加引号,或者添加其他单词,比如Andrew 的秘密,它仍然会正确解析,将title属性设置为Andrew 的秘密字符串,如下所示:

node app.js add --title "secrets from Andrew"

这就是 yargs 真正发挥作用的地方!它使解析参数的过程变得更加容易。这意味着在我们的应用程序中,我们可以利用这种解析并调用适当的函数。

使用 add 命令

让我们以add命令为例,解析您的参数并调用函数。一旦调用add命令,我们希望调用notes中定义的一个函数,这个函数将负责实际添加笔记。notes.addNote函数将完成这项工作。现在,我们想要传递给addNote函数什么?我们想要传递两件事:标题,可以在argv.title上访问,就像我们在前面的例子中看到的那样;和正文,argv.body

console.log('Starting app.js');

const fs = require('fs');
const _ = require('lodash');
const yargs = require('yargs');

const notes = require('./notes.js');

const argv = yargs.argv;
var command = process.argv[2];
console.log('Command:', command);
console.log('Process', process.argv);
console.log('Yargs', argv);

if (command === 'add') {
  console.log('Adding new note');
  notes.addNote(argv.title, argv.body);
} else if (command === 'list') {
  console.log('Listing all notes');
} else if (command === 'read') {
  console.log('Reading note');
} else if (command === 'remove') {
  console.log('Removing note');
} else {
  console.log('Command not recognized');
}

目前,这些命令行参数titlebody并不是必需的。因此从技术上讲,用户可以在没有其中一个的情况下运行应用程序,这将导致应用程序崩溃,但在未来,我们将要求这两者都是必需的。

现在我们已经放置了notes.addNote,我们可以删除我们之前的console.log语句,这只是一个占位符,然后我们可以进入笔记应用程序notes.js

notes.js中,我们将通过创建一个与我们在app.js中使用的方法同名的变量来开始,然后将其设置为一个匿名箭头函数,如下所示:

var addNote = () => {

};

但是,仅仅这样还不太有用,因为我们没有导出addNote函数。在变量下面,我们可以以稍微不同的方式定义module.exports。在之前的部分中,我们添加属性到exports来导出它们。我们实际上可以定义一个整个对象,将其设置为exports,在这种情况下,我们可以将addNote设置为前面代码块中定义的addNote函数:

module.exports = {
  addNote: addNote
};

在 ES6 中,实际上有一个快捷方式。当你设置一个对象属性和一个变量的值,它们都完全相同时,你可以省略冒号和值。无论哪种方式,结果都是相同的。

在前面的代码中,我们将一个对象设置为module.exports,这个对象有一个属性addNote,指向我们在前面的代码块中定义的addNote函数的变量。

再次强调,在 ES6 中,addNote:addNote在 ES6 内部是相同的。我们将在本书中始终使用 ES6 语法。

现在我可以拿到我的两个参数,titlebody,并且实际上对它们做一些事情。在这种情况下,我们将调用console.logAdding note,将两个参数作为console.log的第二个和第三个参数传递进去,titlebody,如下所示:

var addNote = (title, body) => {
  console.log('Adding note', title, body);
};

现在我们处于一个非常好的位置,可以使用titlebody运行add命令,并查看我们是否得到了我们期望的结果,也就是前面代码中显示的console.log语句。

在终端中,我们可以通过node app.js运行应用程序,然后指定文件名。我们将使用add命令;这将运行适当的函数。然后,我们将传入title,将其设置为secret,然后我们可以传入body,这将是我们的第二个命令行参数,将其设置为字符串This is my secret

node app.js add --title=secret --body="This is my secret"

在这个命令中,我们指定了三件事:add命令,title参数,设置为secret;和body参数,设置为"This is my secret"。如果一切顺利,我们将得到适当的日志。让我们运行这个命令。

在下面的命令输出中,你可以看到Adding note secret,这是标题;和This is my secret,这是正文:

有了这个,我们现在有了一个设置好并准备好的方法。我们接下来要做的是转换我们拥有的其他命令——listreadremove命令。让我们再看一个命令,然后你可以自己练习另外两个。

使用list命令

现在,使用list命令,我将删除console.log语句并调用notes.getAll,如下所示:

console.log('Starting app.js');

const fs = require('fs');
const _ = require('lodash');
const yargs = require('yargs');

const notes = require('./notes.js');

const argv = yargs.argv;
var command = process.argv[2];
console.log('Command:', command);
console.log('Process', process.argv);
console.log('Yargs', argv);

if (command === 'add') {
  notes.addNote(argv.title, argv.body);
} else if (command === 'list') {
  notes.getAll();
} else if (command === 'read') {
  console.log('Reading note');
} else if (command === 'remove') {
  console.log('Removing note');
} else {
  console.log('Command not recognized');
}

在某个时候,notes.getAll将返回所有的笔记。现在,getAll不需要任何参数,因为它将返回所有的笔记,而不管标题是什么。read命令将需要一个标题,remove也将需要你想要删除的笔记的标题。

现在,我们可以创建getAll函数。在notes.js中,我们将再次进行这个过程。我们将首先创建一个变量,称之为getAll,并将其设置为一个箭头函数,这是我们之前使用过的。我们从我们的参数list开始,然后设置箭头(=>),这是等号和大于号。接下来,我们指定我们想要运行的语句。在我们的代码块中,我们将运行console.log(Getting all notes),如下所示:

var getAll = () => {
  console.log('Getting all notes');
};

在添加分号之后的最后一步是将getAll添加到exports中,如下面的代码块所示:

module.exports = {
  addNote,
  getAll
};

请记住,在 ES6 中,如果你有一个属性的名称与值相同,这个值是一个变量,你可以简单地删除值变量和冒号。

现在我们在notes.js中有了getAll,并且在app.js中已经连接好了,我们可以在终端中运行。在这种情况下,我们将运行list命令:

node app.js list

在前面的代码输出中,你可以看到屏幕上打印出了Getting all notes。现在我们已经有了这个,我们可以从app.jscommand变量中删除console.log('Process', process.argv)。结果代码将如下所示:

console.log('Starting app.js');

const fs = require('fs');
const _ = require('lodash');
const yargs = require('yargs');

const notes = require('./notes.js');

const argv = yargs.argv;
var command = process.argv[2];
console.log('Command:', command);
console.log('Yargs', argv);

if (command === 'add') {
  notes.addNote(argv.title, argv.body);
} else if (command === 'list') {
  notes.getAll();
} else if (command === 'read') {
  console.log('Reading note');
} else if (command === 'remove') {
  console.log('Removing note');
} else {
  console.log('Command not recognized');
}

我们将保留 yargs 日志,因为我们将在本章中探索其他使用 yargs 的方法和方式。

现在我们已经有了list命令,接下来,我想让你为readremove命令创建一个方法。

读取命令

当使用read命令时,我们希望调用notes.getNote,传入title。现在,title将被传递并使用 yargs 进行解析,这意味着我们可以使用argv.title来获取它。这就是在调用函数时所需要做的一切:

console.log('Starting app.js');

const fs = require('fs');
const _ = require('lodash');
const yargs = require('yargs');

const notes = require('./notes.js');

const argv = yargs.argv;
var command = process.argv[2];
console.log('Command:', command);
console.log('Yargs', argv);

if (command === 'add') {
  notes.addNote(argv.title, argv.body);
} else if (command === 'list') {
  notes.getAll();
} else if (command === 'read') {
  notes.getNote(argv.title);
} else if (command === 'remove') {
  console.log('Removing note');
} else {
  console.log('Command not recognized');
}

下一步是定义getNote,因为目前它并不存在。在notes.js中,在getAll变量的下面,我们可以创建一个名为getNote的变量,它将是一个函数。我们将使用箭头函数,并且它将接受一个参数;它将接受note的标题。getNote函数接受标题,然后返回该笔记的内容:

var getNote = (title) => {

};

getNote中,我们可以使用console.log打印一些类似于Getting note的内容,后面跟着你将要获取的笔记的标题,这将是console.log的第二个参数:

var getNote = (title) => {
  console.log('Getting note', title);
};

这是第一个命令,我们现在可以在继续第二个命令remove之前进行测试。

在终端中,我们可以使用node app.js来运行文件。我们将使用新的read命令,传入一个title标志。我将使用不同的语法,其中title被设置为引号外的值。我将使用类似accounts的东西:

node app.js read --title accounts

这个accounts值将在将来读取accounts笔记,并将其打印到屏幕上,如下所示:

如你在前面的代码输出中所看到的,我们得到了一个错误,现在我们将对其进行调试。

处理解析命令中的错误

遇到错误并不是世界末日。通常出现错误意味着你可能有一个小的拼写错误或者在过程中忘记了一步。所以,我们首先要弄清楚如何解析这些错误消息,因为代码输出中得到的错误消息可能会让人望而生畏。让我们来看一下代码输出中的错误:

正如你所看到的,第一行显示了错误发生的位置。它在我们的app.js文件中,冒号后面的数字 19 是行号。它准确地告诉你事情出了问题的地方。TypeError: notes.getNote is not a function行清楚地告诉你你尝试运行的getNote函数不存在。现在我们可以利用这些信息来调试我们的应用程序。

app.js中,我们看到我们调用了notes.getNote。一切看起来很好,但当我们进入notes.js时,我们意识到我们实际上从未导出getNote。这就是为什么当我们尝试调用该函数时,我们会得到getNote is not a function。我们只需要做的就是导出getNote,如下所示:

module.exports = {
  addNote,
  getAll,
  getNote
};

现在当我们保存文件并从终端重新运行应用程序时,我们将得到我们期望的结果——Getting note后面跟着标题,这里是accounts

这就是我们如何调试我们的错误消息。错误消息包含非常有用的信息。在大多数情况下,前几行是你编写的代码,其他行是内部 Node 代码或第三方模块。在我们的情况下,堆栈跟踪的第一行很重要,因为它准确地显示了错误发生的位置。

删除命令

现在,由于read命令正在工作,我们可以继续进行最后一个命令remove。在这里,我将调用notes.removeNote,传入标题,正如我们所知道的在argv.title中可用:

console.log('Starting app.js');

const fs = require('fs');
const _ = require('lodash');
const yargs = require('yargs');

const notes = require('./notes.js');

const argv = yargs.argv;
var command = process.argv[2];
console.log('Command:', command);
console.log('Yargs', argv);

if (command === 'add') {
  notes.addNote(argv.title, argv.body);
} else if (command === 'list') {
  notes.getAll();
} else if (command === 'read') {
  notes.getNote(argv.title);
} else if (command === 'remove') {
  notes.removeNote(argv.title);
} else {
  console.log('Command not recognized');
}

接下来,我们将在我们的笔记 API 文件中定义 removeNote 函数,就在 getNote 变量的下面:

var removeNote = (title) => { 
 console.log('Removing note', title);
};

现在,removeNote 将与 getNote 工作方式基本相同。它只需要标题;它可以使用这些信息来查找笔记并从数据库中删除它。这将是一个接受 title 参数的箭头函数。

在这种情况下,我们将打印 console.log 语句 Removing note;然后,作为第二个参数,我们将简单地打印 title 回到屏幕上,以确保它成功地通过了这个过程。这一次,我们将导出我们的 removeNote 函数;我们将使用 ES6 语法来定义它:

module.exports = {
  addNote,
  getAll,
  getNote,
  removeNote
};

最后要做的事情是测试它并确保它有效。我们可以使用上箭头键重新加载上一个命令。我们将 read 改为 remove,这就是我们需要做的全部。我们仍然传入 title 参数,这很好,因为这是 remove 需要的:

node app.js remove --title accounts

当我运行这个命令时,我们得到了我们预期的结果。移除笔记打印到屏幕上,如下面的代码输出所示,然后我们得到了我们应该移除的笔记的标题,即 accounts:

看起来很棒!这就是使用 yargs 解析你的参数所需的全部内容。

有了这个,我们现在有了一个地方来定义所有这些功能,用于保存、读取、列出和删除笔记。

获取命令

在我们结束本节之前,我想讨论的最后一件事是——我们如何获取 command

正如我们所知,command_ 属性中作为第一个且唯一的项可用。这意味着在 app.js 中,var command 语句中,我们可以将 command 设置为 argv,然后 ._,然后我们将使用 [] 来抓取数组中的第一个项目,如下面的代码所示:

console.log('Starting app.js');

const fs = require('fs');
const _ = require('lodash');
const yargs = require('yargs');

const notes = require('./notes.js');

const argv = yargs.argv;
var command = argv._[0];
console.log('Command:', command);
console.log('Yargs', argv);

if (command === 'add') {
  notes.addNote(argv.title, argv.body);
} else if (command === 'list') {
  notes.getAll();
} else if (command === 'read') {
  notes.getNote(argv.title);
} else if (command === 'remove') {
  notes.removeNote(argv.title);
} else {
  console.log('Command not recognized');
}

有了这个功能,我们现在有了相同的功能,但我们将在所有地方使用 yargs。如果我重新运行上一个命令,我们可以测试功能是否仍然有效。它确实有效!如下面的命令输出所示,我们可以看到命令:remove 显示出来:

接下来,我们将研究填写各个函数。我们首先来看一下如何使用 JSON 将我们的笔记存储在文件系统中。

JSON

现在你知道如何使用 process.argv 和 yargs 解析命令行参数,你已经解决了 notes 应用程序的第一部分难题。现在,我们如何从用户那里获取独特的输入呢?解决这个难题的第二部分是解决我们如何存储这些信息。

当有人添加新的笔记时,我们希望将其保存在某个地方,最好是在文件系统上。所以下次他们尝试获取、移除或读取该笔记时,他们实际上会得到该笔记。为了做到这一点,我们需要引入一个叫做 JSON 的东西。如果你已经熟悉 JSON,你可能知道它非常受欢迎。它代表 JavaScript 对象表示法,是一种用字符串表示 JavaScript 数组和对象的方法。那么,为什么你会想要这样做呢?

嗯,你可能想这样做是因为字符串只是文本,而且几乎在任何地方都得到支持。我可以将 JSON 保存到文本文件中,然后稍后读取它,将其解析回 JavaScript 数组或对象,并对其进行操作。这正是我们将在本节中看到的。

为了探索 JSON 以及它的工作原理,让我们继续在我们的项目中创建一个名为 playground 的新文件夹。

在整本书中,我将创建 playground 文件夹和各种项目,这些项目存储简单的一次性文件,不是更大应用程序的一部分;它们只是探索新功能或学习新概念的一种方式。

playground 文件夹中,我们将创建一个名为 json.js 的文件,这是我们可以探索 JSON 工作原理的地方。让我们开始,让我们创建一个非常简单的对象。

将对象转换为字符串

首先,让我们创建一个名为obj的变量,将其设置为一个对象。在这个对象上,我们只定义一个属性name,并将其设置为你的名字;我将这个属性设置为Andrew,如下所示:

var obj = {
  name: 'Andrew'
};

现在,假设我们想要获取这个对象并对其进行操作。例如,我们想要将其作为字符串在服务器之间发送并保存到文本文件中。为此,我们需要调用一个 JSON 方法。

让我们定义一个变量来存储结果stringObj,并将其设置为JSON.stringify,如下所示:

var stringObj = JSON.stringify(obj);

JSON.stringify方法接受你的对象,这里是obj变量,并返回 JSON 字符串化的版本。这意味着存储在stringObj中的结果实际上是一个字符串。它不再是一个对象,我们可以使用console.log来查看。我将使用console.log两次。首先,我们将使用typeof运算符打印字符串对象的类型,以确保它实际上是一个字符串。由于typeof是一个运算符,它以小写形式输入,没有驼峰命名法。然后,传入要检查其类型的变量。接下来,我们可以使用console.log来打印字符串本身的内容,打印stringObj变量,如下所示:

console.log(typeof stringObj);
console.log(stringObj);

我们在这里所做的是将一个对象转换为 JSON 字符串,并将其打印到屏幕上。在终端中,我将使用以下命令导航到playground文件夹中:

cd playground

现在,无论你在哪里运行命令都无所谓,但在将来当我们在playground文件夹中时,这将很重要,所以花点时间进入其中。

现在,我们可以使用node来运行我们的json.js文件。运行文件时,我们会看到两件事:

如前面的代码输出所示,首先我们会得到我们的类型,它是一个字符串,这很好,因为记住,JSON 是一个字符串。接下来,我们将得到我们的对象,它看起来与 JavaScript 对象非常相似,但有一些区别。这些区别如下:

  • 首先,你的 JSON 将自动用双引号包裹其属性名称。这是 JSON 语法的要求。

  • 接下来,你会注意到你的字符串也被双引号包裹,而不是单引号。

现在,JSON 不仅支持字符串值,还可以使用数组、布尔值、数字或其他任何类型。所有这些类型在你的 JSON 中都是完全有效的。在这种情况下,我们有一个非常简单的示例,其中有一个name属性,它设置为"Andrew"

这是将对象转换为字符串的过程。接下来,我们将定义一个字符串,并将其转换为我们可以在应用程序中实际使用的对象。

定义一个字符串并在应用程序中使用

让我们开始创建一个名为personString的变量,并将其设置为一个字符串,使用单引号,因为 JSON 在其内部使用双引号,如下所示:

var personString = '';

然后我们将在引号中定义我们的 JSON。我们将首先打开和关闭一些花括号。我们将使用双引号创建我们的第一个属性,我们将其称为name,并将该属性设置为Andrew。这意味着在闭合引号之后,我们将添加:;然后我们将再次打开和关闭双引号,并输入值Andrew,如下所示:

var personString = '{"name": "Andrew"}';

接下来,我们可以添加另一个属性。在值Andrew之后,我将在逗号后创建另一个属性,称为age,并将其设置为一个数字。我可以使用冒号,然后定义数字而不使用引号,例如25

var personString = '{"name": "Andrew","age": 25}';

你可以继续使用你的名字和年龄,但确保其余部分看起来与这里看到的完全相同。

现在,假设我们从服务器获取了先前定义的 JSON,或者我们从文本文件中获取了它。目前它是无用的;如果我们想获取name值,没有好的方法可以做到,因为我们使用的是一个字符串,所以personString.name不存在。我们需要将字符串转换回对象。

将字符串转换回对象

要将字符串转换回对象,我们将使用JSON.stringify的相反操作,即JSON.parse。让我们创建一个变量来存储结果。我将创建一个person变量,并将其设置为JSON.parse,传入作为唯一参数要解析的字符串,即person字符串,我们之前定义过的:

var person = JSON.parse(personString);

现在,这个变量将把你的 JSON 从字符串转换回其原始形式,可以是数组或对象。在我们的情况下,它将其转换回对象,并且我们有person变量作为对象,如前面的代码所示。此外,我们可以使用typeof运算符证明它是一个对象。我将使用console.log两次,就像我们之前做过的那样。

首先,我们将打印persontypeof,然后我们将打印实际的person变量,console.log(person)

console.log(typeof person);
console.log(person);

有了这个,我们现在可以在终端中重新运行命令;我将实际启动nodemon并传入json.js

nodemon json.js 

如下所示的代码输出,您现在可以看到我们正在使用一个对象,这很棒,我们有我们的常规对象:

我们知道Andrew是一个对象,因为它没有用双引号包裹;值没有引号,我们使用单引号Andrew,这在 JavaScript 中是有效的,但在 JSON 中是无效的。

这是将对象转换为字符串,然后将字符串转换回对象的整个过程,这正是我们将在notes应用程序中做的。唯一的区别是,我们将取以下字符串并将其存储在文件中,然后稍后,我们将使用JSON.parse从文件中读取该字符串,将其转换回对象,如下面的代码块所示:

// var obj = {
//  name: 'Andrew'
// };
// var stringObj = JSON.stringify(obj);
// console.log(typeof stringObj);
// console.log(stringObj);

var personString = '{"name": "Andrew","age": 25}';
var person = JSON.parse{personString};
console.log(typeof person);
console.log(person);

将字符串存储在文件中

基本知识已经就位,让我们再进一步,也就是将字符串存储在文件中。然后,我们希望使用fs模块读取该文件的内容,并打印一些属性。这意味着我们需要将从fs.readfilesync获取的字符串转换为对象,使用JSON.parse

在 playground 文件夹中写入文件

让我们继续注释掉到目前为止的所有代码,从干净的板上开始。首先,让我们加载fs模块。const变量fs将被设置为require,我们将传递过去使用过的fs模块,如下所示:

// var obj = {
//  name: 'Andrew'
// };
// var stringObj = JSON.stringify(obj);
// console.log(typeof stringObj);
// console.log(stringObj);

// var personString = '{"name": "Andrew","age": 25}';
// var person = JSON.parse(personString);
// console.log(typeof person);
// console.log(person);

const fs = require('fs');

接下来要做的是定义对象。这个对象将被存储在我们的文件中,然后将被读取并解析。这个对象将是一个名为originalNote的变量,我们将称它为originalNote,因为后来,我们将重新加载它并将该变量称为Note

现在,originalNote将是一个常规的 JavaScript 对象,有两个属性。我们将有title属性,将其设置为Some title,和body属性,将其设置为Some body,如下所示:

var originalNote = {
  title: 'Some title',
  body: 'Some body'
};

您需要做的下一步是获取原始注释并创建一个名为originalNoteString的变量,并将该变量设置为我们之前定义的对象的 JSON 值。这意味着您需要使用我们在本节先前使用过的两种 JSON 方法之一。

现在,一旦你有了originalNoteString变量,我们就可以将文件写入文件系统。我会为你写下这一行,fs.writeFileSync。我们之前使用的writeFileSync方法需要两个参数。一个是文件名,由于我们使用的是 JSON,使用 JSON 文件扩展名很重要。我会把这个文件叫做notes.json。另一个参数将是文本内容,originalNoteString,它还没有被定义,如下面的代码块所示:

// originalNoteString
fs.writeFileSync('notes.json', originalNoteString);

这是整个过程的第一步;这是我们将文件写入playground文件夹的方法。下一步是读取内容,使用之前的 JSON 方法进行解析,并打印其中一个属性到屏幕上,以确保它是一个对象。在这种情况下,我们将打印标题。

读取文件中的内容

打印标题的第一步是使用我们尚未使用过的方法。我们将使用文件系统模块上可用的read方法来读取内容。让我们创建一个名为noteString的变量。noteString变量将被设置为fs.readFileSync

现在,readFileSyncwriteFileSync类似,只是它不需要文本内容,因为它会为你获取文本内容。在这种情况下,我们只需指定第一个参数,即文件名notes.JSON

var noteString = fs.readFileSync('notes.json');

现在我们有了字符串,你的工作就是拿到那个字符串,使用前面的方法之一,将它转换回对象。你可以将那个变量叫做note。接下来,唯一剩下的事情就是测试一切是否按预期工作,通过使用console.log(typeof note)来打印。然后,在这之下,我们将使用console.log来打印标题,note.title

// note
console.log(typeof note);
console.log(note.title);

现在,在终端中,你可以看到(参考下面的截图),我保存了一个损坏的文件,并且它崩溃了,这是使用nodemon时预期的结果:

为了解决这个问题,我要做的第一件事是填写originalNoteString变量,这是我们之前注释掉的。现在它将成为一个名为originalNoteString的变量,并且我们将把它设置为JSON.stringify的返回值。

现在,我们知道JSON.stringify将我们的普通对象转换为字符串。在这种情况下,我们将把originalNote对象转换为字符串。下一行,我们已经填写好了,将保存该 JSON 值到notes.JSON文件中。然后我们将读取该值出来:

var originalNoteString = JSON.stringify(originalNote);

下一步将是创建note变量。note变量将被设置为JSON.parse

JSON.parse方法将字符串 JSON 转换回普通的 JavaScript 对象或数组,取决于你保存的内容。在这里,我们将传入noteString,这是我们从文件中获取的:

var note = JSON.parse(noteString);

有了这个,我们现在完成了。当我保存这个文件时,nodemon将自动重新启动,我们不会看到错误。相反,我们期望看到对象类型以及笔记标题。在终端中,我们有对象和一些标题打印到屏幕上:

有了这个,我们已经成功完成了挑战。这正是我们将保存我们的笔记的方法。

当有人添加新的笔记时,我们将使用以下代码来保存它:

var originalNote = {
  title: 'Some title',
  body: 'Some body'
};
var originalNoteString = JSON.stringify(originalNote);
fs.writeFileSync('notes.json', originalNoteString);

当有人想要阅读他们的笔记时,我们将使用以下代码来读取它:

var noteString = fs.readFileSync('notes.json');
var note = JSON.parse(noteString);
console.log(typeof note);
console.log(note.title);

现在,如果有人想要添加一条笔记呢?这将要求我们首先读取所有的笔记,然后修改笔记数组,然后使用代码(参考前面的代码块)将新数组保存回文件系统中。

如果你打开notes.JSON文件,你可以看到我们的 JSON 代码就在文件中:

.json实际上是大多数文本编辑器支持的文件格式,因此我已经内置了一些不错的语法高亮。现在,在下一节中,我们将填写addNote函数,使用刚刚在本节中使用的完全相同的逻辑。

添加和保存笔记

在上一节中,您学习了如何在 Node.js 中处理 JSON,这是我们将在notes.js应用程序中使用的确切格式。当您首次运行命令时,我们将加载可能已经存在的所有笔记。然后我们将运行命令,无论是添加、删除还是阅读笔记。最后,如果我们已经更新了数组,就像我们在添加和删除笔记时所做的那样,我们将这些新的笔记保存回 JSON 文件中。

现在,所有这些将发生在我们在notes.js应用程序中定义的addNote函数内部,我们已经连接了这个函数。在之前的部分中,我们运行了add命令,这个函数执行了titlebody参数。

添加笔记

要开始添加笔记,我们要做的第一件事是创建一个名为notes的变量,暂时将其设置为空数组,就像下面这样使用我们的方括号:

var addNote = (title, body) => {
  var notes = [];
};

现在我们有了空数组,我们可以继续创建一个名为note的变量,这是单个笔记。这将代表新的笔记:

var addNote = (title, body) => {
  var notes = [];
  var note = {

  }
};

在这一点上,我们将有两个属性:一个title和一个body。现在,title可以设置为title变量,但是,正如我们所知,在 ES6 中,当两个值相同时,我们可以简单地将其删除;因此,我们将添加titlebody如下所示:

var addNote = (title, body) => {
  var notes = [];
  var note = {
    title,
    body
  };
};

现在我们有了notenotes数组。

将笔记添加到笔记数组中

添加笔记过程中的下一步将是将note添加到notes数组中。notes.push方法将让我们做到这一点。数组上的push方法允许您传入一个项目,该项目将被添加到数组的末尾,在这种情况下,我们将传入note对象。因此,我们有一个空数组,并且我们添加了一个项目,如下面的代码所示;接下来,我们将其推入,这意味着我们有一个包含一个项目的数组:

var addNote = (title, body) => {
  var notes = [];
  var note = {
    title,
    body
  };

  notes.push(note);
};

下一步将是更新文件。现在,我们没有文件,但我们可以加载一个fs函数并开始创建文件。

addNote函数的上面,让我们加载fs模块。我将创建一个名为fsconst变量,并将其设置为require的返回结果,并且我们将要求fs模块,这是一个核心的 node 模块,因此不需要使用 NPM 安装它:

const fs = require('fs');

有了这个,我们可以在addNote函数内部利用fs

在我们将项目推入notes数组之后,我们将调用fs.writeFileSync,这是我们以前使用过的。我们知道我们需要传入两件事:文件名和我们想要保存的内容。对于文件,我将调用notes-data.JSON,然后我们将传入要保存的内容,这种情况下将是stringify notes 数组,这意味着我们可以调用JSON.stringify传入notes

notes.push(note);
fs.writeFileSync('notes-data.json', JSON.stringify(notes));

我们本可以将JSON.stringfy(notes)拆分为自己的变量,并在上面的语句中引用该变量,但由于我们只会在一个地方使用它,我认为这是更好的解决方案。

在这一点上,当我们添加一个新的笔记时,它将更新notes-data.JSON文件,该文件将在机器上创建,因为它不存在,并且笔记将位于其中。现在,重要的是要注意,当前每次添加新的笔记时,它将擦除所有现有的笔记,因为我们从未加载现有的笔记,但我们可以开始测试这个笔记是否按预期工作。

我将保存文件,在终端内部,我们可以使用node app.js运行这个文件。因为我们想要添加一个note,我们将使用我们设置的add命令,然后我们将指定我们的标题和正文。title标志可以设置为secret,对于body标志,我将把它设置为Some body here字符串,如下所示:

node app.js add --title=secret --body="Some body here"

现在,当我们从终端运行这个命令时,我们将看到我们所期望的结果:

如前面的屏幕截图所示,我们看到了我们添加的一些文件命令:我们看到add命令被执行了,并且我们有我们的 Yargs 参数。标题和正文参数也显示出来了。在 Atom 中,我们还看到了一个新的notes-data.json文件,在下面的屏幕截图中,我们有我们的笔记,有secret标题和Some body here正文:

这是连接addNote函数的第一步。我们有一个现有的notes文件,我们确实希望利用这些笔记。如果笔记已经存在,我们不希望每次有人添加新笔记时都将它们简单地清除。这意味着在notes.js中,在addNote函数的开头,我们将获取这些笔记。

获取新笔记

我将添加获取新笔记的代码,在那里我定义了notesnote变量。如下面的代码所示,我们将使用fs.readFileSync,这是我们已经探索过的。这将获取文件名,在我们的情况下是notes-data.JSON。现在,我们将希望将readFileSync的返回值存储在一个变量上;我将称这个变量为notesString

var notesString = fs.readFileSync('notes-data.json');

由于这是字符串版本,我们还没有通过JSON.parse方法传递它。因此,我可以将notes(我们在addNote函数中之前定义的变量)设置为JSON.parse方法的返回值。然后JSON.parse将获取我们从文件中读取的字符串,并将其解析为一个数组;我们可以像这样传递notesString

notes = JSON.parse(notesString);

有了这个,添加新的笔记将不再删除已经存在的所有笔记。

在终端中,我将使用上箭头键加载上一个命令,并导航到title标志,将其更改为secret2,然后重新运行命令:

node app.js add --title=secret2 --body="Some body here"

在 Atom 中,这次你可以看到我们的文件中现在有两个笔记:

我们有一个包含两个对象的数组;第一个对象的标题是secret,第二个对象的标题是secret2,这太棒了!

尝试和捕获代码块

现在,如果notes-data.json文件不存在,当用户第一次运行命令时,程序将崩溃,如下面的代码输出所示。我们可以通过简单地删除note-data.JSON文件后重新运行上一个命令来证明这一点:

在这里,你可以看到我们实际上遇到了一个 JavaScript 错误,没有这样的文件或目录;它试图打开notes-data.JSON文件,但并不成功。为了解决这个问题,我们将使用 JavaScript 中的try-catch语句,希望你之前已经见过。为了快速复习一下,让我们来看一下。

要创建一个try-catch语句,你所要做的就是输入try,这是一个保留关键字,然后打开和关闭一对花括号。花括号内部是将要运行的代码。这是可能会或可能不会抛出错误的代码。接下来,你将指定catch块。现在,catch块将带有一个参数,一个错误参数,并且还有一个将运行的代码块:

try{

} catch (e) {

}

只有在try中的一个错误实际发生时,此代码才会运行。因此,如果我们使用readFileSync加载文件并且文件存在,那就没问题,catch块将永远不会运行。如果失败,catch块将运行,我们可以做一些事情来从错误中恢复。有了这个,我们将把noteString变量和JSON.parse语句移到try中,如下所示:

try{
  var notesString = fs.readFileSync('notes-data.json');
  notes = JSON.parse(notesString);
} catch (e) {

}

就是这样;不需要发生其他任何事情。我们不需要在catch中放任何代码,尽管您需要定义catch块。现在,让我们看看运行整个代码时会发生什么。

首先发生的事情是我们创建静态变量——没有什么特别的——然后我们尝试加载文件。如果notesString函数失败,那没关系,因为我们已经定义notes为空数组。如果文件不存在并且加载失败,那么我们可能希望notes为空数组,因为显然没有notes,也没有文件。

接下来,我们将把数据解析成 notes。如果notes-data.JSON文件中有无效数据,这两行可能会失败。通过将它们放在try-catch中,我们基本上保证程序不会出现意外情况,无论文件是否存在,但包含损坏的数据。

有了这个,我们现在可以保存notes并重新运行之前的命令。请注意,我没有放置notes-data文件。当我运行命令时,我们没有看到任何错误,一切似乎都按预期运行:

当您现在访问 Atom 时,您会发现notes-data文件确实存在,并且其中的数据看起来很棒:

这就是我们需要做的一切,获取 notes,使用新 note 更新 notes,最后将 notes 保存到屏幕上。

现在,addNote还存在一个小问题。目前,addNote允许重复的标题;我可以在 JSON 文件中已经有一个标题为secret的 note。我可以尝试添加一个标题为secret的新 note,它不会抛出错误。我想要做的是使标题唯一,这样如果已经有一个具有该标题的 note,它将抛出错误,让您知道需要使用不同的标题创建 note。

使标题唯一

使标题唯一的第一步是在加载 note 后循环遍历所有 note,并检查是否有任何重复项。如果有重复项,我们将不调用以下两行:

notes.push(note);
fs.writeFileSync('notes-data.json', JSON.stringify(notes));

如果没有重复项,那就没问题,我们将调用前面代码块中显示的两行,更新notes-data文件。

现在,我们将在以后重构这个函数。事情变得有点混乱,有点失控,但目前,我们可以将这个功能直接添加到函数中。让我们继续并创建一个名为duplicateNotes的变量。

duplicateNotes变量最终将存储一个数组,其中包含notes数组中已经存在的具有您尝试创建的 note 标题的 note。现在,这意味着如果duplicateNotes数组有任何项目,那就不好。这意味着该 note 已经存在,我们不应该添加该 note。duplicateNotes变量将被设置为对notes的调用,这是我们的notes.filter数组:

var duplicateNotes = notes.filter();

filter方法是一个接受回调的数组方法。我们将使用箭头函数,回调将使用参数调用。在这种情况下,它将是单数形式;如果我有一个 notes 数组,它将被调用为一个单独的 note:

var duplicateNotes = notes.filter((note) => {

});

这个函数会为数组中的每个项目调用一次,并且你有机会返回 true 或 false。如果你返回 true,它会保留数组中的那个项目,最终会保存到duplicateNotes中。如果你返回 false,它生成的新数组就不会包含duplicateNotes变量中的那个项目。我们只需要在标题匹配时返回 true,这意味着我们可以返回note.title === title,如下所示:

var duplicateNotes = notes.filter((note) => {
  return note.title === title;
});

如果标题相等,那么前面的return语句将返回 true,并且该项目将保留在数组中,这意味着有重复的笔记。如果标题不相等,这很可能是情况,那么该语句将返回 false,这意味着没有重复的笔记。现在,我们可以使用箭头函数来简化一下。

箭头函数实际上允许你在只有一个语句的情况下删除花括号。

我将使用箭头函数,如下所示:

var duplicateNotes = notes.filter((note) => note.title === title);

在这里,我已经删除了除了note.title === title之外的所有内容,并在箭头函数语法的前面添加了这个。

这在 ES6 箭头函数中是完全有效的。你的参数在左边,箭头在中间,右边是一个表达式。这个表达式不需要分号,它会自动返回作为函数结果。这意味着我们这里的代码与之前的代码是相同的,只是更简单,而且只占用一行。

现在我们有了这个设置,我们可以继续检查duplicateNotes变量的长度。如果duplicateNotes的长度大于0,这意味着我们不想保存这个笔记,因为已经存在一个具有相同标题的笔记。如果它是0,我们将保存这个笔记。

if(duplicateNotes.length === 0) {

}

在这里,在if条件内部,我们正在将笔记的长度与数字 0 进行比较。如果它们相等,那么我们确实想要将笔记推送到notes数组中并保存文件。我将删除以下两行:

notes.push(note);
fs.writeFileSync('notes-data.json', JSON.stringify(notes));

让我们把它们粘贴到if语句的内部,如下所示:

if(duplicateNotes.length === 0) {
  notes.push(note);
  fs.writeFileSync('notes-data.json', JSON.stringify(notes));
}

如果它们不相等,也没关系;在这种情况下,我们将什么也不做。

有了这个设置,我们现在可以保存我们的文件并测试这个功能。我们有我们的notes-data.json文件,这个文件已经有一个标题为secret2的笔记。让我们重新运行之前的命令,尝试添加一个具有相同标题的新笔记:

node app.js add --title=secret2 --body="Some body here"

你现在在终端中,所以我们将回到我们的 JSON 文件。你可以看到,我们仍然只有一个笔记:

现在我们应用程序中的所有标题都将是唯一的,所以我们可以使用这些标题来获取和删除笔记。

让我们继续测试其他笔记是否仍然可以添加。我将把title标志从secret2改为secret,然后运行该命令:

node app.js add --title=secret --body="Some body here"

在我们的notes-data文件中,你可以看到两个笔记都显示出来:

正如我之前提到的,接下来我们将进行一些重构,因为加载文件的代码和保存文件的代码将在我们已经定义和/或将要定义的大多数函数中使用(即getAllgetNoteremoveNote函数)。

重构

在前面的部分中,你创建了addNote函数,它工作得很好。它首先创建一些静态变量,然后我们获取任何现有的笔记,检查重复项,如果没有,我们将其推送到列表上,然后将数据保存回文件系统。

唯一的问题是,我们将不断重复执行这些步骤。例如,对于getAll,我们的想法是获取所有的笔记,并将它们发送回app.js,以便它可以将它们打印到用户的屏幕上。在getAll语句的内部,我们首先要做的是有相同的代码;我们将有我们的try-catch块来获取现有的笔记。

现在,这是一个问题,因为我们将在整个应用程序中重复使用代码。最好将获取笔记和保存笔记拆分为单独的函数,我们可以在多个位置调用这些函数。

将功能移入各个函数

为了解决问题,我想首先创建两个新函数:

  • fetchNotes

  • saveNotes

第一个函数fetchNotes将是一个箭头函数,它不需要接受任何参数,因为它将从文件系统中获取笔记,如下所示:

var fetchNotes = () => {

};

第二个函数saveNotes将需要接受一个参数。它将需要接受要保存到文件系统的notes数组。我们将设置它等于一个箭头函数,然后提供我们的参数,我将其命名为notes,如下所示:

var saveNotes = (notes) => {

};

现在我们有了这两个函数,我们可以开始将一些功能从addNote中移动到各个函数中。

使用 fetchNotes

首先,让我们做fetchNotes,它将需要以下try-catch块。

我将它从addNote中剪切出来,粘贴到fetchNotes函数中,如下所示:

var fetchNotes = () => {
  try{
    var notesString = fs.readFileSync('notes-data.json');
    notes = JSON.parse(notesString);
  } catch (e) {

}
};

仅此还不够,因为目前我们没有从函数中返回任何内容。我们想要做的是返回这些笔记。这意味着我们不会将JSON.parse的结果保存到我们尚未定义的notes变量上,而是简单地将其返回给调用函数,如下所示:

var fetchNotes = () => {
  try{
    var notesString = fs.readFileSync('notes-data.json');
    return JSON.parse(notesString);
  } catch (e) {

}
};

因此,如果我在addNote函数中调用fetchNotes,如下所示,我将得到notes数组,因为在前面的代码中有return语句。

现在,如果没有笔记,可能根本没有文件;或者有一个文件,但数据不是 JSON,我们可以返回一个空数组。我们将在catch中添加一个return语句,如下面的代码块所示,因为请记住,如果try中的任何内容失败,catch就会运行:

var fetchNotes = () => {
  try{
    var notesString = fs.readFileSync('notes-data.json');
    return JSON.parse(notesString);
  } catch (e) {
    return [];
}
};

现在,这让我们进一步简化了addNote。我们可以删除空格,并且可以取出我们在notes变量上设置的数组,并将其删除,而是调用fetchNotes,如下所示:

var addNote = (title, body) => {
  var notes = fetchNotes();
  var note = {
      title,
      body
};

有了这个,我们现在有了与之前完全相同的功能,但是我们有了一个可重用的函数fetchNotes,我们可以在addNote函数中使用它来处理应用程序将支持的其他命令。

我们已经将代码拆分到一个地方,而不是复制代码并将其放在文件的多个位置。如果我们想要更改此功能的工作方式,无论是更改文件名还是一些逻辑,比如try-catch块,我们只需更改一次,而不必更改每个函数中的代码。

使用 saveNotes

现在,saveNotes的情况与fetchNotes函数一样。saveNotes函数将获取notes变量,并使用fs.writeFileSync来保存。我将剪切addNote中执行此操作的行(即fs.writeFileSync('notes-data.json', JSON.stringfy(notes));),并将其粘贴到saveNotes函数中,如下所示:

var saveNotes = (notes) => {
  fs.writeFileSync('notes-data.json', JSON.stringify(notes));
};

现在,saveNotes不需要返回任何内容。在这种情况下,我们将在addNote函数的if语句中复制saveNotes中的行,并调用saveNotes,如下所示:

if (duplicateNotes.length === 0) {
  notes.push(note);
  saveNotes();
}

这可能看起来有点多余,我们实际上是用不同的行替换了一行,但开始养成创建可重用函数的习惯是一个好主意。

现在,如果没有数据调用saveNotes是行不通的,我们想要传入notes变量,这是我们在saveNotes函数中之前定义的notes数组:

if (duplicateNotes.length === 0) {
  notes.push(note);
  saveNotes(notes);
}

有了这个,addNote函数现在应该像我们进行重构之前一样工作。

测试功能

在这个过程中的下一步将是通过创建一个新的笔记来测试这个功能。我们已经有两个笔记,在notes-data.json中有一个标题是secret,一个标题是secret2,让我们使用终端中的node app.js命令来创建第三个笔记。我们将使用add命令并传入一个标题to buy和一个正文food,就像这里显示的那样:

node app.js add --title="to buy" --body="food"

这应该会创建一个新的笔记,如果我运行这个命令,你会看到我们没有任何明显的错误:

在我们的notes-data.json文件中,如果我向右滚动,我们有一个全新的笔记,标题是to buy,正文是food

所以,即使我们重构了代码,一切都按预期工作。现在,我想在addNote中的下一步是花点时间返回正在添加的笔记,这将发生在saveNotes返回之后。所以我们将返回note

if (duplicateNotes.length === 0) {
  notes.push(note);
  saveNotes(notes);
  return note;
}

这个note对象将被返回给调用该函数的人,而在这种情况下,它将被返回给app.js,我们在app.js文件的add命令的if else块中调用它。我们可以创建一个变量来存储这个结果,我们可以称之为note

if (command === 'add')
  var note = notes.addNote(argv.title, argv.body);

如果note存在,那么我们知道笔记已经创建。这意味着我们可以继续打印一条消息,比如Note created,然后我们可以打印note的标题和note的正文。现在,如果note不存在,如果它是未定义的,这意味着有一个重复的标题已经存在。如果是这种情况,我希望你打印一个错误消息,比如Note title already in use

你可以用很多不同的方法来做这个。不过,目标是根据是否返回了笔记来打印两条不同的消息。

现在,在addNote中,如果duplicateNotesif语句从未运行,我们就没有显式调用return。但是你知道,在 JavaScript 中,如果你不调用return,那么undefined会自动返回。这意味着如果duplicateNotes.length不等于零,将返回 undefined,我们可以将其用作我们语句的条件。

首先,我要做的是在我们在app.js中定义的note变量旁边创建一个if语句:

if (command === 'add') {
  var note = notes.addNote(argv.title, argv.body);
  if (note) {

  }

如果事情进展顺利,这将是一个对象,如果事情进展不顺利,它将是未定义的。这里的代码只有在它是一个对象的时候才会运行。在 JavaScript 中,Undefined的结果会使 JavaScript 中的条件失败。

现在,如果笔记成功创建,我们将通过以下console.log语句向屏幕打印一条消息:

if (note) {
  console.log('Note created');
}

如果事情进展不顺利,在else子句中,我们可以调用console.log,并打印一些像Note title taken这样的东西,就像这里显示的那样:

if (note) {
  console.log('Note created');
} else {
  console.log('Note title taken');
}

现在,如果事情进展顺利,我们想要做的另一件事是打印notes的内容。我会首先使用console.log打印几个连字符。这将在我的笔记上方创建一些空间。然后我可以使用console.log两次:第一次我们将打印标题,我会添加Title:作为一个字符串来显示你究竟看到了什么,然后我可以连接标题,我们可以在note.title中访问到,就像这段代码中显示的那样:

if (note) {
  console.log('Note created');
  console.log('--');
  console.log('Title: ' + note.title);

现在,上面的语法使用了 ES5 的语法;我们可以用我们已经讨论过的内容,即模板字符串,来换成 ES6 的语法。我们会添加Title,一个冒号,然后我们可以使用我们的美元符号和花括号来注入note.title变量,就像这里显示的那样:

console.log(`Title: ${note.title}`);

同样地,我会在此之后添加note.body来打印笔记的正文。有了这个,代码应该看起来像这样:

if (command === 'add') {
  var note = note.addNote(argv.title, argv.body);
  if (note) {
    console.log('Note created');
    console.log('--');
    console.log(`Title: ${note.title}`);
    console.log(`Body: ${note.body}`);
  } else {
    console.log('Note title taken');
}

现在,我们应该能够运行我们的应用程序并看到标题和正文笔记都被打印出来。在终端中,我会重新运行之前的命令。这将尝试创建一个已经存在的购买笔记,所以我们应该会得到一个错误消息,你可以在这里看到Note title taken

现在,我们可以重新运行命令,将标题更改为其他内容,比如从商店购买。这是一个独特的note标题,因此笔记应该可以顺利创建:

node app.js add --title="to buy from store" --body="food"

如前面的输出所示,您可以看到我们确实得到了这样的结果:我们有我们的笔记创建消息,我们的小间隔,以及我们的标题和正文。

addNote命令现在已经完成。当命令实际完成时,我们会得到一个输出,并且我们有所有在后台运行的代码,将笔记添加到存储在我们文件中的数据中。

总结

在本章中,您了解到在process.argv中解析可能会非常痛苦。我们将不得不编写大量手动代码来解析那些连字符、等号和可选引号。然而,yargs 可以为我们完成所有这些工作,并将其放在一个非常简单的对象上,我们可以访问它。您还学会了如何在 Node.js 中使用 JSON。

接下来,我们填写了addNote函数。我们能够使用命令行添加笔记,并且能够将这些笔记保存到一个 JSON 文件中。最后,我们将addNote中的大部分代码提取到单独的函数fetchNotessaveNotes中,这些函数现在是独立的,并且可以在整个代码中重复使用。当我们开始填写其他方法时,我们可以简单地调用fetchNotessaveNotes,而不必一遍又一遍地将内容复制到每个新方法中。

在下一章中,我们将继续探讨有关 node 的基本知识。我们将探索与 node 相关的一些更多概念,比如调试;我们将处理readremove笔记命令。除此之外,我们还将学习有关 yargs 和箭头函数的高级特性。

第四章:Node 基础知识-第三部分

我们开始为笔记应用程序中的所有其他命令添加支持。我们将看看如何创建我们的read命令。read命令将负责获取单个笔记的正文。它将获取所有笔记并将它们打印到屏幕上。除此之外,我们还将研究如何调试损坏的应用程序,并了解一些新的 ES6 功能。您将学习如何使用内置的 Nodedebugger

然后,您将学习更多关于如何配置 yargs 以用于命令行界面应用程序。我们将学习如何设置命令、它们的描述和参数。我们将能够在参数上设置各种属性,例如它们是否是必需的等等。

删除笔记

在这一部分,当有人使用remove命令并传入他们想要移除的笔记的标题时,您将编写删除笔记的代码。在上一章中,我们已经创建了一些实用函数来帮助我们获取和保存笔记,所以代码实际上应该非常简单。

使用removeNote函数

这个过程的第一步是填写我们在之前章节中定义的removeNote函数,这将是您的挑战。让我们从notes.js文件的removeNote函数中删除console.log。您只需要编写三行代码就可以完成这项任务。

现在,第一行将获取笔记,然后工作将是过滤掉笔记,删除参数标题的笔记。这意味着我们想要遍历笔记数组中的所有笔记,如果它们中的任何一个标题与我们想要删除的标题匹配,我们就想要摆脱它们。这可以使用我们之前使用的notes.filter函数来完成。我们只需要在duplicateNotes函数中的等式语句中切换为不等式,这段代码就可以做到这一点。

它将遍历笔记数组。每当它找到一个与标题不匹配的笔记时,它将保留它,这是我们想要的,如果它找到标题,它将返回false并将其从数组中删除。然后我们将添加第三行,即保存新的笔记数组:

var removeNote = (title) => {
  // fetch notes
  // filter notes, removing the one with title of argument
  // save new notes array
};

上述代码行是您需要填写的唯一三行。不要担心从removeNote返回任何内容或填写app.js中的任何内容。

对于fetchNotes行的第一件事是创建一个名为notes的变量,就像我们在上一章的addNote中所做的一样,并将其设置为从fetchNotes返回的结果:

var removeNote = (title) => {
  var notes = fetchNotes();
  // filter notes, removing the one with title of argument
  // save new notes array
};

此时,我们的笔记变量存储了所有笔记的数组。我们需要做的下一件事是过滤我们的笔记。

如果有一个具有这个标题的笔记,我们想要删除它。这将通过创建一个新变量来完成,我将称其为filteredNotes。在这里,我们将filteredNotes设置为将从notes.filter返回的结果,这是我们之前已经使用过的:

var removeNote = (title) => {
  var notes = fetchNotes();
  // filter notes, removing the one with title of argument
  var filteredNotes = notes.filter();
  // save new notes array
};

我们知道notes.filter接受一个函数作为它唯一的参数,并且该函数被调用时会用数组中的单个项目。在这种情况下,它将是一个note。我们可以使用 ES6 箭头语法在一行上完成所有这些。

如果我们只有一条语句,我们不需要打开和关闭大括号。

这意味着在这里,如果note.title不等于传入函数的标题,我们可以返回true

var removeNote = (title) => {
  var notes = fetchNotes();
  var filteredNotes = notes.filter((note) => note.title !== title);
  // save new notes array
};

这将用所有标题与传入标题不匹配的所有笔记填充filteredNotes。如果标题与传入的标题匹配,它将不会被添加到filteredNotes中,因为我们的过滤函数。

最后要做的是调用saveNotes。在这里,我们将调用saveNotes,传入我们在filteredNotes变量下拥有的新笔记数组:

var removeNote = (title) => {
  var notes = fetchNotes();
  var filteredNotes = notes.filter((note) => note.title !== title);
  saveNotes(filteredNotes);
  // save new notes array
};

如果我们传入笔记,它不会按预期工作;我们正在过滤掉笔记,但实际上并没有保存这些笔记,因此它不会从 JSON 中删除。我们需要像前面的代码中所示那样传递filteredNotes。我们可以通过保存文件并尝试删除我们的笔记来测试这些。

我将尝试从notes-data.json文件中删除secret2。这意味着我们需要做的就是运行我们在app.js中指定的命令remove(参考下面的代码图像,然后它将调用我们的函数)。

我将使用app.js运行 Node,并传入remove命令。我们需要为 remove 提供的唯一参数是标题;无需提供正文。我将把这个设置为secret2

node app.js remove --title=secret2

如屏幕截图所示,如果我按enter,你会看到我们没有得到任何输出。虽然我们有删除打印命令,但没有消息表明是否删除了笔记,但我们稍后会在本节中添加。

现在,我们可以检查数据。在这里你可以看到secret2不见了:

这意味着我们的 remove 方法确实按预期工作。它删除了标题匹配的笔记,并保留了所有标题不等于secret2的笔记,这正是我们想要的。

打印删除笔记的消息

现在,我们要做的下一件事是根据是否实际删除了笔记来打印消息。这意味着调用removeNote函数的app.js需要知道是否删除了笔记。我们如何弄清楚呢?在notes.js removeNotes函数中,我们如何可能返回那个信息?

我们可以这样做,因为我们有两个非常重要的信息。我们有原始笔记数组的长度和新笔记数组的长度。如果它们相等,那么我们可以假设没有笔记被删除。如果它们不相等,我们将假设已经删除了一个笔记。这正是我们要做的。

如果removeNote函数返回true,那意味着已删除一个笔记;如果返回false,那意味着没有删除笔记。在removeNotes函数中,我们可以添加返回,如下面的代码所示。我们将检查notes.length是否不等于filteredNotes.length

var removeNote = (title) => {
  var notes = fetchNotes();
  var filteredNotes = notes.filter((note) => note.title !== title);
  saveNotes(filteredNotes);

 return notes.length !== filteredNotes.length;
};

如果它们不相等,它将返回true,这是我们想要的,因为已经删除了一个笔记。如果它们相等,它将返回false,这很好。

现在,在app.js中,我们可以在removeNoteelse if块中添加几行代码,以使此命令的输出更加友好。要做的第一件事是存储布尔值。我将创建一个名为noteRemoved的变量,并将其设置为返回的结果,如下面的代码所示,它将是truefalse

} else if (command == 'remove') {
  var noteRemoved = notes.removeNote(argv.title);
}

在下一行,我们可以创建我们的消息,我将使用三元运算符在一行上完成所有这些。现在,三元运算符允许您指定条件。在我们的情况下,我们将使用一个变量消息,并将其设置为条件noteRemoved,如果删除了一个笔记,则为true,如果没有,则为false

现在,三元运算符可能有点令人困惑,但在 JavaScript 和 Node.js 中非常有用。三元运算符的格式是首先添加条件,问号,要运行的真值表达式,冒号,然后是要运行的假值表达式。

在条件之后,我们将放一个空格,然后是一个问号和一个空格;这是如果条件为真时将运行的语句。如果noteRemoved条件通过,我们要做的是将消息设置为Note was removed

 var message = noteRemoved ? 'Note was removed' :

现在,如果noteRemovedfalse,我们可以在前一个语句的冒号后面指定该条件。在这里,如果没有笔记被删除,我们将使用文本Note not found

var message = noteRemoved ? 'Note was removed' : 'Note not found';

现在,有了这个,我们可以测试一下我们的消息。最后要做的就是使用console.log将消息打印到屏幕上:

var noteRemoved = notes.removeNote(argv.title);
var message = noteRemoved ? 'Note was removed' : 'Note not found';
console.log(message);

这使我们避免了使我们的else-if子句变得不必要复杂的if语句。

回到 Atom 中,我们可以重新运行上一个命令,在这种情况下,没有笔记会被删除,因为我们已经删除了它。当我运行它时,你可以看到Note not found打印到屏幕上:

现在,我将删除一个确实存在的笔记;在notes-data.json中,我有一个标题为 secret 的笔记,如下所示:

让我们重新运行在终端中删除标题中的2的命令。当我运行这个命令时,你可以看到Note was removed打印到屏幕上:

这就是本节的全部内容;我们现在已经有了我们的remove命令。

阅读笔记

在本节中,您将负责填写read命令的其余部分。现在,read命令确实有一个 else-if 块,在app.js中找到我们调用getNote的地方:

} else if (command === 'read') {
  notes.getNote(argv.title);

getNotenotes.js中定义,尽管目前它只是打印一些虚拟文本:

var getNote = (title) => {
  console.log('Getting note', title);
};

在本节中,您需要连接这两个函数。

首先,您需要对getNote的返回值做一些处理。如果我们的getNote函数找到了笔记对象,它将返回该笔记对象。如果没有找到,它将返回 undefined,就像我们在上一章节添加和保存笔记中讨论的addNote一样。

存储了该值之后,您将使用console.log进行一些打印,类似于我们这里所做的:

if (command === 'add') {
  var note = notes.addNote(argv.title, argv.body);
  if (note) {
    console.log('Note created');
    console.log('--');
    console.log(`Title: ${note.title}`);
    console.log(`Body: ${note.body}`);
  } else {
    console.log('Note title taken');
  }

显然,Note created将类似于Note readNote title taken将类似于Note not found,但是一般的流程将完全相同。现在,一旦您在app.js中连接了这一切,您可以继续填写函数的notes.js

现在,在notes.js中的函数不会太复杂。您需要做的只是获取笔记,就像我们在以前的方法中所做的那样,然后您将使用notes.filter,我们探索了只返回标题与作为参数传入的标题匹配的笔记。现在,在我们的情况下,这要么是零个笔记,这意味着找不到笔记,要么是一个笔记,这意味着我们找到了人们想要返回的笔记。

接下来,我们确实需要返回那个笔记。重要的是要记住,notes.filter的返回值始终是一个数组,即使该数组只有一个项目。您需要做的是返回数组中的第一个项目。如果该项目不存在,那没关系,它将返回 undefined,这正是我们想要的。如果存在,那很好,这意味着我们找到了笔记。这个方法只需要三行代码,一个用于获取,一个用于过滤,一个用于返回语句。现在,一旦您完成了所有这些,我们将对其进行测试。

使用 getNote 函数

让我们来处理这个方法。现在,我要做的第一件事是在app.js中填写一个名为 note 的变量,它将存储从getNote返回的值:

} else if (command === 'read') {
  var note = notes.getNote(argv.title);

现在,这可能是一个单独的笔记对象,也可能是 undefined。在下一行,我可以使用if语句来打印消息,如果它存在,或者如果它不存在。我将使用if note,并且我将附加一个else子句:

} else if (command === 'read') {
  var note = notes.getNote(argv.title);
  if (note) {

  } else {

  }

这个else子句将负责在找不到笔记时打印错误。让我们先从这个开始,因为它非常简单,console.logNote not found,如下所示:

  if (note) {

  } else {
    console.log('Note not found');  
  }

现在我们已经填写了else子句,我们可以填写if语句。对于这一点,我将打印一条小消息,console.log ('Note found')就可以了。然后我们可以继续打印实际的笔记详情,我们已经有了这段代码。我们将添加连字符间隔,然后有我们的笔记标题和笔记正文,如下所示:

if (note) {
    console.log('Note found');
    console.log('--');
    console.log(`Title: ${note.title}`);
    console.log(`Body: ${note.body}`);    
  } else {
    console.log('Note not found');  
  }

现在我们已经完成了app.js的内部,我们可以进入notes.js文件,并填写getNote方法,因为目前它没有对传入的标题做任何处理。

在 notes 中,你需要做的是填写这三行。第一行将负责获取笔记。我们在上一节中已经用fetchNotes函数做过了:

var getNote = (title) => {
  var notes = fetchNotes();
};

现在我们已经准备好了笔记,我们可以调用notes.filter,返回所有的笔记。我将创建一个名为filteredNotes的变量,将其设置为notes.filter。现在,我们知道 filter 方法需要一个函数,我将定义一个箭头函数(=>)就像这样:

var filteredNotes = notes.filter(() => {

});

在箭头函数(=>)中,我们将传入的单个笔记,并在笔记标题,也就是我们在 JSON 文件中找到的笔记标题,等于标题时返回true

var filteredNotes = notes.filter(() => {
    return note.title === title;
  });
};

当笔记标题匹配时,这将返回true,如果不匹配则返回 false。或者,我们可以使用箭头函数,我们只有一行代码,如下所示,我们返回了一些东西;我们可以剪切掉我们的条件,删除大括号,然后简单地将该条件粘贴到这里:

var filteredNotes = notes.filter((note) => note.title === title);

这具有完全相同的功能,只是更短,更容易查看。

现在我们已经有了所有的数据,我们只需要返回一些东西,我们将返回filteredNotes数组中的第一个项目。接下来,我们将获取第一个项目,也就是索引为零的项目,然后我们只需要使用return关键字返回它:

var getNote = (title) => {
  var notes = fetchNotes();
  var filteredNotes = notes.filter((note) => note.title === title);
  return filteredNotes[0];
};

现在,有可能filteredNotes,第一个项目不存在,没关系,它将返回 undefined,在这种情况下,我们的 else 子句将运行,打印找不到笔记。如果有笔记,太好了,那就是我们想要打印的笔记,在app.js中我们就是这样做的。

运行getNote函数

既然我们已经准备就绪,我们可以通过在终端中运行我们的应用程序node app.js来测试这个全新的功能。我将使用read命令,并传入一个标题等于我知道在notes-data.json文件中不存在的字符串:

node app.js read --title="something here"

当我运行命令时,我们得到了找不到笔记,如图所示,这正是我们想要的:

现在,如果我尝试获取一个标题存在的笔记,我期望那个笔记会返回。

在数据文件中,我有一个标题为购买的笔记;让我们尝试获取它。我将使用上箭头键填充上一个命令,并将标题替换为to space,购买,并按enter

如前面的代码所示,您可以看到找到笔记打印到屏幕上,这太棒了。在找到笔记之后,我们有我们的间隔符,然后是标题购买和正文食物,正如它出现在数据文件中一样。有了这个,我们就完成了read命令。

DRY 原则

现在,在我们结束本节之前,我还想解决一件事。在app.js中,我们现在在两个地方有相同的代码。我们在add命令以及read命令中都有空格或标题正文:

if (command === 'add') {
  var note = notes.addNote(argv.title, argv.body);
  if (note) {
    console.log('Note created');
    console.log('--');
    console.log(`Title: ${note.title}`);
    console.log(`Body: ${note.body}`);
  } else {
    console.log('Note title taken');
  }
 } else if (command === 'list') {
   notes.getAll();
 } else if (command === 'read') {
   var note = notes.getNote(argv.title);
   if (note) {
     console.log('Note found');
     console.log('--');
     console.log(`Title: ${note.title}`);
     console.log(`Body: ${note.body}`);
   } else {
     console.log('Note not found');
 }

当你发现自己在复制和粘贴代码时,最好将其拆分成一个函数,两个位置都调用该函数。这就是DRY 原则,它代表不要重复自己

使用 logNote 函数

在我们的情况下,我们在重复自己。最好将其拆分成一个函数,我们可以从两个地方调用它。为了做到这一点,我们要做的就是在notes.js中创建一个名为logNote的函数。

现在,在notes.js中,在removeNote函数下面,我们可以将这个全新的函数命名为logNote。这将是一个带有一个参数的函数。这个参数将是笔记对象,因为我们想要打印标题和正文。如下所示,我们期望传入笔记:

var logNote = (note) => {

};

现在,填写logNote函数将会非常简单,特别是当你解决 DRY 问题时,因为你可以简单地将重复的代码剪切出来,然后粘贴到logNote函数中。在这种情况下,变量名已经对齐,所以不需要更改任何内容:

var logNote = (note) => {
  console.log('--');
  console.log(`Title: ${note.title}`);
  console.log(`Body: ${note.body}`);
};

现在我们已经有了logNote函数,我们可以在app.js中进行更改。在app.js中,我们已经删除了console.log语句,我们可以调用notes.logNote,传入笔记对象就像这样:

else if (command === 'read') {
  var note = notes.getNote(argv.title);
  if (note) {
    console.log('Note found');
    notes.logNote(note);
  } else {
    console.log('Note not found');
 }

add命令的if块中也可以做同样的事情。我可以删除这三个console.log语句,并调用notes.logNote,传入笔记:

if (command === 'add') {
  var note = notes.addNote(argv.title, argv.body);
  if (note) {
    console.log('Note created');
    notes.logNote(note);
 } else {
   console.log('Note title taken');
 }

现在我们已经有了这个,我们可以重新运行我们的程序,希望我们看到的是完全相同的功能。

在重新运行程序之前要做的最后一件事是在notes.js文件的exports模块中导出logNote函数。LogNote将被导出,我们使用 ES6 语法来做到这一点:

module.exports = {
  addNote,
  getAll,
  getNote,
  removeNote,
  logNote
};

有了这个,我现在可以使用上箭头键重新运行 Terminal 中的上一个命令,然后按enter

node app.js read --title="to buy"

如所示,我们得到Note found打印到屏幕上,标题和正文就像以前一样。我还将测试add命令,以确保它正常工作,node app.js add;我们将使用一个标题things to do和一个正文go to post office

node app.js add --title="things to do" --body="go to post office"

现在,当我按下enter,我们期望打印的日志与之前的add命令一样,这正是我们得到的:

笔记创建后打印,我们得到我们的间隔,然后得到我们的标题和正文。

在下一节中,我们将涵盖本书中最重要的一个主题;调试。知道如何正确地调试程序将在你的 Node.js 职业生涯中节省你数百个小时。如果你没有正确的工具,调试可能会非常痛苦,但一旦你知道如何做,它其实并不那么糟糕,而且可以节省你大量的时间。

调试

在本节中,我们将使用内置的debugger,这可能看起来有点复杂,因为它在命令行中运行。这意味着你必须使用命令行界面,这并不总是最令人愉快的事情。不过,在下一节中,我们将安装一个使用 Chrome DevTools 来调试你的 Node 应用程序的第三方工具。这个看起来很棒,因为 Chrome DevTools 非常出色。

在调试模式下执行程序

在继续之前,我们将了解到我们确实需要创建一个调试的地方,这将在一个游乐场文件中进行,因为我们要编写的代码对notes应用程序本身并不重要。在 notes 应用程序中,我将创建一个名为debugging.js的新文件:

debugging.js中,我们将从一个基本示例开始。我们将创建一个名为person的对象,在该对象上,我们暂时设置一个属性名。将其设置为你的名字,我将我的设置为字符串Andrew,如下所示:

var person = {
  name: 'Andrew'
};

接下来我们将设置另一个属性,但在下一行,person.age。我将我的设置为我的年龄,25

var person = {
  name: 'Andrew'
};

person.age = 25;

然后我们将添加另一个语句来更改名称,person.name等于Mike之类的东西:

var person = {
  name: 'Andrew'
};

person.age = 25;

person.name = 'Mike';

最后,我们将console.log打印person对象,代码将如下所示:

var person = {
  name: 'Andrew'
};

person.age = 25;

person.name = 'Mike';

console.log(person);

现在,实际上在这个例子中我们已经有了一种调试的形式,我们有一个console.log语句。

当你进行 Node 应用程序开发过程时,你可能已经使用了console.log来调试你的应用程序。也许有些东西不像预期的那样工作,你想准确地弄清楚那个变量里面存储了什么。例如,如果你有一个解决数学问题的函数,也许在函数的某个部分方程式是错误的,你得到了一个不同的结果。

使用console.log可能是一个非常好的方法,但它的功能非常有限。我们可以通过在终端中运行它来查看,我将运行以下命令:

node playground/debugging.js

当我运行文件时,我确实可以在屏幕上打印出我的对象,这很好,但是,你知道,如果你想调试除了person对象之外的东西,你必须添加另一个console.log语句来做到这一点。

想象一下,你有一个类似我们的app.js文件,你想看看命令等于什么,然后你想看看argv等于什么,这可能需要花费很多时间来添加和删除那些console.log语句。有一种更好的调试方法。这就是使用 Node 的debugger。现在,在我们对项目进行任何更改之前,我们将看看debugger在终端内部是如何工作的,正如我在本节开头警告过你的,内置的 Nodedebugger虽然有效,但有点丑陋和难以使用。

不过,现在,我们将以基本相同的方式运行应用程序,只是这一次我们将输入node inspect。Node debug 将以与常规 Node 命令完全不同的方式运行我们的应用程序。我们在 playground 文件夹中运行相同的文件,它叫做debugging.js

node inspect playground/debugging.js

当你按下enter时,你应该会看到类似这样的东西:

在输出中,我们可以忽略前两行。这基本上意味着debugger已经正确设置,并且能够监听后台运行的应用程序。

接下来,我们在 playground 调试中有我们的第一个换行符在第一行,紧接着它你可以看到带有一个小尖号(>)的第一行。当你首次以调试模式运行你的应用程序时,它会在执行第一条语句之前暂停。当我们暂停在像第一行这样的行上时,这意味着这行还没有执行,所以在这个时间点上我们甚至还没有person变量。

现在,正如你在前面的代码中所看到的,我们还没有返回到命令行,Node 仍在等待输入,我们可以运行一些不同的命令。例如,我们可以运行n,它是下一个的缩写。你可以输入n,按下enter,这会移到下一个语句。

我们有下一条语句,第一行的语句被执行了,所以person变量确实存在。然后我可以再次使用n去到下一个语句,我们在那里声明person.name属性,将它从Andrew更新为Mike

注意,在这一点上,年龄确实存在,因为那行已经执行过了。

现在,n命令会逐条执行你的整个程序。如果你意识到你不想在整个程序中这样做,这可能需要花费很多时间,你可以使用cc命令是Continue的缩写,它会一直执行到程序的最后。在下面的代码中,你可以看到我们的console.log语句运行了名字Mike和年龄25

这就是如何使用debug关键字的一个快速示例。

现在,我们实际上并没有进行任何调试,我们只是运行了整个程序,因为在写这些命令时有点陌生,比如下一个和继续,我决定在没有调试的情况下进行一次干运行。你可以使用control + C来退出debugger并返回到终端。

我将使用clear来清除所有输出。现在我们对如何在debug模式下执行程序有了一个基本的了解,让我们看看我们实际上如何进行一些调试。

使用调试

我将使用上箭头键两次重新运行程序,返回到 Node debug命令。然后,我将运行程序,并连续按两次nn

此时,我们在第七行,这就是当前的断点所在。从这里,我们可以使用一个叫做repl的命令进行一些调试,它代表读取-求值-打印-循环。在我们的情况下,repl命令会将你带到debugger的一个完全独立的区域。当你使用它时,你实际上是在一个 Node 控制台中:

你可以运行任何 Node 命令,例如,我可以使用console.log打印出test,然后test就会打印出来。

我可以创建一个变量a,它等于13,然后我可以引用a,我可以看到它等于4,如下所示:

更重要的是,我们可以访问当前程序的状态,也就是在第七行执行之前的状态。我们可以使用这个来打印出person,如下面的代码所示,你可以看到person的名字是Andrew,因为第七行还没有执行,年龄是25,正如程序中显示的那样:

这就是调试变得非常有用的地方。能够在某个特定时间点暂停程序将使查找错误变得非常容易。我可以做任何我想做的事情,我可以打印出person的名字属性,并且它会在屏幕上打印出Andrew,如下所示:

现在,我们还是有这个问题。我必须通过程序按下next。当你有一个非常长的程序时,可能需要运行数百或数千个语句,然后才能到达你关心的点。显然这不是理想的,所以我们要找到更好的方法。

让我们使用control + C退出repl;现在我们回到了debugger

从这里开始,我们将在debugging.js中对我们的应用程序进行快速更改。

假设我们想要在第七行暂停,介于person年龄属性更新和person名字属性更新之间。为了暂停,我们要做的是运行语句debugger

var person = {
  name: 'Andrew'
};

person.age = 25;

debugger;

person.name = 'Mike';

console.log(person);

当你有一个像之前一样的debugger语句时,它告诉 Node debugger在这里停下,这意味着你可以使用c(continue)来继续,而不是使用n(next)逐条语句执行,它会一直执行,直到程序退出或者看到一个debugger关键字。

现在,在终端中,我们将重新运行程序,就像之前一样。这一次,我们不会按两次n,而是使用c来继续:

现在,当我们第一次使用c时,它到达了程序的末尾,打印出了我们的对象。这一次它会继续,直到找到debugger关键字。

现在,我们可以使用repl,访问任何我们喜欢的东西,例如,person.age,如下所示:

一旦我们完成调试,我们可以退出并继续执行程序。同样,我们可以使用control + C来退出repldebugger

真正的调试基本上都是使用debugger关键字。你可以把它放在程序的任何地方,以调试模式运行程序,最终它会到达debugger关键字,然后你可以做一些事情。例如,你可以探索一些变量值,运行一些函数,或者玩弄一些代码来找到错误。没有人真的使用n来逐行打印程序,找到导致问题的那一行。那太费时间了,而且不现实。

在笔记应用程序中使用调试器

现在你对debugger有了一点了解,我希望你在我们的笔记应用内使用它。我们将在notes.js内添加debugger语句到logNote函数的第一行。然后我将以调试模式运行程序,传入一些参数,这些参数将导致logNote运行;例如,读取一个笔记,在笔记被获取后,它将调用logNote

现在,一旦我们在logNote函数中有了debugger关键字,并以这些参数在调试模式下运行它,程序应该在这一点停止。一旦程序以调试模式启动,我们将使用c来继续,它会暂停。接下来,我们将打印出笔记对象并确保它看起来没问题。然后,我们可以退出repl并退出debugger

现在,首先我们在这里添加debugger语句:

var logNote = (note) => {
  debugger;
  console.log('--');
  console.log(`Title: ${note.title}`);
  console.log(`Body: ${note.body}`);
};

我们可以保存文件,现在我们可以进入终端;我们的应用内不需要做其他任何事情。

在终端内,我们将运行我们的app.js文件,node debug app.js,因为我们想以调试模式运行程序。然后我们可以传入我们的参数,比如read命令,我将传入一个标题,"to buy"如下所示:

node debug app.js read --title="to buy"

在这种情况下,我有一个标题为"to buy"的笔记,如下所示:

现在,当我运行上述命令时,它会在第一条语句运行之前暂停,这是预期的:

我现在可以使用c来继续程序。它会运行尽可能多的语句,直到程序结束或找到debugger关键字,如下面的代码所示,你可以看到debugger被找到,我们的程序已经停在notes.js的第49行:

这正是我们想要做的。现在,从这里,我将进入repl并打印出笔记参数,如下面的代码所示,你可以看到我们有一个标题为to buy和正文为food的笔记:

现在,如果这个语句出现了错误,也许屏幕上打印了错误的东西,这将给我们一个很好的想法。无论传递给note的是什么,显然都被用在console.log语句内,所以如果打印出了问题,最有可能是logNote函数内传递的问题。

现在我们已经打印了note变量,我们可以关闭repl,我们可以使用control + Cquit来退出debugger

现在我们回到了常规终端,我们已经成功完成了 Node 应用内的调试。在下一节中,我们将看一种不同的方法来做同样的事情,这种方法有一个更好的图形用户界面,我发现它更容易导航和使用。

列出笔记

现在我们在调试方面取得了一些进展,让我们回到我们应用的命令,因为只剩下一个要填写的命令了(我们已经分别在第三章、Node 基础知识-第二部分和本章中涵盖了addreadremove命令)。这是list命令,它将非常容易,list命令中没有复杂的情况。

使用getAll函数

为了开始,我们所需要做的就是填写list notes函数,这种情况下我们称之为getAllgetAll函数负责返回每一个笔记。这意味着它将返回一个对象数组,即我们所有笔记的数组。

我们所要做的就是返回fetchNotes,如下所示:

var getAll = () => {
  return fetchNotes();
}

无需过滤,也无需操作数据,我们只需要将数据从fetchNotes传递回getAll。现在我们已经做好了这一点,我们可以在app.js内填写功能。

我们必须创建一个变量来存储便签,我本来打算称它为 notes,但我可能不应该这样做,因为我们已经声明了一个 notes 变量。我将创建另一个变量,称为allNotes,将其设置为从getAll返回的值,我们知道这是因为我们刚刚填写了返回所有便签的内容:

else if (command === 'list') {
  var allNotes = notes.getAll();
}

现在我可以使用console.log打印一条小消息,我将使用模板字符串,这样我就可以注入要打印的实际便签数量。

在模板字符串中,我将添加打印,然后使用$(美元)符号和大括号,allNotes.length:这是数组的长度,后跟带有s的便签,括号中的s用于处理单数和复数情况,如下面的代码块所示:

else if (command === 'list') {
  var allNotes = notes.getAll();
  console.log(`Printing ${allNotes.length} note(s).`);
}

因此,如果有六条便签,它会说打印六条便签。

既然我们已经有了这个,我们必须继续实际打印每个便签的过程,这意味着我们需要为allNotes数组中的每个项目调用logNote一次。为此,我们将使用forEach,这是一个类似于 filter 的数组方法。

Filter 允许您通过返回truefalse来操作数组以保留项目或删除项目;forEach只是为数组中的每个项目调用一次回调函数。在这种情况下,我们可以使用allNotes.forEach,传入一个回调函数。现在,该回调函数将是一个箭头函数(=>),在我们的情况下,它将被调用note变量,就像 filter 一样。我们将调用notes.logNote,传入note参数,就像这里一样:

else if (command === 'list') {
  var allNotes = notes.getAll();
  console.log(`Printing ${allNotes.length} note(s).`);
  allNotes.forEach((note) => {
    notes.logNote(note);
  });
}

现在我们已经有了这个,我们可以通过在这里添加logNote调用来简化它:

else if (command === 'list') {
  var allNotes = notes.getAll();
  console.log(`Printing ${allNotes.length} note(s).`);
  allNotes.forEach((note) => notes.logNote(note));
}

这完全是相同的功能,只是使用了表达式语法。现在我们已经有了箭头函数(=>),我们正在为所有便签数组中的每个项目调用notes.logNote一次。让我们保存app.js文件并在终端中测试一下。

为了测试list命令,我将使用node app.jslist命令。不需要传递任何参数:

node app.js list

当我运行这个程序时,我确实得到了打印 3 条便签,然后我得到了我的3 条购买便签从商店购买要做的事情,如下面的代码输出所示,这太棒了:

有了这个,我们所有的命令现在都可以工作了。我们可以添加便签,删除便签,阅读单个便签,并列出存储在我们的 JSON 文件中的所有便签。

接下来,我想清理一些命令。在app.jsnotes.js中,我们有一些不再需要的console.log语句打印出一些东西。

app.js的顶部,我将删除console.log('Starting app.js')语句,使常量fs成为第一行。

我还将删除两个语句:console.log('Command: ', command)console.log('Yargs', argv),打印命令和yargs变量值。

notes.js中,我还将删除文件顶部的console.log('Stating notes.js')语句,因为它不再需要,将常量fs放在顶部。

当我们首次开始探索不同的文件时,它绝对是有用的,但现在我们已经把一切都放在了一起,就没有必要了。如果我重新运行list命令,这次你可以看到它看起来更整洁了:

打印三条便签是出现的第一行。有了这个,我们已经完成了我们的命令。

在下一节中,我们将稍微深入地看一下如何配置 yargs。这将让我们为我们的命令要求某些参数。因此,如果有人尝试添加一个没有标题的便签,我们可以警告用户并阻止程序执行。

高级 yargs

在我们深入讨论 yargs 的高级内容之前,首先,我想查看一下 yargs 文档,这样你至少知道 yargs 的信息是从哪里来的。你可以通过谷歌搜索npm yargs来获取。我们将前往 npm 上的 yargs 包页面。这里有 yargs 的文档,如下所示:

现在 yargs 文档没有目录,这使得导航变得有点困难。它以一些没有特定顺序的示例开始,然后最终列出了你可以使用的所有方法,这就是我们要找的。

所以我将使用command + FC**trl + F)在页面上搜索方法,如下面的截图所示,我们得到了方法标题,这就是我们要找的:

如果你在页面上滚动,你会开始看到一个字母顺序的列表,列出了你在 yargs 中可以访问的所有方法。我们特别寻找.command;这是我们可以用来配置我们的四个命令:addreadremovelist notes 的方法:

我们将指定它们需要哪些选项,如果有的话,我们还可以设置描述和帮助功能。

在 yargs 上使用链式语法

现在,为了开始,我们需要在app.js中进行一些更改。我们将从add命令开始(有关更多信息,请参阅上一章节中的添加和保存笔记部分)。

我们想在app.js中的argv函数中添加一些有用的信息,将:

  • 让 yargs 验证add命令是否被正确执行,并

  • 让用户知道add命令应该如何执行

现在我们将进行属性调用的链式调用,这意味着在访问.argv之前,我想调用.command,然后我将在命令的返回值上调用.argv,如下所示:

const argv = yargs
  .command()
  .argv;

现在,如果你使用过 jQuery,这种链式语法可能看起来很熟悉,支持许多不同的库。一旦我们在yargs上调用.command,我们将传入三个参数。

第一个是命令名称,用户在终端中输入的方式,我们的情况下是add

const argv = yargs
  .command('add')
  .argv;

然后我们将传入另一个字符串,这将是对命令做什么的描述。这将是一种用户可以阅读的英文可读描述,以便用户可以阅读以确定是否是他们想要运行的命令:

const argv = yargs
  .command('add', 'Add a new note')
  .argv;

接下来的是一个对象。这将是一个选项对象,让我们指定这个命令需要什么参数。

调用.help命令

现在,在我们进入选项对象之前,让我们在命令之后再添加一个调用。我们将调用.help,这是一个方法,所以我们将它作为一个函数调用,我们不需要传入任何参数:

const argv = yargs
  .command('add', 'Add a new note', {

  })
  .help()
  .argv;

当我们添加这个帮助调用时,它设置了yargs在有人运行程序时返回一些非常有用的信息。例如,我可以运行node app.js命令并带有help标志。help标志是因为我们调用了那个帮助方法,当我运行程序时,你可以看到我们有哪些可用的选项:

node app.js --help

如前面的输出所示,我们有一个命令add Add a new note,以及当前命令helphelp选项。如果我们运行node app.js add命令并带有help,情况也是如此,如下所示:

node app.js add --help

在这个输出中,我们可以查看add命令的所有选项和参数,这种情况下是没有的,因为我们还没有设置:

添加选项对象

让我们在 Atom 中重新添加选项和参数。为了添加属性,我们将更新选项对象,其中键是属性名称,无论是标题还是正文,值是另一个对象,让我们指定该属性应该如何工作,如下所示:

const argv = yargs
  .command('add', 'Add a new note', {
    title: {

    }
  })
  .help()
  .argv;

添加标题

在标题的情况下,我们将在左侧添加标题,并在右侧放置我们的选项对象。在标题中,我们将配置三个属性describedemandalias

describe属性将被设置为一个字符串,这将描述应该传递给标题的内容。在这种情况下,我们可以使用Title of note

const argv = yargs
  .command('add', 'Add a new note', {
    title: {
      describe: 'Title of note'
    }
  })
  .help()
  .argv;

接下来我们配置demand。它将告诉 yarg 这个参数是否是必需的。demand默认为false,我们将把它设置为true

const argv = yargs
  .command('add', 'Add a new note', {
    title: {
      describe: 'Title of note',
      demand: true
    }
  })
  .help()
  .argv;

现在,如果有人尝试运行 add 命令而没有标题,它将失败,我们可以证明这一点。我们可以保存app.js,在终端中,我们可以重新运行我们之前的命令,删除help标志,当我这样做时,你会看到一个警告,Missing required argument: title如下所示:

请注意,在输出中,标题参数是Title of note,这是我们使用的描述字符串,并且在右侧是required,让您知道在调用add命令时必须提供标题。

除了describedemand,我们还将提供第三个选项,这称为aliasalias让您提供一个快捷方式,这样您就不必输入--title;您可以将别名设置为单个字符,如t

const argv = yargs
  .command('add', 'Add a new note', {
    title: {
      describe: 'Title of note',
      demand: true,
      alias: 't'
    }
  })
  .help()
  .argv;

完成后,您现在可以使用新的语法在终端中运行命令。

让我们运行我们的 add 命令,node app.js add,而不是--title。我们将使用-t,这是标志版本,我们可以将其设置为任何我们喜欢的值,例如,flag title将是标题,--body将被设置为body,如下面的代码所示。请注意,我们还没有设置 body 参数,所以没有alias

node app.js add -t="flag title" --body="body"

如果我运行这个命令,一切都按预期工作。标志标题出现在应该出现的地方,即使我们使用了字母t的别名版本,如下所示:

添加正文

现在我们已经配置了标题,我们可以为正文做完全相同的事情。我们将指定我们的选项对象并提供这三个参数:describedemandalias用于body

const argv = yargs
  .command('add', 'Add a new note', {
    title: {
      describe: 'Title of note',
      demand: true,
      alias: 't'
    },
    body: {

 }
  })
  .help()
  .argv;

第一个是describe,这个很容易。describe将被设置为一个字符串,在这种情况下,Body of note将完成任务:

const argv = yargs
  .command('add', 'Add a new note', {
    title: {
      describe: 'Title of note',
      demand: true,
      alias: 't'
    },
    body: {
      describe: 'Body of note'
    }
  })
  .help()
  .argv;

接下来是demand,为了添加一个注释,我们需要一个body。所以我们将demand设置为true,就像我们之前为title做的那样:

const argv = yargs
  .command('add', 'Add a new note', {
    title: {
      describe: 'Title of note',
      demand: true,
      alias: 't'
    },
    body: {
      describe: 'Body of note'
      demand: true
    }
  })
  .help()
  .argv;

最后但并非最不重要的是aliasalias将被设置为一个单个字母,我将使用字母b代表body

const argv = yargs
  .command('add', 'Add a new note', {
    title: {
      describe: 'Title of note',
      demand: true,
      alias: 't'
    },
    body: {
      describe: 'Body of note'
      demand: true,
      alias: 'b'
    }
  })
  .help()
  .argv;

有了这个设置,我们现在可以保存app.js,在终端中,我们可以花点时间重新运行node app.js add,并使用help标志:

node app.js add --help

当我们运行这个命令时,现在应该会看到正文参数出现,甚至可以看到它显示了标志版本,如下面的输出所示,别名-bBody of note),并且是必需的:

现在我将运行node app.js add,传入两个参数t。我将把它设置为tb设置为b

当我运行命令时,一切都按预期工作:

node app.js add -t=t -b=b

如前面的输出截图所示,创建了一个标题为t,正文为b的新便笺。有了这个设置,我们现在已经成功完成了add命令的设置。我们有我们的add命令标题,一个描述,以及为该命令指定参数的块。现在我们还有三个命令需要添加支持,所以让我们开始做吧。

添加对读取和删除命令的支持

在下一行,我将再次调用.command,传入命令名称。让我们先执行list命令,因为这个命令非常简单,不需要任何参数。然后我们将为list命令传入描述,“列出所有便笺”,如下所示:

.command('list', 'List all notes')
.help()
.argv;

接下来,我们将再次调用命令。这次我们将为read命令执行命令。read命令读取一个单独的便笺,所以对于read命令的描述,我们将使用类似“读取便笺”的内容:

.command('list', 'List all notes')
.command('read', 'Read a note')
.help()
.argv;

现在read命令确实需要标题参数。这意味着我们需要提供该选项对象。我将从add命令中取出title,复制它,并粘贴到read命令的选项对象中:

.command('list', 'List all notes')
.command('read', 'Read a note', {
  title: {
    describe: 'Title of note',
    demand: true,
    alias: 't'
  }
})
.help()
.argv;

您可能刚刚注意到,我们有重复的代码。标题配置刚刚被复制并粘贴到多个位置。如果这是 DRY 的话,如果它在一个变量中,我们可以在addread命令中引用它,那将是非常好的。

在我们调用read命令的地方后面,将调用remove命令。现在,remove命令将有一个描述。我们将坚持使用一些简单的描述,比如“删除便笺”,并且我们将提供一个选项对象:

.command('remove', 'Remove a note', {

})

现在我可以添加与read命令相同的选项对象。但是,在该选项对象中,我将标题设置为titleOptions,如下所示,以避免重复的代码。

.command('remove', 'Remove a note', {
  title: titleOptions
})

添加titleOptionbodyOption变量

现在我还没有创建titleOptions对象,所以代码目前可能会失败,但这是一个一般的想法。我们希望创建titleOptions对象一次,并在我们使用它的所有位置,为addreadremove命令引用它。我可以像这样为readadd命令添加titleOptions

.command('add', 'Add a new note', {
  title: titleOptions,
  body: {
    describe: 'Body of note',
    demand: true,
    alias: 'b'
  }
})
.command('list', 'List all notes')
.command('read', 'Read a note', {
  title: titleOptions
})
.command('remove', 'Remove a note', {
title: titleOptions
})

现在,在常量argv之前,我可以创建一个名为titleOptions的常量,并将其设置为我们之前为addread命令定义的对象,即describedemandalias,如下所示:

const titleOptions = {
  describe: 'Title of note',
  demand: true,
  alias: 't'
};

现在我们已经准备好了titleOptions,这将按预期工作。我们拥有了之前完全相同的功能,但是现在我们将titleOptions放在了一个单独的对象中,这符合我们在读取便笺部分讨论的 DRY 原则。

现在,我们也可以为正文做同样的事情。这可能看起来有点多余,因为我们只在一个地方使用它,但是如果我们坚持将它们分解成变量,我也会在正文的情况下这样做。在titleOptions常量之后,我可以创建一个名为bodyOptions的常量,将其设置为我们在前面的小节中为add命令定义的正文的选项对象:

const bodyOptions = {
  describe: 'Body of note',
  demand: true,
  alias: 'b'
};

有了这个设置,我们现在已经完成了。我们有addreadremove,所有这些都已经设置好了,引用了titleObjectbodyObject变量。

测试删除命令

让我们在终端中测试remove命令。我将列出我的便笺,使用node app.js list,这样我就可以看到我需要删除哪些便笺:

node app.js list

我将使用node app.js remove命令和我们的标志t删除标题为t的便笺:

node app.js remove -t="t"

我们将删除标题为t的便笺,如前所示,“便笺已删除”将打印到屏幕上。如果我两次使用上箭头键,我可以再次列出便笺,你会看到标题为t的便笺确实已经消失了:

让我们使用node app.js remove命令再删除一条笔记。这次我们将使用--title,这是参数名,我们要remove的笔记具有标题标志标题,如下所示:

当我删除它时,它会显示Note was removed,如果我重新运行list命令,我可以看到我们还剩下三条笔记,笔记确实被删除了,如下所示:

这就是笔记应用程序的全部内容。

箭头函数

在本节中,您将学习箭头函数的各个方面。这是一个 ES6 功能,我们已经稍微了解了一下。在notes.js中,我们在一些基本示例中使用它来创建方法,比如fetchNotessaveNotes,我们还将它传递给一些数组方法,比如 filter,对于每个数组,我们将它用作在数组中的每个项目上调用的回调函数。

现在,如果你尝试用箭头函数替换程序中的所有函数,很可能不会按预期工作,因为两者之间存在一些差异,了解这些差异非常重要,这样你就可以决定使用常规的 ES5 函数还是 ES6 箭头函数。

使用箭头函数

本节的目标是为您提供知识,以便做出选择,我们将通过在 playground 文件夹中创建一个名为arrow-function.js的新文件来开始:

在这个文件中,我们将玩几个例子,讨论箭头函数的一些微妙之处。在文件内输入任何内容之前,我将用nodemon启动这个文件,这样每次我们做出更改时,它都会自动在终端中刷新。

如果你还记得,nodemon是我们在第二章中安装的实用程序,Node 基础知识-第一部分。它是一个全局的 npm 模块。nodemon是要运行的命令,然后我们只需像对待其他 Node 命令一样传入文件路径。因为我们要进入playground文件夹,文件本身叫做arrow-function.js,我们将运行以下命令:

nodemon playground/arrow-function.js

我们将运行文件,除了nodemon日志之外,屏幕上什么都不会打印,如下所示:

要开始,在arrowfunction.js文件中,我们将创建一个名为 square 的函数,通过创建一个名为 square 的变量并将其设置为箭头函数。

为了创建我们的箭头函数(=>),我们首先要在括号内提供参数。因为我们将对一个数字进行平方,所以我们只需要一个数字,我将把这个数字称为x。如果我传入 3,我应该期望得到 9,如果我传入 9,我会期望得到 81。

在参数列表之后,我们必须通过将等号和大于号放在一起来放置箭头函数(=>),创建我们漂亮的小箭头。从这里开始,我们可以在花括号内提供我们想要执行的所有语句:

var square = (x) => {

};

接下来,我们可能会创建一个名为 result 的变量,将其设置为x乘以x,然后我们可能会使用return关键字返回结果变量,如下所示:

var square = (x) => {
  var result = x * x;
  return result;
};

现在,显然这可以在一行上完成,但这里的目标是说明当你使用语句箭头函数(=>)时,你可以在这些花括号之间放置任意多的行。让我们调用一个平方,我们将使用console.log来做到这一点,这样我们就可以将结果打印到屏幕上。我会调用 square;我们将用9调用 square,9的平方将是81,所以我们期望81打印到屏幕上:

var square = (x) => {
  var result = x * x;
  return result;
};
console.log(square(9));

我会保存箭头函数(=>)文件,在终端中,81就像我们预期的那样显示出来:

现在我们在之前的例子中使用的语法是箭头函数(=>)的语句语法。我们之前也探讨了表达式语法,它让你在返回一些表达式时简化你的箭头函数。在这种情况下,我们只需要指定我们想要返回的表达式。在我们的例子中,就是x乘以x

var square = (x) => x * x;
console.log(square(9));

你不需要显式添加return关键字。当你使用不带大括号的箭头函数(=>)时,它会隐式地为你提供。这意味着我们可以保存之前显示的函数,完全相同的结果会打印到屏幕上,81出现。

这是箭头函数的一个巨大优势,当你在notes.js文件中使用它们时。它让你简化你的代码,保持一行,使你的代码更容易维护和扫描。

现在,有一件事我想指出:当你有一个只有一个参数的箭头函数(=>)时,你实际上可以省略括号。如果你有两个或更多的参数,或者你有零个参数,你需要提供括号,但如果你只有一个参数,你可以不用括号引用它。

如果我以这种状态保存文件,81仍然打印到屏幕上;这很好,我们有一个更简单的箭头函数(=>)版本:

现在我们已经有了一个基本的例子,我想继续进行一个更复杂的例子,探讨常规函数和箭头函数之间的细微差别。

探索常规函数和箭头函数之间的差异

为了说明区别,我会创建一个名为user的变量,它将是一个对象。在这个对象上,我们会指定一个属性,名字。将名字设置为字符串,你的名字,在这种情况下我会将其设置为字符串Andrew

var user = {
  name: 'Andrew'
};

然后我们可以在user对象上定义一个方法。在名字后面,我在行末加上逗号,我会提供方法sayHi,将其设置为一个不带任何参数的箭头函数(=>)。暂时,我们会让箭头函数非常简单:

var user = {
  name: 'Andrew',
  sayHi: () => {

  }
};

sayHi里我们要做的就是使用console.log打印到屏幕上,在模板字符串里是Hi

var user = {
  name: 'Andrew',
  sayHi: () => {
    console.log(`Hi`);
  }
};

我们还没有使用模板字符串,但我们以后会使用,所以我会在这里使用它们。在用户对象之后,我们可以通过调用它来测试sayHiuser.sayHi

var user = {
  name: 'Andrew',
  sayHi: () => {
    console.log(`Hi`);
  }
};
user.sayHi();

我会称之为然后保存文件,我们期望Hi打印到屏幕上,因为我们的箭头函数(=>)只是使用console.log打印一个静态字符串。在这种情况下,没有任何问题;你可以毫无问题地用箭头函数(=>)替换常规函数。

当你使用箭头函数时,首先会出现的问题是箭头函数不会绑定this关键字。所以如果你在函数内部使用this,当你用箭头函数(=>)替换它时,它不会起作用。现在,this绑定;指的是父绑定,在我们的例子中没有父函数,所以这将指向全局的this关键字。现在我们的console.log不使用this,我会用一个使用this的情况来替换它。

我们会在Hi后面加一个句号,然后我会说我是,接着是名字,通常我们可以通过this.name来访问:

var user = {
  name: 'Andrew',
  sayHi: () => {
    console.log(`Hi. I'm ${this.name}`);
  }
};
user.sayHi();

如果我尝试运行这段代码,它不会按预期工作;我们会得到Hi I'm undefined 打印到屏幕上,如下所示:

为了解决这个问题,我们将看一种替代箭头函数的语法,当你定义对象字面量时非常好用,就像我们在这个例子中所做的那样。

sayHi之后,我将使用不同的 ES6 特性创建一个名为sayHiAlt的新方法。ES6 为我们提供了一种在对象上创建方法的新方式;你提供方法名sayHiAlt,然后直接跳过冒号到括号。虽然它是一个常规函数,但不是箭头函数(=>),所以也不需要函数关键字。然后我们继续到大括号,如下所示:

var user = {
  name: 'Andrew',
  sayHi: () => {
    console.log(`Hi. I'm ${this.name}`);
  },
  sayHiAlt() {

  }
};
user.sayHi();

在这里,我可以有与sayHi函数中完全相同的代码,但它将按预期工作。它将打印Hi. I'm Andrew。我将在下面调用sayHiAlt而不是常规的sayHi方法:

var user = {
  name: 'Andrew',
  sayHi: () => {
    console.log(`Hi. I'm ${this.name}`);
  },
  sayHiAlt() {
    console.log(`Hi. I'm ${this.name}`);
  }
};
user.sayHiAlt();

在终端中,你可以看到Hi. I'm Andrew打印到屏幕上:

sayHiAlt语法是一种可以解决在对象字面量上创建函数时的this问题的语法。现在我们知道this关键字不会被绑定,让我们探索箭头函数的另一个怪癖,它也不会绑定参数数组。

探索参数数组

常规函数,比如sayHiAlt,将在函数内部有一个可访问的参数数组:

var user = {
  name: 'Andrew',
  sayHi: () => {
    console.log(`Hi. I'm ${this.name}`);
  },
  sayHiAlt() {
    console.log(arguments);
    console.log(`Hi. I'm ${this.name}`);
  }
};
user.sayHiAlt();

现在,它不是一个实际的数组,更像是一个带有数组属性的对象;但是 arguments 对象确实在常规函数中指定。如果我传入 1、2 和 3 并保存文件,当我们记录 arguments 时,我们将得到它:

var user = {
  name: 'Andrew',
  sayHi: () => {
    console.log(`Hi. I'm ${this.name}`);
  },
  sayHiAlt() {
    console.log(arguments);
    console.log(`Hi. I'm ${this.name}`);
  }
};
user.sayHiAlt(1, 2, 3);

nodemon中,它需要快速重启,然后我们有我们的对象:

我们有 1、2 和 3,我们为每个属性名有索引,这是因为我们使用了常规函数。但是,如果我们切换到箭头函数(=>),它将不会按预期工作。

我将在我的箭头函数(=>)中添加console.log(arguments),并切换回调用sayHiAlt到原始方法sayHi,如下所示:

var user = {
  name: 'Andrew',
  sayHi: () => {
    console.log(arguments);
    console.log(`Hi. I'm ${this.name}`);
  },
  sayHiAlt() {
    console.log(arguments);
    console.log(`Hi. I'm ${this.name}`);
  }
};
user.sayHi(1, 2, 3);

当我保存在arrow-function.js文件中时,我们将得到与之前完全不同的东西。实际上我们将得到全局的 arguments 变量,这是我们探索的包装函数的 arguments 变量:

在之前的截图中,我们有像 require 函数、定义、我们的模块对象和一些字符串路径到文件和当前目录的东西。显然这不是我们期望的,这是你在使用箭头函数时必须注意的另一件事;你不会得到arguments关键字,你也不会得到你期望的this绑定(在sayHi语法中定义)。

这些问题主要出现在你尝试在对象上创建方法并使用箭头函数时。在这种情况下,我强烈建议你切换到我们讨论的sayHiAlt语法。你会得到一个简化的语法,但你也会得到this绑定和你期望的 arguments 变量。

总结

在本章中,我们能够重用我们在之前章节中已经创建的实用函数,使填写删除笔记的过程变得更加容易。在app.js中,我们处理了removeNote函数的执行,如果成功执行,我们打印一条消息;如果没有,我们打印另一条消息。

接下来,我们成功填写了read命令,并创建了一个非常酷的实用函数,我们可以在多个地方利用它。这使我们的代码保持干净,并防止我们在应用程序内的多个地方重复相同的代码。

然后我们讨论了一下快速调试的简介。基本上,调试是一个让你可以在任何时间点停止程序并玩弄程序的过程。这意味着你可以玩弄存在的变量、函数或者 Node 内部的任何东西。我们更多地了解了 yargs,它的配置,设置命令,它们的描述和参数。

最后,你更多地探索了箭头函数,它们的工作原理,何时使用它们,何时不使用它们。一般来说,如果你不需要 this 关键字或者 arguments 关键字,你可以毫无问题地使用箭头函数,我总是更喜欢在可以的时候使用箭头函数而不是普通函数。

在下一章中,我们将探讨异步编程以及如何从第三方 API 获取数据。我们将更多地使用普通函数和箭头函数,你将能够第一手看到如何在两者之间进行选择。