JavaScript-从前端到后端-全-

JavaScript 从前端到后端(全)

原文:zh.annas-archive.org/md5/caa9de298acf1565f89d991a81f1f93a

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

JavaScript 是世界上使用最广泛的编程语言。它拥有众多的库和模块,以及令人眼花缭乱的必须了解的主题。选择一个起点可能很困难。这本简洁实用的指南将让您在极短的时间内掌握所需技能。

本书面向的对象

这本书是为那些希望加强他们的核心 JavaScript 概念并在构建全栈应用程序中实现它们的 JavaScript 开发者而写的。

本书涵盖的内容

第一章探索 JavaScript 的核心概念,是您发现如何在 JavaScript 中使用变量、条件和循环的地方。

第二章探索 JavaScript 的高级概念,是您学习如何在 JavaScript 中使用面向对象编程的地方。

第三章Vue.js 入门,是您学习 Vue.js 的基础,包括组件和指令的地方。

第四章Vue.js 的高级概念,是您深入探索 Vue.js,包括组件间通信和视觉效果的地方。

第五章使用 Vue.js 管理列表,是您学习如何使用 Vue.js 构建一个完整项目的地方。

第六章创建和使用 Node.js 模块,是您学习使用模块进行 Node.js 编程基础的地方。

第七章使用 Express 与 Node.js,是您探索用于构建 Node.js 应用程序的主要库的地方。

第八章使用 MongoDB 与 Node.js,是您学习如何使用 Mongoose 模块在 Node.js 中使用 MongoDB 数据库的地方。

第九章整合 Vue.js 与 Node.js,是您学习如何构建一个整合 Vue.js 和 Node.js 的完整项目的地方。

要充分利用这本书

对 HTML 和 CSS 的先验知识是这本书的必备条件。

如果您正在使用这本书的数字版,我们建议您亲自输入代码或从书的 GitHub 仓库(下一节中有一个链接)获取代码。这样做将帮助您避免与代码复制粘贴相关的任何潜在错误。

下载示例代码文件

您可以从 GitHub(github.com/PacktPublishing/JavaScript-from-Frontend-to-Backend)下载本书的示例代码文件。如果代码有更新,它将在 GitHub 仓库中更新。

我们还有其他来自我们丰富的书籍和视频目录的代码包,可在github.com/PacktPublishing/找到。去看看吧!

下载彩色图片

我们还提供了一份包含本书中使用的截图和图表彩色图像的 PDF 文件。您可以从这里下载:packt.link/xdibe

使用的约定

本书使用了多种文本约定。

文本中的代码:表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称。以下是一个示例:“所以 { lastname: "Clinton" } 也可以通过将 lastname 属性用单引号或双引号包围来写成 { "lastname": "Clinton" }。”

代码块设置如下:

var p = { lastname : "Clinton", firstname : "Bill" };
console.log("The person is", p);

当我们希望您注意代码块中的特定部分时,相关的行或项目将以粗体显示:

class Person {
  firstname;
  lastname;
  age;
}
var p = new Person;
console.log(p);

粗体:表示新术语、重要单词或您在屏幕上看到的单词。例如,菜单或对话框中的单词以粗体显示。以下是一个示例:“这种写作格式也称为JavaScript 对象表示法JSON)格式。”

小贴士或重要注意事项

看起来像这样。

联系我们

我们读者的反馈总是受欢迎的。

一般反馈:如果您对本书的任何方面有疑问,请通过 customercare@packtpub.com 给我们发邮件,并在邮件主题中提及书名。

勘误:尽管我们已经尽最大努力确保内容的准确性,但错误仍然可能发生。如果您在这本书中发现了错误,我们将不胜感激,如果您能向我们报告,我们将不胜感激。请访问www.packtpub.com/support/errata并填写表格。

盗版:如果您在互联网上发现我们作品的任何形式的非法副本,我们将不胜感激,如果您能提供位置地址或网站名称,我们将不胜感激。请通过 copyright@packt.com 与我们联系,并提供材料的链接。

如果您有兴趣成为作者:如果您在某个主题上具有专业知识,并且您有兴趣撰写或为本书做出贡献,请访问authors.packtpub.com

分享您的想法

一旦您阅读了 从前端到后端的 JavaScript,我们很乐意听听您的想法!请点击此处直接访问此书的亚马逊评论页面并分享您的反馈。

您的评论对我们和科技社区都非常重要,它将帮助我们确保我们提供高质量的内容。

第一部分:JavaScript 语法

本部分解释了您在客户端或服务器上使用 JavaScript 所需了解的基本知识。它解释了 JavaScript 中的语法和主要数据类型。

本节包含以下章节:

  • 第一章,探索 JavaScript 的核心概念

  • 第二章,探索 JavaScript 的高级概念

第一章:第一章 探索 JavaScript 的核心概念

JavaScript 语言是在 1990 年代中期创建的,用于在互联网浏览器中执行,以便使网站更加流畅。最初,它被用来控制输入表单中的内容。例如,它被用来做以下事情:

  • 允许在字段中输入数字字符——并且只有数字字符。在这种情况下,其他字符,例如字母,必须被拒绝。这使得由于浏览器中包含的 JavaScript 语言,可以不验证表单的输入并避免将数据发送到服务器,在这种情况下会表明输入错误。

  • 在将表单字段发送到服务器之前,检查表单的所有必填字段是否都已填写。

这两个示例(以及其他许多示例)表明,在将用户输入的数据发送到服务器之前,检查数据的有效性是可取的。这避免了在输入的数据不正确的情况下,从浏览器到服务器的数据传输。对于更复杂的检查,例如检查两个人是否具有相同的标识符,这可以在服务器上继续进行,因为服务器可以访问所有现有的标识符。

因此,JavaScript 的目标是在其初期,让浏览器尽可能多地检查,然后将输入的信息传输到服务器进行处理。

为了这个目的,创建了一种内部浏览器语言:JavaScript 语言,其名称包含当时一个非常流行的词——“Java”(尽管 Java 和 JavaScript 这两种语言之间没有任何关系)。

多年来,开发者们有了将之与服务器端关联起来的想法,以便在客户端和服务器端使用相同的语言。这允许创建 Node.js 服务器,它今天被广泛使用。

不论是客户端还是服务器端,JavaScript 语言使用一种基本的语法,允许你编写自己的程序。这是我们将在本章中要探讨的。

在本章中,我们将涵盖以下主题:

  • JavaScript 中使用的变量类型

  • 运行 JavaScript 程序

  • 在 JavaScript 中声明变量

  • 编写条件测试的条件

  • 创建处理循环

  • 使用函数

技术要求

要在 JavaScript 中开发,并在此书中编写和运行程序,你需要以下内容:

  • 用于计算机程序的文本编辑器,例如 Notepad++、Sublime Text、EditPlus 或 Visual Studio。

  • 一个互联网浏览器,例如 Chrome、Firefox、Safari 或 Edge。

  • 一个 PHP 服务器,例如 XAMPP 或 WampServer。PHP 服务器将用于在 HTML 页面上执行包含 import 语句的 JavaScript 程序,因为这些 import 语句只能在 HTTP 服务器上工作。

  • Node.js 服务器:Node.js 服务器将通过 Node.js 安装创建。我们还将安装并使用 MongoDB 数据库,以将 Node.js 服务器与数据库关联。

  • 您可以在 GitHub 上找到本章的代码文件:github.com/PacktPublishing/JavaScript-from-Frontend-to-Backend/blob/main/Chapter%201.zip

让我们现在开始探索 JavaScript,通过研究它为我们提供的不同类型的变量。

JavaScript 中使用的变量类型

与任何语言一样,JavaScript 允许你创建用于操作数据的变量。JavaScript 是一种非常简单的语言,因此,例如,数据类型非常基础。因此,我们将以下内容作为主要数据类型:

  • 数值

  • 布尔值

  • 字符串

  • 数组

  • 对象

让我们快速浏览这些不同类型的数据。

数值

数值可以是正数或负数,甚至可以是小数形式(例如,0,-10,10.45)。所有称为实数的数学数都包含数值或数据点。

布尔值

当然,这是两种布尔值——true 或 false,这在大多数语言中都可以找到。这些值用于表达条件:如果条件为真,则执行特定过程,否则执行另一个过程。因此,条件的结果是 true 或 false 值,分别用两个值 truefalse 表示。

我们将在本章后面的“编写条件”部分中看到如何表达条件。

字符串

字符串指的是像 "a""abc""Hello, how are you?" 这样的值。空字符串将表示为 ""(连续的引号,里面没有任何内容)。注意,你可以使用双引号(")或单引号(')。因此,字符串 "abc" 也可以写成 'abc'(使用单引号)。

数组

数组,如 [10, "abc", -36],可以包含任何类型的值,例如这里我们既有数值也有字符串。空数组将表示为 [],这意味着它不包含任何值。

存储在数组中的值通过索引访问,索引从 0(用于访问数组中放置的第一个元素)到数组的长度减 1(用于访问数组的最后一个元素)。因此,如果数组 [10, "abc", -36] 通过变量 tab 表示,例如,以下情况会发生:

  • tab[0] 将允许访问数组的第一个元素:10

  • tab[1] 将允许访问数组的第二个元素:"abc"

  • tab[2] 将允许访问数组的第三个和最后一个元素:-36

    注意

    注意,你可以在数组为空的情况下向数组中添加元素。因此,如果我们访问前面数组 tab 的索引 3,我们可以写 tab[3] = "def"。因此,数组 tab 现在将是 [10, "abc", -36, "def"]

对象

对象类似于数组。它们用于存储任意信息,例如,值43"Clinton",和"Bill"。但与使用索引的数组不同,你必须指定一个名称来访问这些值中的每一个。这个名称被称为键,因此它允许访问它所代表的值。

假设之前的值43是某人的年龄,而"Clinton"是他们的姓,"Bill"是他们的名。那么我们将对象写成以下形式:{ age: 43, lastname: "Clinton", firstname: "Bill" }。对象的定义是通过花括号完成的,内部是key: value形式的键值对,由逗号分隔。这种书写格式也称为JavaScript 对象表示法JSON)格式。

因此,如果之前的对象与变量person相关联,我们可以通过编写person["age"](在这里将是43)来访问他们的年龄,但也可以编写person.age,这也会是43。同样,我们也可以编写person.lastnameperson["lastname"]以及person.firstnameperson["firstname"]来分别访问该人的姓和名。

键也被称为对象的属性。因此,age键也被称为age属性。我们可以为键选择任何名称;你只需指出键,然后使用这个名称。所以,如果你在person对象中将age指定为属性,你必须使用person.ageperson["age"]中的术语;否则它将不起作用。

注意,如果你写person[age]而不是person["age"],JavaScript 会将age视为一个具有先前定义值的变量,而在这里它不是,因此在这种情况下无法工作。你必须将age变量设置为具有值"age"才能使其工作。

数组的元素按照它们的索引顺序排列(从 0 开始,然后是 1,依此类推),而包含在对象中的元素按照每个元素指定的键顺序排列。尽管lastname键在person对象中列在firstname键之前,但这并不区分对象{ age: 43, lastname: "Clinton", firstname: "Bill" }和对象{ firstname: "Bill", lastname: "Clinton", age: 43 },因为键写入对象的顺序是不相关的。

最后,存在空对象,例如那些不包含键(因此没有值)的对象。我们以{ }的形式写一个空对象,表示里面没有任何内容。然后我们可以向一个对象添加一个或多个键,即使它最初是空的。

现在我们已经看到了 JavaScript 中使用的的主要变量类型,让我们看看如何使用它们在我们的程序中定义变量。

运行 JavaScript 程序

JavaScript 是一种可以在浏览器(Edge、Chrome、Firefox、Safari 等等)或安装了 Node.js 的服务器上执行的语言。让我们看看如何为这两种配置编写 JavaScript 程序。

在浏览器中运行 JavaScript 程序

要在浏览器中运行 JavaScript 程序,必须将 JavaScript 代码插入到 HTML 文件中。然后,该 HTML 文件将在浏览器中显示,这将导致文件中包含的 JavaScript 代码执行。

JavaScript 代码可以在 HTML 文件中以两种不同的方式指定:

  • 第一种方式是直接在 HTML 文件中的 <script></script> 标签之间编写。<script> 标签表示 JavaScript 代码的开始,而 </script> 标签表示其结束。在这两个标签之间编写的任何内容都被认为是 JavaScript 代码。

  • 第二种方式是将 JavaScript 代码写入外部文件,然后将其包含在 HTML 文件中。外部文件通过在 HTML 文件中包含一个 <script> 标签来包含,其中 src 属性指示的值是要包含在 HTML 页面中的 JavaScript 文件的名称。

让我们来看看这两种在浏览器中运行的 JavaScript 代码的编写方式。

在 标签之间编写 JavaScript 代码

使用 .html 扩展名的文件;例如,index.html 文件。这是一个传统的 HTML 文件,我们在其中插入了 <script></script> 标签,如下面的代码片段所示:

index.html 文件

<html>
  <head>
    <meta charset="utf-8" />
    <script>
alert("This is a warning message displayed by 
      JavaScript");
    </script>
  </head>
  <body>
  </body>
</html>

我们在 HTML 页面的 <head> 部分插入了 <script> 标签(及其结束标签 </script>)。<meta> 标签用于指示要使用的字符编码。在前面的代码中,我们使用了 utf-8,以便正确显示带重音的字符。

在此处插入的 JavaScript 代码非常基础。我们使用了 alert() 函数,该函数会在浏览器屏幕上显示一个对话框,显示函数第一个参数中指示的消息文本。

要运行此 HTML 文件,只需将其(通过拖放)从文件管理器移动到任何浏览器;例如,Firefox。然后会显示以下屏幕:

![图 1.1 – 在浏览器窗口中显示消息Figure 1.1 – Displaying a message in the browser window

图 1.1 – 在浏览器窗口中显示消息

<script> 标签中存在的 JavaScript 代码在 HTML 页面加载时运行。因此,alert() 函数中指示的消息会被显示出来。点击 确定 按钮验证显示的消息并继续执行 JavaScript 代码。正如我们所看到的,程序中没有任何其他内容;程序立即通过在屏幕上显示空白页面结束(因为没有将 HTML 代码插入到页面中)。

将 JavaScript 代码写入外部文件

而不是直接将 JavaScript 代码集成到 HTML 文件中,我们可以将其放在一个外部文件中,然后通过在 <script> 标签的 src 属性中指定其名称来将此文件插入到我们的 HTML 文件中。

让我们首先编写将包含 JavaScript 代码的文件。这个文件具有 .js 文件扩展名,将被命名为 codejs.js,例如,其代码如下:

codejs.js 文件(位于 index.html 同一目录下)

alert("This is a warning message displayed by JavaScript");

codejs.js 文件包含我们之前在 <script></script> 标签之间插入的 JavaScript 代码。

index.html 文件被修改以包含 codejs.js 文件,使用 <script> 标签的 src 属性如下:

index.html 文件

<html>
  <head>
    <meta charset="utf-8" />
    <script src="img/codejs.js"></script>
  </head>
  <body>
  </body>
</html>

注意

注意 <script></script> 标签的使用。它们是连续的(也就是说,它们之间没有空格或换行符),这对于代码的正常运行是必要的。

在我们接下来的示例中,我们将主要使用直接在 HTML 文件中插入 JavaScript 代码,但使用外部文件会产生相同的结果。

现在让我们解释另一种显示消息的方法,这种方法不会像之前使用 alert(message) 函数那样阻塞程序。

使用 console.log() 方法代替 alert() 函数

之前使用的 alert() 函数在 HTML 页面上显示一个窗口,JavaScript 程序会挂起等待用户在窗口中点击 确定 按钮。因此,该函数需要用户的干预才能继续程序的执行。

一种替代方法使得可以在不阻塞程序执行的情况下使用显示。这是在控制台中,使用 console.log() 方法。

注意

console.log() 写法意味着我们使用与 console 对象关联的 log() 方法。这将在下一章中详细解释。

让我们再次编写程序,这次使用 console.log() 方法而不是 alert() 函数。index.html 文件将按如下方式修改:

使用 console.log() 方法的 index.html 文件

<html>
  <head>
    <meta charset="utf-8" />
    <script>
      // display a message in the console
      console.log("This is a warning message displayed by 
      JavaScript");
    </script>
  </head>
  <body>
  </body>
</html>

注意

在 JavaScript 程序中使用注释需要将 // 放在需要注释的内容之前(在同一行上)。您也可以通过在开头和结尾使用 /**/ 来注释多行。

通过按键盘上的 F5 键来运行此程序以刷新窗口。会出现一个空白屏幕,没有任何消息。

事实上,消息只会在控制台中显示。控制台只有在您按下 F12 键时才会可见(可以通过再次按下 F12 来移除)。

注意

您可以访问网站 balsamiq.com/support/faqs/browserconsole/,该网站解释了在 F12 键无效的情况下如何显示控制台。

以下是在控制台显示的内容:

![图 1.2 – 控制台中显示的消息

![图 1.2 – 控制台中显示的消息

图 1.2 – 控制台中显示的消息

消息显示在浏览器窗口的下半部分。

现在我们已经学会了如何在浏览器中运行 JavaScript 程序,接下来让我们学习如何在 Node.js 服务器上运行 JavaScript 程序。

在 Node.js 服务器上运行 JavaScript 程序

要在 Node.js 服务器上运行 JavaScript 程序,你必须首先安装 Node.js 服务器。要安装,只需访问 nodejs.org/ 并下载和安装服务器。请注意,如果你使用 macOS,Node.js 已经安装了。

我们可以通过打开一个壳并输入命令 node -h 来验证 Node.js 的正确安装。如果命令帮助显示如下,则表示 Node.js 已正确安装:

![Figure 1.3 – node -h 命令显示帮助信息

![img/Figure_1.3_B17416.jpg]

图 1.3 – 显示帮助信息的 node -h 命令

一旦安装了 Node.js,它就可以运行你想要的任何 JavaScript 程序。你所要做的就是创建一个包含 JavaScript 代码的文件,例如,testnode.js。该文件的内容将由服务器使用 node testnode.js 命令执行。

这里是一个非常简单的 JavaScript 文件示例,它可以由 Node.js 执行:它在服务器控制台中显示一条消息。这里的“服务器控制台”代表命令解释器,你在其中输入命令以执行 testnode.js 文件:

testnode.js 文件

console.log("This is a warning message displayed by JavaScript");

让我们在前面的终端窗口中输入命令 node testnode.js

![Figure 1.4 – 运行 Node.js 程序

![img/Figure_1.4_B17416.jpg]

图 1.4 – 运行 Node.js 程序

我们看到消息直接显示在命令解释器中。

在前面的例子中,我们编写的 JavaScript 代码既可以在客户端(浏览器)运行,也可以在服务器端运行。可以提出的问题是:相同的代码是否可以在客户端和服务器端以完全相同的方式运行?

为浏览器和服务器编写的 JavaScript 代码之间的差异

虽然这两段代码很相似,但我们不能说它们是相同的,因为在两种情况下要处理的问题不同。实际上,在客户端,我们主要会想用 JavaScript 管理用户界面,而在服务器端,我们更想管理文件或数据库。因此,在这两种情况下要使用的库将不会相同。

另一方面,我们在两种情况下都找到了相同的基本语言,那就是我们将要描述的 JavaScript 语言。

在 JavaScript 中声明变量

在“JavaScript 中使用的变量类型”部分之前描述的变量类型,如我们所知,包括数值、布尔值、字符字符串、数组和对象。

JavaScript 是一种弱类型语言,这意味着你可以在任何时候更改变量的类型。例如,数值变量可以被转换成字符字符串,甚至可以变成数组。

当然,在程序中做出这样的自愿更改是不明智的,并且为了理解,保持变量的类型在整个程序中是谨慎的。然而,重要的是要知道 JavaScript 允许更改变量类型。一种名为 TypeScript 的 JavaScript 变体通过防止这些类型更改来提供更多的安全性。

现在,让我们学习如何定义变量。我们将使用以下关键字之一:constvarlet

使用 const 关键字

const 关键字用于定义一个值将保持不变的变量。任何后续尝试更改值都将产生错误。

让我们定义一个常量变量 c1,其值为 12。尝试修改其值,给它赋予一个新的值:控制台将显示错误:

注意

将我们定义一个常量变量称为语言上的滥用。我们更应该说我们在定义一个常量值。

定义常量值(index.html 文件)

<html>
  <head>
    <meta charset="utf-8" />
    <script>
      const c1 = 12;
      console.log(c1);
      c1 = 13;   // attempt to modify the value of a 
                 // constant: error
      console.log(c1);  // no display because an error 
                        // occurred above
    </script>
  </head>
  <body>
  </body>
</html>

在实现前面的代码后,我们还将看到控制台(如果控制台不可见,可以通过按 F12 键显示)中显示的错误如下:

![Figure 1.5 – 修改常量值时的错误Figure 1.5_B17416.jpg

Figure 1.5 – 修改常量值时的错误

从前面的图中我们可以看到,常量 c1 的第一次显示值为 const,关键字不应被修改。

使用 var 关键字

定义变量(其值可以修改)的另一种方式是使用 var 关键字。让我们通过以下代码示例来看看如何使用:

几个变量的定义

<html>
  <head>
    <meta charset="utf-8" />
    <script>
      var a = 12;
      var b = 56;
      var c = a + b;
      var s1 = "My name is ";
      var firstname = "Bill";
      console.log("a + b = " + a + b);
      console.log("c = " + c);
      console.log(s1 + firstname);    
    </script>
  </head>
  <body>
  </body>
</html>

我们通过在变量前加上关键字 var 并赋予它们默认值来定义变量 abs1firstname。变量 c 对应于变量 ab 的和。

注意

变量的名称由字母数字字符组成,但必须以字母字符开头。在编写变量名称时,大小写很重要(变量的名称是区分大小写的)。因此,变量 a 与变量 A 不同。

前一个程序的结果在浏览器控制台中显示(如果不可见,必须通过按 F12 键显示):

![Figure 1.6 – 使用 var 关键字Figure 1.6_B17416.jpg

Figure 1.6 – 使用 var 关键字

在前面的图中,我们可以看到一个可能看起来令人惊讶的结果。确实,a + b 的直接计算第一次显示为 1256,然后第二次显示为 68

事实上,当我们写 console.log("a + b = " + a + b); 时,我们开始通过写入 "a + b = " 来显示字符的事实意味着 JavaScript 将将显示的其余部分解释为字符字符串;特别是,位于同一行的 ab 的值。因此,ab 的值不再被解释为数值,而是作为字符字符串 1256。当这些字符字符串通过 + 运算符连接时,这并不对应于加法,而是连接。

相反,变量 c 的计算不涉及字符字符串,因此这里 a + b 的结果是变量 ab 的值的总和,因此 68

注意,同样的程序可以在 Node.js 服务器上运行。为此,我们可以在 testnode.js 文件中这样编写:

testnode.js 文件

var a = 12;
var b = 56;
var c = a + b;
var s1 = "My name is ";
var firstname = "Bill";
console.log("a + b = " + a + b);
console.log("c = " + c);
console.log(s1 + firstname);

然后,我们可以使用 node testnode.js 命令执行前面的代码。在 Node.js 下显示的结果与在浏览器控制台显示的结果相似:

![图 1.7 – 在 Node.js 下运行程序图片 1.7_B17416.jpg

图 1.7 – 在 Node.js 下运行程序

我们学习了用于定义变量的 constvar 关键字;现在我们还需要学习如何使用 let 关键字。

使用 let 关键字

要理解 let 关键字的使用并看到它与 var 关键字的区别,我们必须在我们的程序中使用大括号。大括号用于创建程序块,在其中插入指令,特别是在条件 ifelse 指令之后(我们将在 编写条件 部分看到)。

让我们写一个简单的 if(true) 条件,它总是 true:因此,条件后面的括号内的代码总是被执行:

包含条件的 index.html 文件

<html>
  <head>
    <meta charset="utf-8" />
    <script>
      var a = 12;
      if (true) {  // always executed (because always true)
        var b = 56;
        let c = 89;
        console.log("In the brace:");
        console.log("a = " + a);
        console.log("b = " + b);
        console.log("c = " + c);
      }
      console.log("After the brace:");
      console.log("a = " + a);
      console.log("b = " + b);
      console.log("c = " + c);
    </script>
  </head>
  <body>
  </body>
</html>

在前面的代码中,我们在任何大括号之外定义了变量 a。因此,一旦定义,这个变量将在任何地方(包括和不包括大括号)都是可访问的。

变量 bc 在条件之后的大括号内定义。变量 b 使用 var 定义,而变量 c 使用 let 关键字定义。两个变量之间的区别在退出大括号块时就会显现出来。确实,变量 c(由 let 定义)在其定义的大括号块外部不再被识别,而变量 b(由 var 定义)即使在块外部也是可访问的。

这可以通过在浏览器中按如下方式运行程序来检查:

![图 1.8 – 由 let 定义的变量 c 在其定义的块外部不可访问图片 1.8_B17416.jpg

图 1.8 – 由 let 定义的变量 c 在其定义的块外部不可访问

注意,同样的程序在 Node.js 服务器上也会得到类似的结果,如下面的屏幕截图所示:使用 let 定义的变量 c 在块外部变得不可知。

图 1.9 – Node.js 服务器上的相同结果

图 1.9 – Node.js 服务器上的相同结果

正如我们在前面的屏幕上所看到的,由 let 在块中定义的变量 c 在块外变得不可知。

如果我们不使用 var 或 let 来定义一个变量会怎样?

有可能不使用 varlet 关键字来定义一个变量。我们可以简单地写出变量的名称,然后写出它的值(由 = 符号分隔)。让我们通过以下示例看看这样做会怎样:

不指定 var 或 let 创建变量

a = 12;
b = 56;
console.log("a = " + a);    // displays the value 12
console.log("b = " + b);    // displays the value 56

在前面的例子中,变量在没有 varlet 前被初始化,这些变量是全局变量。一旦它们被初始化,它们就可以在程序的其他任何地方访问。当我们学习本章 使用函数 部分的函数时,这一点将变得明显。

注意

强烈建议在程序中尽可能少地使用全局变量,因为这会复杂化包含它们的程序的设计和调试。

未初始化的变量有什么价值?

前面的每个变量都是通过初始化其值(使用 = 符号,即赋值符号)来声明的。让我们看看如果我们不对变量赋值,只使用 varlet 声明会发生什么:

未初始化的变量声明

<html>
  <head>
    <meta charset="utf-8" />
    <script>
      var a;
      let b;
console.log("a = " + a);    // displays the value 
                                  // undefined
console.log("b = " + b);    // displays the value 
                                  // undefined
    </script>
  </head>
  <body>
  </body>
</html>

在前面的代码中,我们定义了两个变量,ab – 一个使用 var,另一个使用 let。这两个变量都没有初始值(也就是说,它们后面没有 = 符号)。

在这种情况下,对于这些未初始化的变量显示的结果是 JavaScript 中的一个值,称为 undefined。这对应于尚未有值的变量。undefined 值是 JavaScript 语言中的一个重要关键字。

注意

变量 ab 没有被初始化,必须使用 varlet 来声明它们。实际上,你不能简单地写 a;b;,因为这会导致运行时错误。

让我们在浏览器中运行前面的程序,并观察控制台显示的结果:

图 1.10 – 未初始化的变量是未定义的

图 1.10 – 未初始化的变量是未定义的

注意

如果使用 Node.js 服务器端的 JavaScript,undefined 值也与未初始化的变量相关联。

我们现在知道了如何在 JavaScript 中定义变量。要创建有用的 JavaScript 程序,你必须编写一系列指令。最常用的指令之一允许你使用 if 语句编写条件测试,我们将在下一节讨论这一点。

编写条件测试的条件

JavaScript 显然允许你在程序中编写条件。条件通过 if (condition) 语句表达:

  • 如果条件是 true,则执行后面的语句(或花括号中的块)。

  • 如果条件为 false,则执行 else 关键字后面的语句(如果存在)。

编写指令的形式

我们可以使用以下形式来表示条件:

使用 if (condition) 的条件表达式形式

// condition followed by a statement
if (condition) statement;   // statement executed if condition is true
// condition followed by a block
if (condition) {
  // block of statements executed if condition is true
  statement 1;   
  statement 2;   
  statement 3;   
}

使用 if (condition) … else … 的条件表达式形式

// condition followed by a statement
if (condition) statement 1;   // statement 1 executed if 
                              // condition is true
else statement 2;             // statement 2 executed if 
                              // condition is false
// condition followed by a block
if (condition) {
  // block of statements executed if condition is true
  statement 1;   
  statement 2;   
  statement 3;   
}
else {
  // block of statements executed if condition is false
  statement 5;   
  statement 6;   
  statement 7;   
}

注意

如果要执行的过程包含多个指令,这些指令将组合在一起,用大括号括起来形成一个块。一个块可以只包含一个语句,即使在这个情况下,块是可选的(不需要大括号)。

让我们在 testnode.js 文件中编写以下程序,我们将使用命令解释器中的 node testnode.js 命令来执行它,如下所示:

testnode.js 文件

var a = 12;
console.log("a = " + a);
if (a == 12) console.log("a is 12");
else console.log("a is not 12");

在前面的代码中,条件以 a == 12 的形式表达。实际上,习惯上通过连续两次重复使用等号 = 来测试两个值之间的相等性(因此是 ==)。

注意

我们使用 == 表示相等,!= 表示不等,>>= 检查大于或等于,<<= 检查小于或等于。

在前面的代码中,由于变量 a 的值为 12,可以看到以下结果:

![图 1.11 – 使用条件测试图片

图 1.11 – 使用条件测试

如果我们将值 13 赋给变量 a,则语句的 else 部分将被执行:

![图 1.12 – 运行测试的 else 部分图片

图 1.12 – 运行测试的 else 部分

我们已经看到了如何根据条件执行代码的一部分或另一部分。现在让我们研究如何编写比之前写过的更复杂的条件。

用于编写条件的表达式

之前编写的条件是两个值之间简单相等性的测试。但有时需要编写的测试可能更复杂。目标是得到条件的最终结果,即 truefalse,这将使系统能够决定下一步的行动。

条件用布尔形式书写,使用 OR 关键字(写作 ||)或使用 AND 关键字(写作 &&)。不同条件之间可能需要括号来表示最终条件,如下所示:

使用 “or” 表达的条件

var a = 13;
var b = 56;
console.log("a = " + a);
console.log("b = " + b);
if (a == 12 || b > 50) console.log("condition a == 12 || b > 50 is true");
else console.log("condition a == 12 || b > 50 is false");

在前面的代码中,由于变量 b 大于 50,条件为 true,如 图 1.13 所示。

注意

OR 条件中,只要其中一个条件为 true,最终条件就为 true

AND 条件中,所有条件都必须为 true,最终条件才为 true

![图 1.13 – 使用 or 条件图片

图 1.13 – 使用 or 条件

默认情况下,if(condition) 中表达的条件与值 true 进行比较。有时我们可能更喜欢与值 false 进行比较。在这种情况下,只需在条件前加上符号 ! 即可,这对应于对后续条件的否定。

有时需要根据前一个测试的结果连续进行几个测试。这时,我们就有了一系列的测试,称为级联测试。

嵌套测试套件

在要执行的过程中可以链式测试。以下是一个示例:

测试嵌套

var a = 13;
var b = 56;
console.log("a = " + a);
console.log("b = " + b);
if (a == 12) console.log("condition a == 12 is true");
else {
  console.log("condition a == 12 is false");
  if (b > 50) console.log("condition b > 50 is true");
  else console.log("condition b > 50 is false");
}

else 部分由多个语句组成,并放在由大括号包围的块中:

图 1.14 – 测试嵌套

图 1.14 – 测试嵌套

我们学习了如何在 JavaScript 程序中编写条件。现在我们将学习如何编写处理循环,这使得在程序中只需编写一次指令成为可能。然而,这些指令可以根据需要执行多次。

创建处理循环

有时需要多次重复一个指令(或指令块)。而不是在程序中多次编写它,我们将其放入处理循环中。这些指令将根据需要重复执行多次。

JavaScript 中有两种处理循环类型:

  • 使用 while() 语句的循环

  • 使用 for() 语句的循环

让我们来看看这两种循环类型。

使用 while() 循环

while(condition) 指令允许你重复执行后面的指令(或指令块)。只要条件为 true,就会执行该语句(或块)。当条件变为 false 时停止执行。

使用这个 while() 语句,让我们显示从 05 的数字:

显示从 0 到 5 的数字

var i = 0;
while (i <= 5) {
  console.log("i = " + i);
  i++;
}

前面的 console.log() 指令在程序中只写一次,但由于它被插入到循环(while() 指令)中,所以当条件为 true 时,它将被重复执行多次。

变量 i 允许你在循环中管理条件。每次通过循环时,变量 i 都会增加 1(通过 i++),并且当值超过 5 时我们停止:

图 1.15 – 显示数字从 0 到 5

图 1.15 – 显示数字从 0 到 5

我们可以验证这个程序在客户端(即网页浏览器)中以类似的方式工作,如下所示:

在浏览器控制台中显示数字 0–5

<html>
  <head>
    <meta charset="utf-8" />
    <script>
      var i = 0;
      while (i <= 5) {
        console.log("i = " + i);
        i++;
      }
    </script>
  </head>
  <body>
  </body>
</html>

结果在浏览器控制台中以类似的方式显示:

图 1.16 – 在浏览器控制台中显示从 0 到 5 的数字

图 1.16 – 在浏览器控制台中显示从 0 到 5 的数字

使用 for() 循环

另一种广泛使用的循环形式是使用 for() 语句的循环。它通过减少要编写的指令数量来简化前面循环的编写。

让我们用 for() 语句而不是 while() 语句编写与之前相同的程序来显示从 0 到 5 的数字:

for (var i=0; i <= 5; i++) console.log("i = " + i);

如前所述的代码所示,一行代码可以替代几行代码。

for() 语句有三个部分,由 ; 分隔:

  • 第一个对应于初始化指令。在这里,它是变量 i 的声明,初始化为 0(这是循环的开始)。

  • 第二个对应于条件:只要这个条件是 true,就会执行语句(或随后的语句块)。在这里,条件对应于变量 i 没有超过最终值 5

  • 第三个对应于每次循环迭代后执行的指令。在这里,我们通过 1 增加变量 i。这确保了在某个时刻,条件将变为 false,以便退出循环。

让我们验证它是否与 while() 语句完全相同:

图 1.17 – 使用 for()语句的循环

图 1.17 – 使用 for()语句的循环

在本节中,我们学习了如何使用 while()for() 语句编写将被多次执行的语句序列。现在让我们看看如何使用所谓的函数来组合语句。

使用函数

函数用于给一组指令命名,以便可以在程序的不同地方使用。通常,在函数中,我们将一组用于执行特定任务的指令分组,例如:

  • 显示前 10 个整数的列表。

  • 计算前 10 个数字(从 0 到 9)的和。

  • 计算前 N 个数字(从 0 到 N-1)的和。在这种情况下,N 将是函数的一个参数,因为每次调用(或使用)函数时它都可能改变(或使用)。

上文所述的函数非常简单,但展示了函数的作用是通过总结一句话来封装任何过程。赋予函数的名称象征着期望执行的动作,这允许开发者轻松理解指令序列(包括未参与开发的外部开发者)。让我们逐一讨论我们列出的三个函数。

显示前 10 个整数的函数

让我们编写第一个函数,该函数显示前 10 个整数的列表。我们将把这个函数命名为 display_10_first_integers()。名称必须尽可能明确,因为一个 JavaScript 程序由许多函数组成,这些函数的名称在程序中必须是唯一的(如果两个函数名称相同,则只考虑最后一个,因为它会覆盖前面的)。

函数是通过使用关键字 function,然后是函数的名称,然后是括号来定义的。然后,我们在随后的花括号中指示组成函数的指令。每次在程序中调用函数时,将执行这个指令块。

让我们编写函数 display_10_first_integers(),该函数显示前 10 个整数:

使用函数显示前 10 个整数(testnode.js 文件)

function display_10_first_integers() {
  for (var i=0; i <= 10; i++) console.log("i = " + i);
}

函数使用 function 关键字定义,后跟函数名和括号。

函数声明被分组在接下来的花括号之间的块中。我们找到了之前的 for() 循环作为指令,但它也可能是 while() 循环,它们的工作方式相同。

假设它包含在 testnode.js 文件中,让我们运行这个程序:

图 1.18 – 使用函数显示 1 到 10 的数字

图 1.18 – 使用函数显示 1 到 10 的数字

如前图所示,屏幕保持空白,因为在控制台中没有注册任何显示。

事实上,我们只是简单地定义了函数,但我们还必须使用它,即在程序中调用它。你可以按需多次调用它——这是函数的目的:我们应该能够在任何时间调用(或使用)它们。但至少必须调用一次;否则,它就毫无用处,如前图所示。

让我们在函数定义之后添加函数调用:

函数的定义和调用

// function definition
function display_10_first_integers() {
  for (var i=0; i <= 10; i++) console.log("i = " + i);
}
// function call
display_10_first_integers();

前面代码的结果可以在以下图中看到:

图 1.19 – 调用 display_10_first_integers() 函数

图 1.19 – 调用 display_10_first_integers() 函数

有趣的是,函数可以在程序的多个地方调用。让我们在以下示例中看看如何:

依次调用 display_10_first_integers() 函数

// function definition
function display_10_first_integers() {
  for (var i=0; i <= 10; i++) console.log("i = " + i);
}
// function call
console.log("*** 1st call *** ");
display_10_first_integers(); 
console.log("*** 2nd call *** ");
display_10_first_integers(); 
console.log("*** 3rd call *** ");
display_10_first_integers();  

在前面的代码中,函数连续调用了三次,显示前 10 个整数的列表多次。每次调用前的顺序如下所示:

图 1.20 – 依次调用 display_10_first_integers() 函数

图 1.20 – 依次调用 display_10_first_integers() 函数

计算前 10 个整数和的函数

现在我们想要创建一个函数,该函数计算前 10 个整数的和,即 1+2+3+4+5+6+7+8+9+10。结果是 55。这将使我们能够展示函数如何将结果返回到外部(即使用它的程序)。在这里,函数应该返回 55

让我们调用函数 add_10_first_integers()。它可以写成以下形式:

添加前 10 个整数的函数

// function definition
function add_10_first_integers() {
  var total = 0;
  for (var i = 0; i <= 10; i++) total += i;
  return total;
}
// function call
var total = add_10_first_integers();
console.log("Total = " + total);

我们在函数中定义了 total 变量。因为这个变量是使用 varlet 关键字定义的,所以它是函数的局部变量。这允许这个 total 变量与函数外部定义的变量不同,即使名称相同。

注意

如果函数中的 total 变量不是使用 varlet 关键字定义的,它将创建一个所谓的全局变量,该变量即使在函数外部也可以直接访问。这不是好的编程,因为你希望尽可能少地使用全局变量。

该函数使用 for() 循环来累加前 10 个整数,然后使用 return 关键字返回这个总和。这个关键字使得在函数外部可以访问任何变量的值,在我们的例子中,是 total 变量。

让我们运行之前的程序。我们应该看到以下输出:

图 1.21 – 计算前 10 个整数的和

图 1.21 – 计算前 10 个整数的和

计算前 N 个整数和的函数

之前的函数不是很实用,因为它总是返回相同的结果。一个更有用的函数是计算前 N 个整数的和,其中 N 可以在每次函数调用时不同。

在这种情况下,N 将是函数的一个参数。它的值在使用函数时在括号中指示。

让我们调用 add_N_first_integers() 函数来计算这个和。参数 N 将在函数名称后面的括号中指示。一个函数可以使用多个参数,只需按顺序用逗号分隔即可。在我们的例子中,一个参数就足够了。

让我们编写 add_N_first_integers(n) 函数,并使用它来计算前 10 个、然后 25 个、然后 100 个整数的和。在函数连续调用过程中,将使用值 10、25 和 100 作为参数,并将替换函数定义中指示的参数 n

添加前 N 个整数的函数

// function definition
function add_N_first_integers(n) {
  var total = 0;
  for (var i = 0; i <= n; i++) total += i;
  return total;
}
// calculation of the first 10 integers
var total_10 = add_N_first_integers(10);
console.log("Total of the first 10 integers = " + total_10);
// calculation of the first 25 integers
var total_25 = add_N_first_integers(25);
console.log("Total of the first 25 integers = " + total_25);
// calculation of the first 100 integers
var total_100 = add_N_first_integers(100);
console.log("Total of the first 100 integers = " + total_100);

add_N_first_integers(n) 函数与之前编写的 add_10_first_integers() 函数非常相似。它使用括号中指示的参数 n,并且不像之前那样从 010 循环,而是从 0n。根据调用函数时使用的 n 的值,循环将不同,函数返回的结果也将不同。

调用函数时,它传递参数 1025,然后 100,正如所期望的那样。函数的 total 变量返回结果,然后由函数外部的 total_10total_25total_100 变量使用:

图 1.22 – 计算前 10 个、然后 25 个、然后 100 个整数的和

图 1.22 – 计算前 10 个、然后 25 个、然后 100 个整数的和

摘要

本章介绍了 JavaScript 的基本特性:不同类型的变量、条件测试、循环和函数。它们在客户端和服务器端都得到应用。

在下一章中,我们将探讨 JavaScript 的更多深入特性,例如使用 JavaScript 进行面向对象编程。

第二章:第二章:探索 JavaScript 的高级概念

在本章中,我们将探讨 JavaScript 的高级特性,例如面向对象编程。我们还将研究 JavaScript 中广泛使用的两种类型的对象:数组和字符串。最后,我们将看到 JavaScript 如何允许你使用所谓的回调函数来触发延迟处理。

在本章中,我们将涵盖以下主题:

  • 类和对象

  • 数组

  • 字符串

  • 多任务

  • 使用承诺

所有这些主题对于构建 JavaScript 应用程序都是基本的。现在让我们开始吧!

技术要求

你可以在 GitHub 上找到本章的代码文件:github.com/PacktPublishing/JavaScript-from-Frontend-to-Backend/blob/main/Chapter%202.zip

类和对象

类和对象的概念是编程语言的基础。JavaScript 允许它们被使用。

类用于表示任何类型的数据。例如,人、客户、汽车等等。我们可以定义一个类来表示这些类型的元素,例如,一个Person类来表示人,一个Client类来表示客户,一个Car类来表示汽车。

注意

注意,类名传统上以大写字母开头。

另一方面,一个对象将是类的一个特定元素(这个元素也将被称为实例)。例如,在Person类中的所有人中,被其名字“Clinton”和名字“Bill”所标识的人代表了这个类Person的一个特定对象。这个对象可以与程序中的变量p相关联。因此,我们可以创建变量来标识与该类关联的每个对象。

定义一个类

创建类时,你需要问自己的问题是,你想要对这个数据类型执行哪些操作。

例如,如果我们创建Person类,我们应该问什么特征可以定义一个人,以及我们可以在该类上执行哪些操作。例如,我们可以说Person类由人的姓氏、名字和年龄来定义。你也可以添加地址、电话号码、电子邮件等。

至于对人的可能操作,我们可以想象,例如,与另一个人结婚的操作,搬到另一个城市的操作,更换雇主的操作等等。

注意

姓氏、名字、年龄等这样的特征被称为类的属性,而结婚、搬家等这样的操作被称为类的方法。因此,一个类将把一组属性和一组方法组合在一起。

使用class关键字后跟类名,然后是描述内容的括号来创建 JavaScript 类。例如,Person类的创建如下所示:

人员类

class Person {
}

这种 Person 类的定义现在不会很有用,因为它内部没有定义属性或方法。我们将在以后看到如何改进它。

通过使用类创建对象

一旦定义了类,我们就可以创建与该类关联的对象。为此,我们使用关键字 new 后跟类的名称。这创建了一个表示该类对象的变量:

创建 Person 类的对象 p

// define the Person class
class Person {
}
// create an object of class Person
var p = new Person;  // object p of class Person
console.log(p);

这是你将看到的内容:

图 2.1 – 创建 Person 类对象

图 2.1 – 创建 Person 类对象

p 对象在控制台中显示。我们被告知它是一个 Person 类对象,它是空的 {}。对象以花括号形式表示的传统在 JavaScript 中,正如我们在上一章的 JavaScript 中使用的变量类型 部分中看到的。

我们可以验证它也可以在客户端,在浏览器中工作。HTML 文件如下:

index.html 文件

<html>
  <head>
    <meta charset="utf-8" />
    <script>
      class Person {
      }
      var p = new Person;
      console.log(p);
    </script>
  </head>
  <body>
  </body>
</html>

图 2.2 – 在浏览器中创建对象

图 2.2 – 在浏览器中创建对象

我们找到了花括号的显示,这表示了 JavaScript 对象的显示。

不使用类创建对象

没有先创建类,也可以创建对象。你只需使用花括号 {} 的符号。

例如,我们可以编写以下内容:

使用花括号符号创建对象

var p = { lastname : "Clinton", firstname : "Bill" };
console.log("The person is", p);

这将创建具有 lastnamefirstname 属性的对象 p。请注意,你可以通过将属性名称用引号括起来或不括起来来指定属性名称。因此,{ lastname: "Clinton" } 也可以写成 { "lastname": "Clinton" },通过将 lastname 属性用单引号或双引号包围。

现在让我们看看如何通过向其中添加属性和方法来改进之前创建的 Person 类。

向类中添加属性

在我们的例子中,一个人有一个姓氏、一个名字和一个年龄。我们将为 Person 类的人创建这三个属性。

你只需在 Person 类的主体中按名称指示这些属性即可。首先,不要使用 varlet 关键字来定义它们:

在 Person 类中添加 firstname、lastname 和 age 属性

class Person {
  firstname;
  lastname;
  age;
}
var p = new Person;
console.log(p);

图 2.3 – 在 Person 类中创建 lastname、firstname 和 age 属性

图 2.3 – 在 Person 类中创建 lastname、firstname 和 age 属性

Person 类对象 p 现在具有在类中添加的属性。这个类的任何其他对象也将具有它们。

注意,添加的属性值是 undefined。这是正常的,因为这些属性在 p 对象或 Person 类中尚未指定值。

让我们修改 Person 类,以便属性具有默认值,而不是 undefined

具有默认值的属性

class Person {
  firstname = "";
  lastname = "";
  age = 0;
}
var p = new Person;
console.log(p);

每个属性都使用其默认值进行初始化。lastnamefirstname 属性使用空字符串 "" 进行初始化,而 age 默认初始化为 0

![图 2.4 – 具有默认值的属性![图 2.4 – 使用 display() 方法图 2.4 – 具有默认值的属性一个类既有属性,也有方法。现在让我们看看如何向类中添加方法。## 向类中添加方法您可以向类中添加方法。从该类(使用 new)创建的对象将能够直接使用这些方法。例如,让我们创建一个 display() 方法,该方法显示包含人员的名字和姓氏的文本行。指令 p.display()(假设 pPerson 类对象)用于显示与对象 p 相关的人员的姓氏和名字:在 Person 类中创建 display() 方法jsclass Person {``````js  // class properties``````js  firstname = "";``````js  lastname = "";``````js  age = 0;``````js  // class methods``````js  display() {``````jsconsole.log("The person's lastname is = " + ``````js                this.lastname +``````js                ", firstname = " + this.firstname);``````js  }``````js}``````jsvar p = new Person;``````jsconsole.log("Variable p = ", p);``````jsp.display();  // use of the display() method on the p object类的属性可以通过在它们前面加上关键字 this 在类的方法中访问。例如,this.lastname 提供了对类中 lastname 属性的访问。this 关键字指的是使用 display() 方法的对象本身,因此在这里是 p 对象。如果您省略 this 关键字并直接使用 lastname 属性,您将得到一个语法错误,因为属性只有在使用 this 关键字时才是可访问的。上述代码片段的输出显示在此处:![图 2.5 – 使用 display() 方法![图 2.5 – 使用 display() 方法图 2.5 – 使用 display() 方法display() 方法显示与变量 p 相关人员的 firstnamelastname,但由于 lastnamefirstname 已经被初始化为空字符串,因此没有显示姓氏或名字。让我们看看如何修改属性的值。## 修改对象的属性值您可以通过直接使用这些属性来修改对象的属性值,例如,p.lastname 允许您读取或修改对象 plastname 属性的值:初始化人员的 lastnamefirstname````jsclass Person {``````js  // class properties``````js  lastname = "";``````js  firstname = "";``````js  age = 0;``````js  // class methods``````js  display() {``````js    console.log(" The person's lastname = " + this.lastname +``````js                ", firstname = " + this.firstname);``````js  }``````js}``````jsvar p = new Person;``````jsp.lastname = "Clinton";  // initialization of the lastname ``````js                         // property of the object p``````jsp.firstname = "Bill";    // initialization of the firstname ``````js                         // property of the object p``````jsconsole.log("Variable p = ", p);``````jsp.display();```这就是您将看到的内容:![图 2.6 – 初始化 lastnamefirstname属性![图 2.6 – 初始化lastnamefirstname属性图 2.6 – 初始化lastnamefirstname属性一旦使用new操作符创建了对象p,我们就将其 lastnamefirstname属性初始化为指示的值。这里没有修改age属性,因此它将保持等于值 0。我们使用p.lastnamep.firstname修改了使用new创建的对象plastnamefirstname属性的值。这种属性值的修改是在对象p创建之后进行的。在创建对象的过程中进行这种修改也是可能的,这需要定义一个名为constructor() 的方法,它允许这种初始化。## 使用类构造函数constructor()方法被称为类的构造函数。如果类中存在constructor()方法,则在每次使用new 语句创建对象时都会自动调用它。如果我们想在每次在这个类中创建对象时执行特定的过程,我们就在类中定义它。constructor()方法可以有任何数量的参数,也可以没有任何参数。这里指示的参数将用于初始化人的lastnamefirstname 属性:使用构造函数创建 Person 类```jsclass Person {``````js  // class properties``````js  lastname = "";``````js  firstname = "";``````js  age = 0;``````js  // class methods``````js  constructor(lastname, firstname, age) {``````js    this.lastname = lastname;``````js    this.firstname = firstname;``````js    this.age = age;``````js  }``````js  display() {``````js    console.log(" The person's lastname = " + this.lastname +``````js                ", firstname = " + this.firstname);``````js  }``````js}``````jsvar p = new Person("Clinton", "Bill");``````jsconsole.log("Variable p = ", p);``````jsp.display();````constructor() 方法是通过提供三个参数 lastnamefirstnameage 来定义的。它们通过 this.lastnamethis.firstnamethis.age 的方式被转移到对象的属性中。最后,通过将 lastnamefirstnameage 的值作为参数传递给 new 来创建对象 p。在这里,agenew 指令中没有指定参数;因此,它将是一个 undefined 值,将被传递给构造函数。图 2.7 – 使用构造函数

图 2.7 – 使用构造函数

我们发现 lastnamefirstname 属性已初始化,但 age 属性现在初始化为 undefined 而不是 0。要为其分配另一个值,只需在创建对象时传递一个额外的值。这个额外的值将代表人的年龄,例如:

在创建 Person 类对象时使用年龄

class Person {
  // class properties
  lastname = "";
  firstname = "";
  age = 0;
  // class methods
  constructor(lastname, firstname, age) {
    this.lastname = lastname;
    this.firstname = firstname;
    this.age = age;
  }
  display() {
    // the age of the person is also displayed
    console.log("The person's lastname = " + this.lastname +
", firstname = " + this.firstname + 
                ", age = " + this.age);         
  }
}
var p = new Person("Clinton", "Bill", 33);    // age is now 
                                              // transmitted
console.log("Variable p = ", p);
p.display();

图 2.8 – 人的年龄现在被传输

图 2.8 – 人的年龄现在被传输

我们已经看到了如何通过直接使用类定义其属性和方法来创建一个对象。然而,我们也可以从一个对象创建另一个对象。让我们看看如何做到这一点。

合并一个对象与另一个对象

有可能存在需要从一个旧对象创建新对象的情况。让我们看看如何做到这一点。

如果对象 p 包含一个值,则语句 var p2 = p 并不会创建一个新的对象 p2,它与对象 p 是不同的,而只是一个指向与引用 p 相同值的引用 p2。因此,对对象 p 的属性所做的任何修改都将反映在对象 p2 中,因为它们都指向相同的内存位置。

这可以通过以下示例进行验证:

修改内存中的对象

var p = { lastname : "Clinton", firstname : "Bill" };
console.log("p (before modification of p2) =", p);  
       // p = { lastname : "Clinton", firstname : "Bill" }
var p2 = p;
p2.city = "Washington";
console.log("p (after modification of p2) =", p);  
       // p = { lastname : "Clinton", firstname : "Bill", 
       // city : "Washington"}
console.log("p2 =", p2);  
       // p2 = { lastname : "Clinton", firstname : "Bill", 
       // city : "Washington"}

即使只修改了 p2 对象,p 对象也会被修改,因为它们是内存引用,指向相同的位置。如果内存位置的内容发生变化,两个引用都会看到相同的变化。

为了避免这种情况,不需要编写 p2 = p,而是将对象 p 的属性复制到对象 p2 的属性中,从而创建一个新的内存位置。为此,JavaScript 提供了扩展运算符,其形式为 ,它允许这样做:

使用扩展运算符 ...

var p = { lastname : "Clinton", firstname : "Bill" }
console.log("p (before modification of p2) =", p);
var p2 = { ...p};   // copy the properties of object p into 
                    // object p2
p2.city = "Washington";
console.log("p (after modification of p2) =", p);
console.log("p2 =", p2);

扩展运算符通过在大括号{}周围包围原始对象,并在对象前使用扩展运算符(例如,{...p})来使用。

图 2.9 – 使用扩展运算符...

图 2.9 – 使用扩展运算符...

当对象p2被修改时,对象p不再被修改。

也可以用简化的形式来写:

从对象p创建对象p2,添加城市

// to avoid writing p2.city = "Washington"
var p2 = { ...p, city : "Washington" };  

现在我们已经了解了类和对象以及如何使用它们,让我们看看一个重要的类对象:Array类。

数组

数组按照它们的索引顺序存储数据集合。索引也称为数组的索引。它从 0 开始,增加到数组的总元素数减 1(0 到 n-1)。

让我们首先学习如何创建一个数组。

使用方括号创建数组

在 JavaScript 中,数组对应于Array类对象。因此,我们使用new Array指令来创建一个数组。

然而,由于数组在 JavaScript 程序中广泛使用,因此也可以使用方括号表示法[]来创建它们。这是一种更简单的方法,无需通过Array类即可使用它们。

让我们详细看看这两种创建数组的方式(使用方括号和使用Array类)。

使用方括号[和]创建数组

创建数组最简单、最快的方法是使用方括号表示法:

使用方括号创建数组

var tab = ["Element 1", "Element 2", "Element 3", "Element 4", "Element 5"];
console.log(tab);

数组以一个开方括号[开始,以一个闭方括号]结束。数组中的元素由逗号分隔。我们在这里插入的是字符串元素,但实际上,任何类型的元素都可以插入到数组中。

图 2.10 – 插入到数组中的元素

图 2.10 – 插入到数组中的元素

注意,可以创建一个空数组(没有任何元素)。我们将其写为[],不在方括号内指定任何元素。然后就可以向这个数组中添加元素。

使用Array类创建数组

您还可以使用Array类来创建数组。Array类包括一个构造函数,其中我们指定数组元素的列表,每个元素之间由逗号分隔。

可以通过new Array语句以以下方式创建与之前相同的数组:

使用new Array创建数组

var tab = new Array("Element 1", "Element 2", "Element 3", "Element 4", "Element 5");
console.log(tab);

图 2.11 – 使用创建数组

图 2.11 – 使用new Array创建数组

创建的数组与之前相同。

要创建一个空数组,只需在构造函数中不传递任何参数,如下所示:

使用new Array()创建空数组

var tab = new Array();    // or new Array;
console.log(tab);

![图 2.12 – 创建空数组 []

](https://github.com/OpenDocCN/freelearn-html-css-js-zh/raw/master/docs/js-fe-be/img/Figure_2.12_B17416.jpg)

图 2.12 – 创建空数组 []

现在我们已经看到了如何创建一个数组,接下来让我们看看如何访问它的每个元素。

访问数组元素

在之前的程序中,我们使用console.log(tab)语句显示了整个数组。可以单独访问数组的每个元素。每个元素可以按以下方式访问:

  • 通过索引

  • 使用for()循环

  • 使用forEach()方法

让我们看看这三种方法中的每一种。

通过索引访问元素

让我们以之前包含五个元素的数组为例,即tab = ["元素 1", "元素 2", "元素 3", "元素 4", "元素 5"]

  • 第一个元素可以通过其索引 0 访问,即tab[0]

  • 下一个,索引为 1 的,将通过tab[1]访问。

  • 最后一个,索引为 4 的,将通过tab[4]访问。

这是您显示每个元素的方式:

通过索引显示数组中的每个元素

var tab = ["Element 1", "Element 2", "Element 3", "Element 4", "Element 5"];
console.log("tab =", tab);
console.log("tab[0] =", tab[0]);
console.log("tab[1] =", tab[1]);
console.log("tab[2] =", tab[2]);
console.log("tab[3] =", tab[3]);
console.log("tab[4] =", tab[4]);
console.log("tab[5] =", tab[5]);

结果将在以下图中显示:

![Figure 2.13 – 通过索引显示每个元素img/Figure_2.13_B17416.jpg

图 2.13 – 通过索引显示每个元素

该数组包含五个元素,这意味着索引从 0 到 4。然而,为了进行测试,我们也访问了索引为 5 的元素。可以访问数组中不存在的元素的索引。在这种情况下,结果是 JavaScript 值undefined,这意味着此元素的值尚未分配。

注意,使用这种访问方法可以修改数组元素的值——只需给它一个新的值:

修改数组索引 2 和 3 中元素的值

var tab = ["Element 1", "Element 2", "Element 3", "Element 4", "Element 5"];
console.log("Array before modification");
console.log("tab =", tab);
// modification of elements, index 2 and 3
tab[2] = "New element 3";
tab[3] = "New element 4";
console.log("Array after modification");
console.log("tab =", tab);

这是结果:

![Figure 2.14 – 修改数组元素img/Figure_2.14_B17416.jpg

图 2.14 – 修改数组元素

接下来,我们将查看使用for()while()循环访问元素。

使用 for()或 while()循环访问元素

在上一章中已经研究过的for()while()循环允许您浏览数组的所有元素。循环的索引从 0 开始(为了访问数组的第一个元素,即索引为 0 的元素)并结束于数组的最后一个索引。

要知道最后一个索引,JavaScript 在Array类中提供了length属性,这允许我们知道数组中元素的总数。最后一个索引将是值为length – 1的索引:

使用 for()循环访问数组元素

var tab = ["Element 1", "Element 2", "Element 3", "Element 4", "Element 5"];
console.log("tab =", tab);
console.log("Access to each element by a for() loop");
for (var i = 0; i < tab.length; i++) console.log("tab[" + i + "]=", tab[i]);

注意,循环的结束是通过测试值i < tab.length来写的。这相当于写i <= tab.length – 1

![Figure 2.15 – 使用 for()循环访问数组元素img/Figure_2.15_B17416.jpg

图 2.15 – 使用 for()循环访问数组元素

接下来,我们将查看使用forEach(callback)方法访问元素。

使用 forEach(callback)方法访问元素

forEach(callback)方法是由 JavaScript 在Array类上定义的方法。它通过将数组的每个元素传递给作为参数传递的函数来遍历数组的元素。因此,作为参数指定的函数可以访问数组的每个元素(如果需要,还可以访问其索引)。

回调函数

在 JavaScript 中,将函数作为方法参数的指示原则非常常见。参数中的函数被称为回调函数,这意味着实际要执行的处理是在回调函数中指定的。

我们在这里展示了如何使用forEach(callback)方法参数中指定的回调函数。

我们使用之前看到的五个元素的tab数组,并对其应用forEach()方法:

使用forEach()方法访问数组元素

var tab = ["Element 1", "Element 2", "Element 3", "Element 4", "Element 5"];
console.log("tab =", tab);
console.log("Access to each element by the forEach() method");
tab.forEach(function(elem, i) {
  console.log("tab[" + i + "]=", elem);
});

我们将函数作为forEach()方法的参数。这个所谓的回调函数将由 JavaScript 自动为tab数组(使用forEach()方法)的每个元素调用。

回调函数将其第一个参数作为被调用的函数的数组元素(参数elem),以及其索引(参数i)。

Figure 2.16 – Accessing array elements using the forEach() method

Figure 2.16 – B17416.jpg

图 2.16 – 使用forEach()方法访问数组元素

结果与for()循环得到的结果相同。然而,我们立即发现了一个(小)差异。

for()循环和forEach()方法的区别

之前的程序在访问数组元素时没有显示for()循环和forEach()方法结果之间的任何差异。

为了展示这两种方法之间的区别,让我们在数组中引入一个新的元素,在索引 10 处,我们知道在创建数组时使用的最后一个索引是 4。因此,我们创建了一个比数组当前最后一个元素远得多的新元素。数组会如何对这个扩展做出反应?

在索引 10 处添加元素

// original array
var tab = ["Element 1", "Element 2", "Element 3", "Element 4", "Element 5"];
// adding a new element in the array, at index 10
tab[10] = "Element 9";
console.log("tab =", tab);
// display the array with a for() loop
console.log("Access to each element by a for() loop");
for (var i = 0; i < tab.length; i++) console.log("tab[" + i + "]=", tab[i]);
// display the array by the forEach() method
console.log("Access to each element by the forEach() method");
tab.forEach(function(elem, i) {
  console.log("tab[" + i + "]=", elem);
});

我们使用tab[10] = "Element 9"向数组中添加一个元素,然后使用for()循环和forEach()方法显示数组的所有内容。

结果显示在下图中:

Figure 2.17 – Adding an element at index 10 of the array

Figure 2.17 – B17416.jpg

图 2.17 – 在数组索引 10 处添加元素

for()循环的显示表明索引 5 到 9 的元素存在,但它们的值为undefined,因为实际上没有为这些数组的索引插入任何值。然而,for()循环显示了具有undefined值的索引 5 到 9。

相反,forEach()方法只提供参数中指定的回调函数,该回调函数具有数组中实际受影响的数组元素。因此,避免了索引 5 到 9 的元素,这些元素在程序中没有分配。

我们已经看到了如何创建一个数组,然后如何访问其每个元素。现在让我们看看如何向数组中添加新元素。

向数组中添加项目

一旦创建数组(空或非空),就可以向其中添加元素。我们将主要使用以下两种技术之一:

  • 在数组中通过索引添加元素

  • 使用 push() 方法添加项目

现在,让我们来看看这两种技术。

通过索引添加元素

这对应于赋值 tab[i] = value。我们之前在写 tab[10] = "Element 9" 时使用了它。

注意,如果使用的索引大于数组中当前元素的数量,这将通过创建初始化为值 undefined 的新元素来扩大数组。如果使用的索引小于数组中的元素数量,它将修改目标元素的当前值。

使用 push() 方法添加元素

push() 方法定义在 Array 类中。它允许你添加一个新元素到数组中,而无需担心插入索引,因为它会自动将元素插入数组的末尾:

使用 push() 方法插入元素

// original array
var tab = ["Element 1", "Element 2", "Element 3", "Element 4", "Element 5"];
// insert an element using the push() method
tab.push("Element 6");
console.log("tab =", tab);
// display the array with a for() loop
console.log("Access to each element by a for() loop");
for (var i = 0; i < tab.length; i++) console.log("tab[" + i + "]=", tab[i]);
// display the array by the forEach() method
console.log("Access to each element by the forEach() method");
tab.forEach(function(elem, i) {
  console.log("tab[" + i + "]=", elem);
});

指令 tab.push("Element 6") 将此元素插入数组的末尾。然后使用之前看到的各种方法显示数组。

图 2.18 – 使用 push() 方法添加元素

图 2.18 – 使用 push() 方法添加元素

我们知道如何添加和修改数组中的元素。剩下的就是知道如何从数组中删除元素。

删除数组元素

JavaScript 允许我们以两种方式删除数组元素:

  • 删除数组中元素的值,同时保留具有 undefined 值的元素在数组中

  • 从数组中删除元素本身

现在我们来探讨这两种可能性。

删除元素值(不删除数组中的元素)

我们使用 delete 关键字来删除数组中元素的值。例如,delete tab[0] 通过将其赋值为 undefined 来删除数组 tab 中索引为 0 的元素的值。元素没有被从数组中删除,数组仍然保留与之前相同的元素数量:

删除索引为 0 的元素的值

// original array
var tab = ["Element 1", "Element 2", "Element 3", "Element 4", "Element 5"];
// delete the value of the element with index 0
delete tab[0];
console.log("tab =", tab);
// display the array with a for() loop
console.log("Access to each element by a for() loop");
for (var i = 0; i < tab.length; i++) console.log("tab[" + i + "]=", tab[i]);
// display the array by the forEach() method
console.log("Access to each element by the forEach() method");
tab.forEach(function(elem, i) {
  console.log("tab[" + i + "]=", elem);
});

图 2.19 – 删除索引为 0 的元素的值

图 2.19 – 删除索引为 0 的元素的值

我们可以看到,for() 循环显示了元素的 undefined 值,而 forEach() 方法不再显示元素,因为它的值已经被删除。

注意

注意,如果我们不使用 delete tab[0],而是使用 tab[0] = undefined,则 forEach() 方法将索引 0 的元素显示为数组的第一个元素,因为元素的值实际上并没有被删除,而是被分配了新的值,这里为 undefined

现在我们来看第二种从数组中删除元素的方法。

从数组中删除元素

使用 delete 关键字不会从数组中删除元素,数组保留相同的元素数量。

Array 类中定义的 splice(begin, count) 方法允许你从数组中物理删除元素,因此在使用后数组将至少少一个元素。

splice(begin, count) 方法包含 begincount 参数,这允许你指定从哪个索引开始删除(begin 参数)元素,以及你想要删除的连续元素的数量(count 参数)。

因此,要从数组 tab 中删除索引为 0 的元素,只需编写 tab.splice(0, 1)

使用 splice() 方法删除数组中的索引为 0 的元素

// original array
var tab = ["Element 1", "Element 2", "Element 3", "Element 4", "Element 5"];
// remove 1 element from index 0
tab.splice(0, 1);
console.log("tab =", tab);
// display the array with a for() loop
console.log("Access to each element by a for() loop");
for (var i = 0; i < tab.length; i++) console.log("tab[" + i + "]=", tab[i]);
// display the array by the forEach() method
console.log("Access to each element by the forEach() method");
tab.forEach(function(elem, i) {
  console.log("tab[" + i + "]=", elem);
});

这是你将看到的内容:

图 2.20 – 删除索引为 0 的元素

图 2.20 – 删除索引为 0 的元素

我们已经看到了如何在数组中添加和删除元素。现在让我们看看如何从当前数组中的元素提取一个新的数组。

在数组中过滤元素

过滤数组元素是常见的操作,例如,保留特定元素或返回新的元素。Array 类有两个方法——filter(callback)map(callback)——允许我们根据条件返回一个新数组。

使用 filter(callback) 方法

tab.filter(callback) 方法返回一个新数组,同时只保留 tab 数组中所需的元素。

形式为 callback(element, index) 的回调函数会对数组 tab 的每个元素进行调用。如果决定保留该元素,则必须返回 true;否则,该元素将被排除。tab.filter() 方法返回一个新数组作为结果,但原始 tab 数组不会被修改(除非在方法返回时将其赋值,如下例所示)。

让我们使用 filter() 方法来保留数组中索引大于或等于 2 的元素:

使用 filter() 方法

// original array
var tab = ["Element 1", "Element 2", "Element 3", "Element 4", "Element 5"];
console.log("initial tab =", tab);
// keep only items with index >= 2
tab = tab.filter(function(element, index) {
  if (index >= 2) return true;   // keep this element
});
console.log("\nfinal tab =", tab);

如果回调函数返回 true,则保留该元素;否则,将其排除。回调函数也可以返回 false,甚至可以返回空值,就像这里一样,在这种情况下,该元素将被排除:

图 2.21 – 使用 filter() 方法

图 2.21 – 使用 filter() 方法

这就结束了 filter() 方法的介绍。

使用 map(callback) 方法

tab.map(callback) 方法用于从初始 tab 数组的元素返回一个新数组。初始数组中的每个元素都会传递给形式为 callback(element, index) 的回调函数,该回调函数必须为每个元素返回一个新元素,该元素将替换原始元素。

让我们使用 map(callback) 方法来返回一个新数组,其中所有元素都已大写:

使用 map() 方法

// original array
var tab = ["Element 1", "Element 2", "Element 3", "Element 4", "Element 5"];
console.log("initial tab =", tab);
// capitalize all elements
tab = tab.map(function(element, index) {
  return element.toUpperCase();
});
console.log("\nfinal tab =", tab);

toUpperCase() 方法是在 String 类上定义的方法(以下截图),允许你将使用该方法字符字符串大写。

结果显示在下图中:

图 2.22 – 使用 map() 方法

图 2.22 – 使用 map() 方法

在本节中,我们研究了使用 Array 类的对象。JavaScript 中还有另一类广泛使用的对象:字符串,它们由 String 类表示。现在让我们看看如何使用 String 类的对象。

字符串

字符串在编程语言中被广泛使用。它们用于表示用户输入的文本或将要显示给用户的文本。

创建一个字符串

字符串由 String 类的对象表示。但由于字符串在 JavaScript 中被广泛使用,因此语言允许通过用双引号 " " 或单引号 ' ' 包围它们来使用它们。在某些情况下,也可以使用反引号(反向引号 ' ')。

注意

在这种情况下,字符串字面量必须以相同类型的引号开始和结束。

现在让我们看看如何使用这些不同的方法创建字符串。

使用双或单引号创建字符串字面量

创建字符串字面量的最简单方法是使用单引号或双引号记法:

使用双引号创建字符串字面量

var s = "String 1";
console.log("s =", s);

或者,使用单引号:

使用单引号创建字符串字面量

var s = 'String 1';
console.log("s =", s);

在这两种情况下,显示的字符字符串是相同的。

![图 2.23 – 创建字符字符串图片

图 2.23 – 创建字符字符串

使用单/双引号选项的优势

如果字符串本身包含引号,使用单或双引号的可能性优势就显而易见了。例如,如果字符串是 "I'll love JavaScript",使用单引号创建字符串将产生错误,因为字符串将被假定以单词 I'll 中的撇号结束。在这种情况下,你必须使用双引号以避免错误。

使用反引号创建字符串字面量

你也可以使用反引号。这在需要以更简单的方式在字符串中使用变量的值时非常有用。

例如,假设你想显示一个包含一个人的姓和名的字符串。姓和名分别存储在名为 lastnamefirstname 的变量中:

连接字符串和变量

var lastname = "Clinton";
var firstname = "Bill";
// old way of concatenating strings and variables
var s1 = "lastname is " + lastname + ", firstname is " + firstname;
// new way of concatenating strings and variables
var s2 = `lastname is ${lastname}, firstname is ${firstname}`;
console.log("s1 =", s1);
console.log("s2 =", s2);

当使用反向引号时,不再使用 + 符号来连接字符串和变量。所有内容都写在一个字符串中,变量通过“符号” ${variable} 来识别。

大括号 {} 之间可以是一个简单的变量(如这里所示),也可以是一个更复杂的可以计算的 JavaScript 表达式(例如,{a+b})。

我们可以看到,这两个结果字符串是相同的。

![图 2.24 – 使用 String 类创建字符串的字符字符串和变量序列图片

图 2.24 – 使用 String 类创建字符串的字符字符串和变量序列

最后,可以使用 String 类来创建字符字符串。String 类有一个构造函数,其中要构造的字符串作为参数指示:

使用 String 类

var s = new String("I'll love JavaScript");
console.log("s =", s);

下图显示了结果:

图 2.25 – 使用 String 类

图 2.25 – 使用 String 类

String 类具有属性和方法。例如,length 属性可以让你知道字符串中的字符数,因此可以比较,例如,两个字符字符串的长度。

让我们使用 length 属性来显示使用引号和 String 类创建的两个字符串的长度:

使用 String 类的 length 属性

var s1 = new String("I'll love JavaScript");
var s2 = "I'll love JavaScript";
console.log("s1 =", s1);
console.log("s2 =", s2);
console.log("s1.length =", s1.length);
console.log("s2.length =", s2.length);

这是结果:

图 2.26 – 使用 String 类的 length 属性

图 2.26 – 使用 String 类的 length 属性

无论字符串是如何创建的,它的长度都是相同的(这里,20 个字符)。我们已经看到了如何创建字符字符串,现在让我们看看如何访问组成它的字符。

访问字符串中的字符

String 类定义了用于访问字符串中字符的方法。特别是,这些方法是 charAt(index)slice(start, end)charAt(index) 用于检索字符串中索引指示的字符,从索引 0 开始。最大索引是 length 属性的值减去 1。slice(start, end) 通过提取从 start 索引(包含)到 end 索引(排除)的字符来将字符串分割成子字符串。

使用 charAt(index) 方法

让我们使用 charAt(index) 方法逐个显示字符串中的字符:

显示字符串中的字符

var s = "Hello";
console.log("s =", s);
for (var i = 0; i <s.length; i++) console.log(`s.charAt(${i}) = ${s.charAt(i)}`);

注意使用反引号来显示结果字符串。

结果显示在下图中:

图 2.27 – 使用 charAt() 方法

图 2.27 – 使用 charAt() 方法

现在,让我们看看 slice(start, end) 方法。

使用 slice(start, end) 方法

之前的 charAt(index) 方法从字符串中检索单个字符,而 slice(start, end) 方法可以检索多个连续的字符:

注意

注意,slice(start, end) 方法不会修改应用该方法的原字符串,而是返回一个新的字符串。原始字符串不会被修改,因此可以保持完整。

在“Hello”字符串上使用 slice()

var s = "Hello";
console.log("s =", s);
console.log(`s.slice(0,2) = ${s.slice(0,2)}`);
console.log(`s.slice(0,3) = ${s.slice(0,3)}`);
console.log(`s.slice(1,3) = ${s.slice(1,3)}`);
console.log(`s.slice(0,-1) = ${s.slice(0,-1)}`);
console.log(`s.slice(0,-2) = ${s.slice(0,-2)}`);
console.log(`s.slice(1,-2) = ${s.slice(1,-2)}`);

如果 slice(start, end) 方法的 end 索引(第二个参数)是负数,这意味着计数从字符串的末尾开始(如果它是正数,则从开头开始)。

我们得到以下结果:

图 2.28 – 使用 slice() 方法

图 2.28 – 使用 slice() 方法

现在我们已经看到了如何获取组成字符串的字符,让我们看看如何修改字符串。

修改字符字符串

要修改字符串,只有一个可能性:必须从它构建一个新的字符串。原始字符串不能直接更改。

因此,我们将使用之前的slice()charAt()方法,这将使我们能够提取原始字符串的部分,以便构建结果字符串。

但为了搜索或修改字符字符串的部分,最好使用正则表达式。我们将在下面学习它们。

使用正则表达式

正则表达式与字符串相关。它们用于检查字符串是否具有某种格式(例如,电子邮件格式、电话号码格式等),或者用其他字符替换这些格式的字符。

因此,String类有match(regexp)方法来检查字符字符串是否具有特定的格式,以及replace(regexp, str)方法来将此格式中的字符串部分替换为新字符串str

在这两种方法中,regexp参数对应于正则表达式,其含义我们将在下面学习。

检查字符串是否具有特定的格式

match(regexp)方法用于检查方法所用的字符字符串是否与regexp中指示的格式相符。regexp参数被称为正则表达式。

正则表达式

正则表达式是由//包围的字符序列,例如,/abc/。正则表达式/abc/表示我们在字符字符串中寻找字符序列abc。如果字符串包含序列abc,则match(/abc/)方法将返回此字符序列作为结果,否则返回值null

正则表达式的完整描述可以在developer.mozilla.org/fr/docs/Web/JavaScript/Reference/Global_Objects/RegExp找到。

下面是一些正则表达式的示例,以及当在字符串"Hello"上使用match()方法时返回的值:

使用 match(regexp)

var s = "Hello";
console.log("s =", s);
// search for "Hel"
console.log(`s.match(/Hel/) = ${s.match(/Hel/)}`);
// search for "hel"
console.log(`s.match(/hel/) = ${s.match(/hel/)}`);  
// search for "hel" ignoring upper/lower case
console.log(`s.match(/hel/i) = ${s.match(/hel/i)}`);
// search for H followed by a or b or e followed by l
console.log(`s.match(/H[abe]l/) = ${s.match(/H[abe]l/)}`);
// search for He followed by 0 or 1 a followed by l
console.log(`s.match(/Hea?l/) = ${s.match(/Hea?l/)}`);
// search for He followed by 0 (min) to 1 (max) followed by l
console.log(`s.match(/Hea{0,1}l/) = ${s.match(/Hea{0,1}l/)}`);
// search for He followed 1 (min) to 2 (max) followed by l
console.log(`s.match(/Hea{1,2}l/) = ${s.match(/Hea{1,2}l/)}`);

当正则表达式在"Hello"字符串中找到时,match()方法将返回找到的字符串部分,否则返回null

正则表达式末尾的i标志表示必须忽略大小写字母。

一系列字母周围的方括号[]表示只需要这些字母中的一个。

问号?表示前面的字符是可选的(它可以出现,也可以不出现)。

大括号{min,max}表示前面的字符至少出现min次,最多出现max次。

之前程序的输出如下:

![Figure 2.29 – 使用正则表达式Figure 2.29 – 使用正则表达式

图 2.29 – 使用正则表达式

注意

编写正则表达式有时可能比较复杂。网站regex101.com/允许您测试您想要的正则表达式。

正则表达式也可以使用 replace() 方法修改字符字符串的部分。

使用 replace() 方法替换字符串的一部分

replace(regexp, str) 方法用于将字符串中符合正则表达式 regexp 格式的部分替换为字符串 str。它返回一个新的字符串,而原始字符串不会被修改。如果正则表达式指示的格式未找到,则返回原始字符串,不做任何修改。

让我们取前一个示例中的正则表达式,并使用正则表达式将找到的字符串替换为字符串“abc”:

使用 replace() 方法

var s = "Hello";
console.log("s =", s);
// search for "Hel" and replace with "abc"
console.log(`s.replace(/Hel/, "abc") => ${s.replace(/Hel/, "abc")}`);
// search for "hel" and replace with "abc"
console.log(`s.replace(/hel/, "abc") => ${s.replace(/hel/, "abc")}`);  
// search for hel ignoring upper/lower case and replacing with 
// "abc"
console.log(`s.replace(/hel/i, "abc") => ${s.replace(/hel/i, "abc")}`);
// search for H followed by a or b or e followed by l and 
// replace with "abc"
console.log(`s.replace(/H[abe]l/, "abc") => ${s.replace(/H[abe]l/, "abc")}`);
// search for He followed by 0 or 1 a followed by l and 
// replaced by "abc"
console.log(`s.replace(/Hea?l/, "abc") => ${s.replace(/Hea?l/, 
"abc")}`);
// search for He followed by 0 (min) to 1 (max) followed by l 
// and replaced by "abc"
console.log(`s.replace(/Hea{0,1}l/, "abc") => ${s.replace(/Hea{0,1}l/, "abc")}`);
// search for He followed by 1 (min) to 2 (max) followed by l 
// and replaced by "abc"
console.log(`s.replace(/Hea{1,2}l/, "abc") => ${s.replace(/Hea{1,2}l/, "abc")}`);

输出如下所示:

图 2.30 – 使用 replace() 方法

图 2.30 – 使用 replace() 方法

所有之前的程序执行都是立即执行的。我们现在将研究如何执行延迟处理。

JavaScript 中的多任务处理

当你开始用 JavaScript 编码时,经常会遇到一个问题:是否可以同时执行多个过程(在计算机中称为多任务处理)?如果需要执行的过程将花费很长时间,这将非常有用,以免阻塞其他同样紧急的过程。

JavaScript 不允许同时执行多个处理操作。另一方面,可以通过使用回调函数(在我们研究 使用 forEach(callback) 方法访问元素 部分时已经讨论过的)不阻塞程序(在浏览器客户端和 Node.js 服务器端)。

回调函数

回调函数对应于用作 JavaScript 方法或函数参数的处理函数。回调函数将由使用它的方法或函数在期望的时间执行。

Node.js 广泛使用此功能。例如,在读取文件时,readFile(callback) 方法在文件被读取时将回调函数作为参数调用,这允许程序不阻塞待读取文件的挂起处理。

JavaScript 定义了两个主要的标准函数,它们使用此回调函数概念:setTimeout()setInterval() 函数。这两个函数都使用回调函数作为参数。我们将在下面描述这两个函数。

使用 setTimeout() 函数

setTimeout(callback, timeout) 函数用于定位一个将在 timeout(以毫秒为单位)表示的时间间隔过去后执行的执行函数(callback 函数)。

这允许你,例如,在 5 秒后(即 5,000 毫秒)执行处理。你可以在等待这个延迟的同时执行其他指令,因此程序在这段时间内不会被阻塞:

延迟 5 秒后的处理指令

console.log("Before setTimeout()");
setTimeout(function() {
  console.log("In the callback function");
}, 5000);  // 5000 milliseconds, or 5 seconds
console.log("After setTimeout()");

我们在程序开始时在控制台中显示一条消息("Before setTimeout()")。我们编程一个 5 秒的延迟,之后触发一个回调函数,在控制台中显示另一条消息("In the callback function")。最后,我们通过显示一条新消息("After setTimeout()")来结束程序。

例如,使用 node testnode.js 命令运行此程序。要在浏览器中测试此程序,只需将前面的 JavaScript 代码放置在 index.html 文件的 <script></script> 标签之间。

以下截图显示了 1 秒后的显示效果:

![Figure 2.31 – 使用 setTimeout()img/Figure_2.31_B17416.jpg

Figure 2.31 – 使用 setTimeout()

注意,开始显示的消息和结束显示的消息是连续的,即使 5 秒的时间限制还没有结束。这表明程序没有被阻塞,等待超时到期。

以下截图显示了至少 5 秒后的显示效果(当 setTimeout() 方法中使用的延迟已过去)。

![Figure 2.32 – 5 秒延迟后的显示Figure 2.32 – B17416.jpg

Figure 2.32 – 5 秒延迟后的显示

我们可以看到,当 5 秒的延迟过去后,setTimeout() 函数中注册的回调函数会自动由 setTimeout() 函数调用。

让我们通过显示消息显示的时间来改进程序。这使得可以验证是否遵守了 5 秒的时间限制:

显示消息发布的时间

console.log(time(), "Before setTimeout()");
setTimeout(function() {
  console.log(time(), "In the callback function");
}, 5000);   // 5000 = 5 seconds
console.log(time(), "After setTimeout()");
function time() {
 // return time as HH:MM:SS
 var date = new Date();
 var hour = date.getHours();
 var min = date.getMinutes();
 var sec = date.getSeconds();
 if (hour < 10) hour = "0" + hour;
 if (min < 10) min = "0" + min;
 if (sec < 10) sec = "0" + sec;
 return "" + hour + ":" + min + ":" + sec + " ";
}

time() 函数用于生成一个包含 HH:MM:SS 格式的字符字符串。这个时间在每个显示在控制台中的消息的开头显示。

这里使用的 Date 类是一个 JavaScript 类,它允许你管理日期并提取小时、分钟和秒。

我们现在得到以下结果:

![Figure 2.33 – 显示在控制台中消息显示的时间img/Figure_2.33_B17416.jpg

Figure 2.33 – 显示在控制台中消息显示的时间

我们可以清楚地看到,回调函数是在 setTimeout() 函数参数中指定的 5 秒周期的末尾执行的。

使用 setInterval() 函数

setInterval(callback, timeout) 函数与之前看到的 setTimeout() 函数类似。但与 setTimeout() 函数只在延迟结束时只执行一次回调函数不同,setInterval() 函数会在每次回调函数执行结束后设置一个新的延迟,从而重复执行回调函数。因此,回调函数会以固定的时间间隔执行。停止这个循环的唯一方法是使用 clearInterval() 函数。

setInterval() 函数对于定期运行进程非常有用。

让我们使用 setInterval() 函数每秒显示一个初始化为 1 的计数器的值。计数器每秒递增:

每秒增加计数器

console.log(time(), "Start of timer");
var count = 1;
setInterval(function() {
  console.log(time(), `count = ${count}`);
  count++;
}, 1000);    // 1000 = 1 second
function time() {
 // return time as HH:MM:SS
 var date = new Date();
 var hour = date.getHours();
 var min = date.getMinutes();
 var sec = date.getSeconds();
 if (hour < 10) hour = "0" + hour;
 if (min < 10) min = "0" + min;
 if (sec < 10) sec = "0" + sec;
 return "" + hour + ":" + min + ":" + sec + " ";
}

这是你将看到的内容:

图 2.34 – 每秒增加计数器

图 2.34 – 每秒增加计数器

图 2.34 – 每秒增加计数器

计数器每秒增加一次,无限进行。要停止这个无休止的循环,你必须使用一个新的 JavaScript 函数,即 clearInterval()

使用 clearInterval() 函数

clearInterval(timer) 函数用于停止由 setInterval() 指令启动的循环。

注意

注意,可以通过多次调用 setInterval() 函数启动多个计时器。因此,clearInterval(timer) 函数必须指定它想要停止哪个计时器:timer 参数用于告诉它。

要做到这一点,setInterval() 函数返回将在调用 clearInterval(timer) 函数时使用的 timer 参数。

让我们使用 clearInterval() 函数在 count 计数器达到值 5 时停止计时器:

使用 clearInterval() 函数停止计时器

console.log(time(), "Start of timer");
var count = 1;
var timer = setInterval(function() {
  console.log(time(), `count = ${count}`);
  if (count == 5) {
    clearInterval(timer);  // timer stop
    console.log(time(), "End of timer");
  } else count++;
}, 1000);
function time() {
 // return time as HH:MM:SS
 var date = new Date();
 var hour = date.getHours();
 var min = date.getMinutes();
 var sec = date.getSeconds();
 if (hour < 10) hour = "0" + hour;
 if (min < 10) min = "0" + min;
 if (sec < 10) sec = "0" + sec;
 return "" + hour + ":" + min + ":" + sec + " ";
}

回调函数的程序被修改:一旦计数器达到 5,计时器就会停止。否则,计数器会增加 1。

检查计数在 5 次后停止:

图 2.35 – 计数器计数 5 次后停止

图 2.35 – 计数器计数 5 次后停止

图 2.35 – 计数器计数 5 次后停止

setTimeout()setInterval() 函数中使用的回调函数直接包含在每个函数的参数中。JavaScript 通过使用称为 Promise 的新类型对象来简化回调函数的编写。

使用 Promise

Promise 是使用回调函数的另一种方式。而不是将回调函数集成到方法调用中(作为参数),我们将其用作新 then(callback) 方法的参数。这种方式简化了使用回调函数的 JavaScript 代码的阅读。

要使用 then(callback) 方法,对象必须是 Promise 类对象。Promise 类是在 JavaScript 语言中定义的一个类。

Promise 类

Promise 类对象使用形式为 callback(resolve, reject) 的回调函数作为其构造函数的参数。

resolvereject 参数是函数,它们将从 Promise 的回调中调用:

  • resolve() 函数被调用时,它触发 then(callback) 方法。

  • reject() 函数被调用时,它触发 catch(callback) 方法。

resolve() 函数必须被调用,否则 then(callback) 方法无法执行。另一方面,调用 reject() 函数是可选的,如果未使用,则 catch(callback) 方法将不会调用(因此不需要在程序中存在)。

多亏了 resolvereject 参数,因此我们有执行成功情况(使用 then(callback) 方法)和失败情况(使用 catch(callback) 方法)的可能性。这种编写方式确保了 JavaScript 代码的可读性更高。

为了说明这一点,让我们以之前看到的setTimeout(callback, timeout)函数为例。这里的回调函数包含在方法调用中,我们希望使用 Promise 来避免这种情况。让我们编写新的wait(timeout)方法,它可以以wait(timeout).then(callback)的形式使用。现在,回调函数已经从wait()方法外部化。

当超时到期时,将调用在then(callback)方法中注册的回调函数。

这种写法比之前的setTimeout()写法更易读,因为它显示了在执行过程之前的时间延迟。

要实现这一点,wait(timeout)方法必须返回一个Promise对象:

创建 Promise 对象,然后使用 then()方法

function time() {
 // return time as HH:MM:SS
 var date = new Date();
 var hour = date.getHours();
 var min = date.getMinutes();
 var sec = date.getSeconds();
 if (hour < 10) hour = "0" + hour;
 if (min < 10) min = "0" + min;
 if (sec < 10) sec = "0" + sec;
 return "" + hour + ":" + min + ":" + sec + " ";
}
function wait(sec) {
  return new Promise(function(resolve, reject) {
    setTimeout(function() {
      resolve(sec);  // triggers the then() method
    }, sec*1000);
  });
}
console.log(time(), "Start of timer");
wait(2).then(function(sec) {
  console.log(time(), `End of timer of ${sec} seconds`);
});

wait()方法通过return new Promise()语句返回一个Promise对象。在callback(resolve, reject)函数中,当我们认为then()方法可以执行时,我们调用resolve()函数,这里是在超时结束时。

可以为resolve()reject()方法指定参数。这些参数将在then(callback)catch(callback)方法中使用的回调函数中使用。例如,在这里,我们调用resolve(sec)方法,这使得我们可以在then()方法的回调函数中使用sec参数。

注意

注意,在我们的示例中未使用reject()函数,因为没有错误情况可以发生。然而,必须调用resolve()函数;否则,then()方法将永远不会执行。

time()函数用于显示每个过程的执行时间,以检查执行是否正确。

![图 2.36 – 使用 then()方法图 2.36 – 使用 then()方法

图 2.36 – 使用 then()方法

这就带我们结束了本章的内容。

摘要

在本章中,我们学习了与 JavaScript 相关的高级概念。

我们学习了如何使用类和对象,特别是ArrayString类。我们还看到了如何延迟指令的执行。

在本书的其余部分,我们将发现与客户端应用开发相关的 Vue.js JavaScript 库的使用。

我们将看到如何利用在这里获得的知识,使我们能够在客户端和服务器端编程的各个方面使用这种语言。

第二部分:客户端的 JavaScript

在这部分,我们将探索在浏览器中(所谓客户端)使用 JavaScript。我们将学习如何使用 Vue.js 库在客户端构建 JavaScript 应用程序。我们还构建了一个列表管理应用程序(虽小但能代表现实)。

本节包括以下章节:

  • 第三章, Vue.js 入门

  • 第四章, Vue.js 的高级概念

  • 第五章, 使用 Vue.js 管理列表

第三章:第三章: Vue.js 入门

JavaScript 世界在不断发展。近年来,一个新概念出现了:通过创建组件来开发应用程序。

新的基于组件的 Web 应用程序开发 JavaScript 库已经出现,其中主要的是 Angular、React、Svelte 和 Vue.js。在这些彼此之间相当相似的库中,我们选择向您介绍 Vue.js,因为它被广泛使用且易于实现。提到的其他库遵循相同的原理。

为什么使用 Vue.js?

Vue.js 的主要优势是能够使用组件来开发应用程序。我们将 Web 应用程序切割成一系列组件(实际上是 JavaScript 文件),然后组装它们以形成最终的应用程序。Vue.js 可以独立于其他组件测试每个组件,并且还可以在其他应用程序中重用它们。

在本章中,我们将研究如何通过创建和使用我们的第一个组件来使用 Vue.js 构建我们的第一个应用程序。

在本章中,我们将涵盖以下主要主题:

  • 在 HTML 页面中使用 Vue.js

  • 创建我们的第一个 Vue.js 应用程序

  • 使用响应性

  • 创建我们的第一个组件

  • 在组件中添加方法

  • 在组件中使用属性

  • 使用指令

技术要求

您可以在 GitHub 上找到本章的代码文件:github.com/PacktPublishing/JavaScript-from-Frontend-to-Backend/blob/main/Chapter%203.zip

在 HTML 页面中使用 Vue.js

要在 HTML 页面中使用 Vue.js,只需使用 <script> 标签将其库文件插入到页面中。

为了检查 Vue.js 是否正确集成到页面中,让我们在 Vue.version 变量中显示库的版本号:

显示 Vue.js 版本号(index.html 文件)

<html>
  <head>
    <meta charset="utf-8" />
    <script src="img/vue@next"></script>
  </head>
  <body>
  </body>
  <script>
    alert(`Vue.version = ${Vue.version}`);
  </script>
</html>

如果 Vue.js 可在页面上访问,Vue 对象通过其 version 属性提供对版本号的访问,正如我们可以在以下图中看到:

![图 3.1 – 显示 Vue.js 版本号图 3.1 – 显示 Vue.js 版本号

图 3.1 – 显示 Vue.js 版本号

现在我们已经将 Vue.js 集成到我们的 HTML 页面中,让我们着手创建我们的第一个应用程序。

创建我们的第一个 Vue.js 应用程序

一旦将 Vue.js 插入到 HTML 页面中,您必须定义页面上 Vue.js 将要使用的 HTML 元素。

通常,您希望在整个 HTML 页面上使用 Vue.js,但也可以仅在某些页面元素上使用它。例如,我们可以使用 jQuery 管理一个 HTML 页面,除了特定的 <div> 元素,它将使用 Vue.js 管理。

为了说明这一点,让我们创建一个包含两个 <div> 元素的 HTML 页面,其中只有第一个将由 Vue.js 管理:

创建部分由 Vue.js 管理的 HTML 页面

<html>
  <head>
    <meta charset="utf-8" />
    <script src="img/vue@next"></script>
  </head>
  <body>
    <div id="app">First div</div>
<div>The rest of the page is not managed by 
    Vue.js</div>
  </body>
  <script>
    var app = Vue.createApp({
      template : "This div is managed with Vue.js"
    });
// mount the Vue.js application on the <div> having the 
    // id "app"
    var vm = app.mount("div#app");    
</script>
</html>

在前面的代码中,我们使用了定义在 Vue 对象上的 Vue.createApp(options) 方法。options 对象用于设置创建 Vue.js 应用程序的选项。Vue.createApp(options) 的一个选项是 template 选项,它允许我们定义将在页面上显示的视图(即 HTML 显示),这得益于 app.mount(element) 方法的调用:

  • app 对象是 Vue.createApp() 方法调用得到的结果。

  • element 参数代表 Vue.js 将在其上起作用的 HTML 元素。

运行前面的程序;我们应该看到以下输出:

图 3.2 – 第一个 Vue.js 应用程序

图 3.2 – 第一个 Vue.js 应用程序

在前面的屏幕上,我们可以看到在页面上使用 Vue.js 的结果。第一个 <div> 的内容被 Vue.createApp(options) 方法的 options 参数中编写的模板所替换。第二个 <div> 没有被转换。

因此,要使用 Vue.js 管理整个 HTML 页面,只需在页面的 <body> 部分指定一个单独的 <div> 元素,Vue.js 将会在这个元素上激活。

现在我们来看一下如何使用 Vue.js 的一个重要概念,即程序中定义的变量与它们在 HTML 页面上显示之间的对应关系。这个概念被称为响应性。

使用响应性

Vue.js 的一个目标是将显示管理(即 视图)和数据管理(即 模型)分离。这是在所谓的 模型-视图-控制器MVC)模型中经常遇到的概念。

为了说明,假设我们想要显示一个从 0 开始递增的计数器。视图和模型的良好分离将使视图持续显示计数器的值,即使该值在其他地方被更改。这个概念使得不必将显示与显示数据的管理链接起来。为此,我们使用 Vue.js 提供的响应性,通过创建所谓的 响应式变量

响应式变量

如果一个变量的内存修改导致它在显示的地方自动修改,则称该变量为响应式。

Vue.createApp(options) 方法中定义的 options 对象中定义响应式变量。为此,我们在 options 对象中添加,并定义 data() 方法,该方法必须返回一个包含应用程序所谓的响应式变量的对象。

在我们的 Vue.js 应用程序中使用一个名为 count 的响应式变量:

定义一个计数响应式变量

<html>
  <head>
    <meta charset="utf-8" />
    <script src="img/vue@next"></script>
  </head>
  <body>
    <div id="app"></div>
  </body>
  <script>
    var app = Vue.createApp({
      template : "The counter is: {{count}}",
      data() {
// return an object containing the reactive 
        // variables
        return {
          count : 0
        }
      }
    });
    var vm = app.mount("div#app");
</script>
</html>

在前面的代码中,count 响应式变量是在 data() 方法中定义的,该方法返回包含程序的响应式变量 { count : 0 } 的对象。之后可以定义其他变量。

这个响应式变量可以通过 {{ 和 }} 的符号在模板中使用。这种符号用于表示 JavaScript 表达式,例如变量的值。

一个所谓的响应式变量的定义使得将显示与变量的值联系起来成为可能。一旦变量被修改,显示也会相应修改。我们可以在以下图中看到计数器的值:

![图 3.3 – 显示一个响应式变量图 3.3 – 显示一个响应式变量

图 3.3 – 显示一个响应式变量

计数器保持在它的初始值:count变量被修改。

要做到这一点,让我们按照以下代码每秒增加变量的值:

每秒增加计数变量

<html>
  <head>
    <meta charset="utf-8" />
    <script src="img/vue@next"></script>
  </head>
  <body>
    <div id="app"></div>
  </body>
  <script>
    var app = Vue.createApp({
      template : "The counter is: {{count}}",
      data() {
        // return an object containing the reactive 
        // variables
        return {
          count : 0
        }
      }
    });
    var vm = app.mount("div#app");
    setInterval(function() {
      vm.count += 1;
    }, 1000);
</script>
</html>

使用 JavaScript 的setInterval()函数,我们每秒增加count变量的值。Vue.js 通过vm.count提供对count变量的访问,其中vmapp.mount()方法返回的对象。响应式变量成为这个vm对象的属性。在之前的代码中,我们可以看到视图和数据处理的分离,这是 MVC 模式所倡导的。变量的增加是在视图外部完成的,这在像 jQuery 这样的库中是不可能的。

我们可以通过 Vue.js 提供的响应性,在以下图中看到增加和显示的自动更新

![图 3.4 – 增加一个响应式变量图 3.4 – 增加一个响应式变量

图 3.4 – 增加一个响应式变量

之前的程序非常简单,但在现实中,应用程序当然更加复杂。因此,有必要将应用程序分解成小块,然后将它们组装起来。现在让我们学习如何编写应用程序的一个小部分,称为组件。

创建我们的第一个组件

让我们看看如何使用 Vue.js 创建我们自己的组件。

Vue.js 组件将类似于一个新的 HTML 元素。如果需要,它将以 HTML 标签的形式使用,可以将其与新的属性关联。要使用组件,您只需使用相应的标签。

因此,组件是通过创建我们自己的标签来丰富 HTML 代码的一种方式。

如何发现用于构建我们应用程序的组件

您需要做的就是将您想要显示的 HTML 页面视觉上切割成最简单的元素(这些将成为您应用程序的基本组件),然后将几个元素组合在一起形成一个组件,这些组件将组合它们,依此类推,直到您拥有主组件,这将是您的完整应用程序。

例如,如果 HTML 页面上显示了一个元素列表,列表中的每一行对应一个基本组件,而将这些不同组件组合起来的全局列表将与另一个组件相关联。HTML 页面上所有组件的集合对应于主组件,通常命名为<App><GlobalApp>。让我们先学习如何插入组件,然后看看如何创建和使用与之前计数器相对应的<counter>组件。

你可以直接在 HTML 页面中创建组件,或者从外部文件中包含它。让我们看看这两种方法。

在应用程序文件中插入组件

一个组件可以简单地嵌入到主应用程序 Vue.js 文件中。只需使用 app.component(name, options) 方法来创建,如下所示。变量 app 对应于 Vue.createApp() 返回的对象:

直接在应用程序中创建 组件

<html>
  <head>
    <meta charset="utf-8" />
    <script src="img/vue@next"></script>
  </head>
  <body>
    <div id="app"></div>
  </body>
  <script>
    var app = Vue.createApp({
      template : "<counter />"
    });
    app.component("counter", {
      template : "The counter is: {{count}}",
      data() {
        return {
          count : 0
        }
      }
    });
    var vm = app.mount("div#app");
</script>
</html>

在前面的代码中,变量 app 对应于 Vue.createApp() 返回的对象。

app.component(name, options) 方法与 Vue.createApp(options) 的工作原理相同:

  • name 参数对应于组件的名称,然后将在 HTML 模板中用作标签。

  • 在这两种情况下,options 参数类似。有 template 部分、data 等等。

然后,<counter> 组件可以在其他模板中使用,包括为应用程序定义的模板。当你运行前面的代码时,你会看到以下屏幕:

![图 3.5 – 组件图片

图 3.5 – 组件

如前图所示,目前计数器保持在 0。为了在组件创建后能够写入递增指令,以递增组件中的响应式变量 count,必须能够这样做。为此,Vue.js 提供了允许访问每个创建的组件生命周期的内部方法。

组件生命周期的方法之一是 created() 方法。当组件被创建时,会调用此方法。你可以使用此方法使用 setInterval() 函数每秒写入变量 count 的递增。

让我们按照以下方式使用组件的 created() 方法:

使用组件的 created() 方法

<html>
  <head>
    <meta charset="utf-8" />
    <script src="img/vue@next"></script>
  </head>
  <body>
    <div id="app"></div>
  </body>
  <script>
    var app = Vue.createApp({
      template : "<counter />"
    });
    app.component("counter", {
      template : "The counter is: {{count}}",
      data() {
        return {
          count : 0
        }
      },
      created() {
        setInterval(()=>{  // do not use the function()
                           // form here,
                           // otherwise the "this" object
                           // would not be the same
          this.count++;
        }, 1000);
      }
    });
    var vm = app.mount("div#app");
  </script>
</html>

在前面的代码中,我们使用了 ()=> 语法而不是 function()。这种语法(称为 lambda 函数)是在 JavaScript 的最新版本中引入的,以便允许在回调函数内部保持 this 的值,这在当前情况下是必要的。如果你将 lambda 函数 ()=> 替换为 function() 关键字,程序将无法工作,因为 this 的值将不同。

运行前面的代码后,你会看到以下输出:

![图 3.6 – 在组件中递增计数器图片

图 3.6 – 在组件中递增计数器

从外部文件插入组件

与直接在 HTML 页面中定义组件相比,最好在外部文件中定义它。组件可以通过在 HTML 页面中包含外部文件来在 HTML 页面中使用。为此,我们使用 JavaScript 提供的模块概念。

在外部文件中定义的组件的优势

将组件定义在外部文件中的优势是能够将此文件包含在多个不同的 HTML 页面中,因此可以在多个不同的应用程序中使用该组件。

<counter> 组件如下定义在外部 counter.js 文件中:

<counter> 组件定义(counter.js 文件)

const Counter = {
  data() {
    return {
      count: 0
    }
  },
  template : "The counter is: {{count}}",
  created() {
    setInterval(() => {
      this.count += 1;
    }, 1000)
  }
}
export default Counter;

<counter> 组件被定义为具有 templatedatacreated 属性的对象。其定义与之前在 app.component() 方法中展示的类似。

export default Counter 指令使得组件可以在导入此模块的其他文件中使用。

现在,我们可以将 <counter> 组件集成到我们应用程序的主文件中。我们使用 JavaScript 的 import 语句来实现这一点。代码如下所示:

将组件导入程序(index.html 文件)

<html>
  <head>
    <meta charset="utf-8" />
    <script src="img/vue@next"></script>
  </head>
  <body>
    <div id="app"></div>
  </body>
  <script type="module">
    import Counter from "./counter.js";
    var app = Vue.createApp({
      components : {
        Counter:Counter
      },
      template : "<counter />"   // or "<Counter />"
    });
    var vm = app.mount("div#app");
  </script>
</html>

在前面的代码中,为了导入 counter.js 文件并使用相应的组件,发生以下操作:

  • <script> 标签中的 type="module" 属性表示。这允许在 <script> 标签的 JavaScript 语句中使用 import 语句。

  • 我们使用 import 语句来导入相应的模块。

  • 我们在新的 components 部分声明导入的组件。组件被声明为一个对象。该对象中属性的名称对应于组件在模板中使用的名称(<counter><Counter>),而值对应于导入的组件的名称(Counter)。

    使用 HTTP 而不是 FILE 协议

    然而,由于我们使用 JavaScript 模块的导入,有必要在 HTTP 服务器上运行我们的应用程序,而不是像以前那样简单地拖放。因此,使用了以 http://localhost 开头的 URL。如果您需要了解如何安装 HTTP 服务器,例如,您可以使用以下文档:developer.mozilla.org/en-US/docs/Learn/Common_questions/set_up_a_local_testing_server

在以下图中,我们可以看到直接在 HTML 页面或在外部文件中创建组件会产生相同的结果:

![图 3.7 – 在 HTTP 服务器上执行 HTML 文件(此处为 localhost)]

](https://github.com/OpenDocCN/freelearn-html-css-js-zh/raw/master/docs/js-fe-be/img/Figure_3.7_B17416.jpg)

图 3.7 – 在 HTTP 服务器上执行 HTML 文件(此处为 localhost)

当前组件仅有一个简单的响应式变量。在组件中,可以添加方法供组件使用。现在让我们看看如何实现。

在组件中添加方法

我们已经看到如何在组件中使用 data 部分创建响应式变量。在组件中还可以创建可以在组件模板中使用的方法。

向组件添加方法有两种方式:

  • 第一种方法是在组件的 methods 部分定义方法。

  • 第二种方法是创建一个所谓的计算属性,它将在组件的 computed 部分中定义。

让我们看看这两种实现方式。

methods 部分中定义方法

对于计数器的每次增加,都应该显示它发生的时间。在组件中,一个 time() 函数将非常有用,允许我们以 HH:MM:SS 的形式显示时间。这个 time() 函数将在组件的 methods 部分中定义。

<counter> 组件被修改以集成行首时间的显示。我们可以使用以下代码实现所有这些:

显示时间的 <counter> 组件(counter.js 文件)

const Counter = {
  data() {
    return {
      count: 0
    }
  },
  template : `{{time()}} &nbsp;&nbsp; The counter is: 
  {{count}}`,
  created() {
    setInterval(() => {
      this.count += 1;
    }, 1000)
  },
  methods : {
    time() {
     // return time as HH:MM:SS
     var date = new Date();
     var hour = date.getHours();
     var min = date.getMinutes();
     var sec = date.getSeconds();
     if (hour < 10) hour = "0" + hour;
     if (min < 10) min = "0" + min;
     if (sec < 10) sec = "0" + sec;
     return "" + hour + ":" + min + ":" + sec + " ";
    }
  }
}
export default Counter;

在前面的代码中,time() 方法在 methods 部分中定义,然后直接在组件模板中的双大括号 {{}} 内使用。

methods 部分中定义的方法可以使用该部分的其它方法或 data 部分的响应式变量,通过在它们前面加上 this 关键字来使用。

结果将在以下图中显示:

![Figure 3.8 – 组件中的时间显示]

img/Figure_3.8_B17416.jpg

![Figure 3.8 – 组件中的时间显示]

Vue.js 允许你以方法的形式定义新的变量,这些变量将是响应式的。它们被称为计算属性。让我们看看如何创建和使用它们。

computed 部分中定义计算属性

计算属性类似于响应式变量。它是通过对一个或多个响应式变量执行计算得到的结果,并且它也将是响应式的。任何对与该计算属性关联的响应式变量的修改都将立即导致它的修改。

让我们创建一个 countX2 属性,它计算 count 变量的两倍,如下所示:

在组件中定义计算属性 countX2(counter.js 文件)

const Counter = {
  data() {
    return {
      count: 0
    }
  },
 template : `{{time()}} &nbsp;&nbsp; The counter is: 
  {{count}}, double is: {{countX2}}`,
  created() {
    setInterval(() => {
      this.count += 1;
    }, 1000)
  },
  methods : {
    time() {
     // return time as HH:MM:SS
     var date = new Date();
     var hour = date.getHours();
     var min = date.getMinutes();
     var sec = date.getSeconds();
     if (hour < 10) hour = "0" + hour;
     if (min < 10) min = "0" + min;
     if (sec < 10) sec = "0" + sec;
     return "" + hour + ":" + min + ":" + sec + " ";
    }
  },
  computed : {
    countX2() {
      return 2 * this.count;
    }
  }
}
export default Counter;

前面代码的输出将如下所示:

![Figure 3.9 – 使用计算属性]

img/Figure_3.9_B17416.jpg

![Figure 3.9 – 使用计算属性]

在前面的图中,我们可以看到 count 变量的修改。每秒钟都会自动修改 countX2 变量,这得益于它在 computed 部分的定义。

我们已经看到了如何在组件中定义方法和响应式变量。现在让我们看看如何通过使用组件的属性来传递参数。

在组件中使用属性

组件中的属性允许它传递使用参数。例如,我们可以在 <counter> 组件中使用一个 start 属性来指示我们从哪个值开始计数。如果没有指定此属性,则默认为 0(即,计数从 0 开始,如前面的代码示例所示)。

为了使组件能够在使用时使用属性,只需在组件的 props 部分中指明属性的名称。组件可以使用 this 关键字访问属性值(例如,this.start 来访问组件中的 start 属性)。我们可以在以下代码中看到这一点:

在组件中使用 start 属性(index.html 文件)

<html>
  <head>
    <meta charset="utf-8" />
    <script src="img/vue@next"></script>
  </head>
  <body>
    <div id="app"></div>
  </body>
  <script type="module">
    import Counter from "./counter.js";
    var app = Vue.createApp({
      components : {
        Counter:Counter
      },
      template : "<counter start='10' />"
    });
    var vm = app.mount("div#app");
  </script>
</html>

在以下代码中,属性是在使用组件时传递的,就像在传统的 HTML 中做的那样。这里的属性值将是一个字符字符串 "10",而不是值 10

<counter> 组件(counter.js 文件)中设置 start 属性

const Counter = {
  data() {
    return {
      count : parseInt(this.start),  // we initialize the
                                     // count to the value
                                     // of start
    }
  },
  template : `{{time()}} &nbsp;&nbsp; The counter is: 
  {{count}}, double is: {{countX2}}`,
  created() {
    var timer = setInterval(() => {
      this.count += 1;
    }, 1000)
  },
  methods : {
    time() {
     // return time as HH:MM:SS
     var date = new Date();
     var hour = date.getHours();
     var min = date.getMinutes();
     var sec = date.getSeconds();
     if (hour < 10) hour = "0" + hour;
     if (min < 10) min = "0" + min;
     if (sec < 10) sec = "0" + sec;
     return "" + hour + ":" + min + ":" + sec + " ";
    }
  },
  computed : {
    countX2() {
      return 2 * this.count;
    }
  },
  props : [
    "start"
  ]
}
export default Counter;

在前面的代码中,注意使用了 parseInt() 函数(在 JavaScript 中定义为标准函数)来以整数形式检索 this.start 的值。确实,属性是以字符字符串的形式传递的,因此需要将 this.start 转换为整数值。

可以避免将属性值转换为整数值。你只需要在使用属性时指明你想要保留 JavaScript 值而不是字符字符串。我们用字符 : 前缀属性名,例如,:start='10'。在这种情况下,值 10 将被传递,而不是字符串 "10"

这使得可以在属性中传递任何类型的值:数值、字符字符串、数组或对象。

在以下图中,我们可以看到计数器从 start 属性中指示的值开始:

图 3.10 – 在组件中使用 start 属性

图 3.10 – 在组件中使用 start 属性

因此,我们已经看到了如何在组件中创建新属性。Vue.js 有一些标准属性,可以在所有组件中使用。这些由 Vue.js 创建的特定属性被称为指令。我们现在将研究它们。

使用指令

Vue.js 通过提供编写其自身组件的功能来改进 HTML 代码的编写,正如我们在上一节所看到的。该框架还通过向 HTML 元素或创建的组件添加新属性来简化基本 HTML 代码的编写。这些新属性被称为指令。

注意

指令仅用于 HTML 元素或创建的组件,即在组件的 template 部分中。

它们的名称以 v- 开头,这样就不会与其他现有的 HTML 属性混淆。主要的指令有 v-ifv-elsev-showv-forv-model。现在我们将解释它们。

v-if 和 v-else 指令

v-if 指令用于指定条件。如果条件为真,HTML 元素(或组件)将被插入到 HTML 页面中。否则,它将不存在。

让我们使用 v-if 指令来表示我们只想在计数器的值小于或等于 20 时显示计数器的值。一旦超过值 20,计数器将不再显示。

在以下代码片段中,我们只指出了组件 template 部分的代码,知道其余部分没有修改:

使用 v-if 指令

template : `
  {{time()}} &nbsp;&nbsp;
  <span v-if='count<=20'>The counter is: {{count}}</span>
`,

使用反引号 '' 来定义模板可以避免在多行上管理字符串的连接。

应用 v-if 指令的 <span> 元素只有在以下条件为真时才会包含在 HTML 页面中:如果 count<=20。超过 20,则只显示时间,而不显示计数器值。

只要计数器小于或等于 20,它将按以下方式显示:

图 3.11 – 显示值小于 20 的计数器

图 3.11 – 显示值小于 20 的计数器

当计数器超过值 20 时,它将不再显示:

图 3.12 – 计数器超过值 20 时的显示

图 3.12 – 计数器超过值 20 时的显示

v-else 指令用于在 v-if 表达的条件为 false 时表示一个替代项。如果 v-if 表达的条件为 false,则使用 v-else 指令的元素将被插入到 HTML 页面中。

让我们使用 v-else 指令在计数器超过值 20 时显示另一条消息:

使用 v-else 指令

template : `
  {{time()}} &nbsp;&nbsp;
  <span v-if='count<=20'>The counter is: {{count}}</span>
  <span v-else>The counter has exceeded 20, it is: 
  {{count}}</span>
`,

当计数器超过值 20 时,我们现在得到以下结果:

图 3.13 – 计数器超过值 20

图 3.13 – 计数器超过值 20

v-show 指令

v-show 指令与 v-if 指令类似。接下来给出一个条件。如果条件为 true,则使用该指令的元素将被显示;否则,它不会被显示。

v-if 指令的区别在于,如果元素未显示,则仅隐藏,但仍然被插入到页面中。而使用 v-if 指令时,元素不会被插入(如果条件为 false)。

v-for 指令

v-for 指令允许您遍历一组元素或遍历一个对象的所有属性。对于循环的每次迭代,它都会在指令定位的 HTML 元素上插入元素。

让我们假设 <counter> 组件是与变量 counts 相关的一组计数器,counts 是一个 JavaScript 数组。在我们的例子中,每个计数器都是一个字符串(例如,"Counter 1"),我们希望以列表的形式显示整个集合(请参阅以下代码片段)。

让我们看看 v-for 指令的两种可能形式。

使用指令 v-for="count in counts"

让我们使用 v-for 指令的第一种形式。它允许访问指令中指定的数组中的每个元素(在我们的例子中,是 JavaScript 的 counts 数组):

以列表形式显示计数器(counter.js 文件)

const Counter = {
  data() {
    return {
      counts : ["Counter 1", "Counter 2", "Counter 3", 
      "Counter 4", "Counter 5"]
    }
  },
  template : `
    <ul>
      <li v-for="count in counts">
        <span>{{count}}</span>
      </li>
    </ul>
  `,
}
export default Counter;

在前面的代码中,我们将 v-for 指令放置在我们想要重复的元素上(在这种情况下,是 <li> 元素)。与 v-for 指令关联的值是一个字符串,形式为 "count in counts",我们知道 counts 是我们迭代的变量。因此,count 变量对应于 counts 数组中的每个元素:

图 3.14 – 使用 v-for 指令

图 3.14 – 使用 v-for 指令

使用指令 v-for="count, index) in counts"

v-for 指令的第二种形式提供了对数组中每个元素的访问,就像之前一样,但同时也提供了其索引(从 0 开始):

显示计数器和它们的索引(counter.js 文件)

const Counter = {
  data() {
    return {
      counts : ["Counter 1", "Counter 2", "Counter 3", 
      "Counter 4", "Counter 5"]
    }
  },
  template : `
    <ul>
      <li v-for="(count, index) in counts">
        <span>Index {{index}} : {{count}}</span>
      </li>
    </ul>
  `,
}
export default Counter;

运行前面的代码后,显示以下内容:

图 3.15 – 在 v-for 指令中使用索引

图 3.15 – 在 v-for 指令中使用索引

使用带有 v-for 指令的关键属性

v-for 指令也可以用于显示大型列表,其中必须保持反应性。也就是说,更改 v-for 指令中指定的反应式变量应该更新相应的显示列表。

为了尽可能快地执行更新,Vue.js 使用一个特殊的属性(仅用于此特定情况)命名为 key。此属性可以放置在 v-for 指令之后。其值必须对列表中的每个项目都是唯一的。例如,索引值对于每个列表元素是唯一的,可以用作 key 属性的值:

使用带有 v-for 指令的 key 属性

<li v-for="(count, index) in counts" :key="index">

在前面的代码中,属性的值是一个 JavaScript 表达式(变量 index)。我们使用 :key 而不是 key;否则,该属性将始终具有字符串 "index" 作为其值(而不是变量 index 的值)。

当然,添加 key 属性不会产生任何显示变化,但性能上的提升在后续对显示列表的更改中会变得明显(它帮助 Vue.js 跟踪元素并防止不必要的重新渲染)。

v-model 指令

v-model 指令用于在交互过程中管理表单元素(字段中的输入、复选框或单选按钮的点击、列表中元素的选取)。

v-model 指令用于立即检索输入或选择的结果,而不需要执行任何特定的处理。正是 v-model 指令为我们执行了这种更新(反应式变量的更新)。

我们使用 v-model 指令的形式 v-model="varname",其中 varname 是一个反应式变量的名称,该变量将在输入或选择时更新。

让我们在表单输入字段中使用v-model指令。为了清楚地看到使用或不使用它会发生什么,我们显示两个输入字段:一个未使用v-model管理,另一个使用:

在表单输入字段中使用 v-model 指令(counter.js 文件)

const Counter = {
  data() {
    return {
      count : 10
    }
  },
  template : `
    Without v-model:
<input type="text" :value="count" /> &nbsp;&nbsp; 
      count = {{count}} <br><br>
    With v-model:
<input type="text" v-model="count" /> &nbsp;&nbsp; 
      count = {{count}}
  `,
}
export default Counter;

下面是关于前面程序的几点说明:

  • 第一个<input>字段不使用v-model,只使用value属性,其值将根据count变量进行更新。

  • 第二个<input>字段使用与相同count变量关联的v-model指令。

  • count变量的值显示在两个输入字段之后。

当程序启动时,响应式变量count的值被传递到第一个输入字段的value属性,以及第二个输入字段。这导致了两个输入字段内容的初始化,如下所示:

![图 3.16 – 程序启动时的显示图 3.16 – 程序启动时的显示

图 3.16 – 程序启动时的显示

如果我们更改第一个输入字段的内容(该字段未使用v-model),我们会看到如下所示的内容:

![图 3.17 – 不使用 v-model 编辑输入字段图 3.17 – 不使用 v-model 编辑输入字段

图 3.17 – 不使用 v-model 编辑输入字段

注意,修改输入字段(不使用v-model)不会对其关联的响应式变量产生影响。

现在我们来修改由v-model管理的第二个输入字段的内容:

![图 3.18 – 使用 v-model 编辑输入字段图 3.18 – 程序启动时的显示

图 3.18 – 使用 v-model 编辑输入字段

我们现在可以看到,使用v-model会导致与之关联的响应式变量的立即修改,然后导致第一个输入字段的value属性被修改(因为它与响应式变量相关联)。

摘要

在本章中,我们主要学习了如何创建一个组件以及与之相关的属性或方法。

现在有必要研究如何管理组件中的用户操作,然后是如何组装组件以形成一个应用程序。

第四章:第四章:Vue.js 的高级概念

在本章中,我们将探讨 Vue.js 的高级用法。我们将研究组件中的事件处理,然后组装各种组件以形成一个完整的 Vue.js 应用程序。

为什么了解如何在组件中处理事件很重要?

Vue.js 组件通常是一组 HTML 元素,如构建块,例如按钮、列表和输入字段。因此,了解如何管理这些元素与用户可能采取的行动(如点击按钮、在输入字段中输入值或从列表中选择元素)之间的交互是至关重要的。

同样,为什么了解如何组装组件很重要?

一个 Web 应用程序汇集了许多元素,最终将代表整个应用程序。Vue.js 的原则是将应用程序分解成组件,然后组装它们以形成一个完整的应用程序。我们将学习如何将应用程序分解成组件,然后通过允许它们,例如,共享数据来组装它们。

我们通过展示如何利用 Vue.js 在页面上轻松产生视觉效果来结束本章。

在以下页面中,我们将解释以下主要主题:

  • 事件管理

  • 组装组件

  • 使用视觉效果

技术要求

你可以在 GitHub 上找到本章的代码文件:github.com/PacktPublishing/JavaScript-from-Frontend-to-Backend/blob/main/Chapter%204.zip

事件管理

现在我们来看看如何使用 Vue.js 处理事件。为此,使用 v-on 指令,后跟冒号 : 和要处理的事件名称。例如,如果你想在一个按钮被点击时执行特定过程,我们将使用按钮上的 click 事件,并编写 v-on:click 来处理 click 事件。指令的值(跟随等号 = 的部分)对应于要执行的 JavaScript 表达式(可以是语句或函数调用)。

小贴士

Vue.js 通过更简单地编写 @click 来简化编写 v-on:click。此规则适用于所有事件。

在这个例子中,我们将实现一个按钮,每次点击都会增加一个响应式变量 count。我们还将定义一个在组件的 methods 部分中的 incr() 方法,用于增加 count 变量:

增加计数器 countcounter.js 文件)

const Counter = {
  data() {
    return {
      count : 0
    }
  },
  template : `
    <button @click="count++">Increment counter by 
    count++</button> 
       &nbsp;&nbsp; count = {{count}} <br><br>
    <button @click="incr()">Increment counter by 
incr()</button> 
      &nbsp;&nbsp; count = {{count}}
  `,
  methods : {
    incr() {
      this.count++;
    }
  }
}
export default Counter;

我们定义了两个按钮,其 @click 的值如下:

  • @click="count++" (第一个按钮)

  • @click="incr()" (第二个按钮)

因此,我们展示了这些书写形式的等价性。

每次点击按钮,计数器都会增加 1。

![图 4.1 – 按钮点击管理图片

图 4.1 – 按钮点击管理

在执行处理过程中,可以连续编写多个方法调用(由逗号或分号分隔)。只要这些方法在组件的methods部分中定义,就足够了。

例如,@click="incr();incr()"允许在每次点击按钮时执行incr()方法两次。

我们已经解释了如何在组件的methods部分定义的方法中捕获事件并处理它。让我们进一步使用接收事件中传递的参数,例如,知道哪个键盘键被按下。

使用$event参数

Vue.js 提供了对与事件关联的Event对象的访问。然后可以使用该对象获取有关事件的附加信息。信息根据事件类型的不同而不同:

  • 鼠标坐标或鼠标上点击的按钮,对于鼠标相关的事件

  • 用于键盘相关事件的键盘键,或按下的键的组合(CtrlShiftEsc等)

可以从$event变量访问Event对象。它可以作为参数传递给处理方法。然后,在事件处理函数中检索此参数。

让我们看看两个示例,说明如何在输入编辑控件中输入字符时使用此参数:

  • 通过在输入的数值等于或超过 100 时立即显示错误消息

  • 如果编辑控件只能包含数字(这是前一个示例的改进)

检查输入的值是否小于 100

让我们使用$event参数来检查counter输入字段的内容是否小于 100。如果是这样,则使用输入的值更新count变量;否则,显示错误消息。

为了实现这一点,我们使用输入字段的blur事件,并在事件处理过程中检索输入字段的值。使用响应式的message变量来显示错误消息(如果需要):

注意

当离开输入字段时,例如通过点击输入字段外部,会触发blur事件。

如果计数器大于 100(counter.js文件),则显示错误消息

const Counter = {
  data() {
    return {
      count : 0,
      message : ""
    }
  },
  template : `
   count (less than 100): <input type="text" 
    :value="count" @blur="valid($event)" />
 &nbsp;&nbsp; count = {{count}} 
    <br><br>
    <span>{{message}}</span>
  `,
  methods : {
    valid(event) {
this.message = "";  // reset of the error message 
                          // before each check
if (event.target.value < 100) this.count = 
      event.target.value;
      else this.message = "Error: count must be less than 100";
    }
  }
}
export default Counter;

$event参数传递给valid(event)处理函数。event.target属性提供对 HTML 元素的直接访问。它的value属性包含字段的值。

如果输入的值小于 100(这里为 45),则更新计数器:

图 4.2 – 输入授权的值

图 4.2 – 输入授权的值

如果输入的值大于 100(例如,150),则显示错误,并恢复计数器的旧值(45)。

图 4.3 – 输入禁止的值

图 4.3 – 输入禁止的值

然后,我们将查看$event参数的另一种用途——只允许输入数字。

允许只输入数字

$event 参数的另一个用途是只允许在字段中输入数字。其他键盘键被禁止(除了 退格删除 键,左右箭头键,以及 Tab 键)。

为了实现这一点,我们使用 keydown 事件,该事件在每次按下键盘上的键时触发:

禁止输入非数字字符(counter.js 文件)

const Counter = {
  data() {
    return {
      count : 0,
      message : ""
    }
  },
  template : `
    count (less than 100):
    <input type="text" :value="count" @blur="valid($event)" 
    @keydown="verif($event)"/>
      &nbsp;&nbsp; count = {{count}} 
    <br><br>
    <span>{{message}}</span>
  `,
  methods : {
    valid(event) {
      this.message = "";  // reset of the error message 
// before each check
      if (event.target.value < 100) this.count = event.target.
      value;
      else this.message = "Error: count must be less than 100";
    },
    verif(event) {
console.log(event.key);   // display in the console 
// the value of the key 
                                // pressed
if (event.key != "Backspace" && event.key != "Delete" 
&& 
event.key != "ArrowLeft" && event.key != 
          "ArrowRight" &&
          event.key != "Tab") {
        // forbid the key if it is not numeric
if (event.key < "0" || event.key > "9") 
        event.preventDefault();  // forbidden key
      }
    }
  }
}
export default Counter;

用于过滤键的事件对应于 keydown,在按下键盘上的键时被激活。因此,我们指示使用在 methods 部分定义的 verif() 方法处理每个按键。

使用 event.key 和 event.preventDefault()

event.key 参数包含按下的键的代码。对于数值,键码在“0”和“9”之间。为了禁止其他键,我们使用 event.preventDefault() 方法(在 JavaScript 中定义),这表示不考虑该事件,因此禁止按键的按下。

我们在 第三章 中学习了如何创建组件,以及如何在其中管理事件(本章开头)。一个完整的应用程序由多个组件组成。现在让我们解释如何组装多个组件以形成一个完整的应用程序。

组装组件

Vue.js 将应用程序划分为一组组件。然后,这些组件被组装成最终的应用程序。

让我们研究一个如何创建组件然后组装创建的组件的例子。目标是使用三个计数器(与三个输入字段相关联),就像上一个例子中的那样,然后显示这些计数器的总和。当在输入字段中输入数字时,总和会更新。

我们将为此创建两个组件:

  • <counter> 组件用于管理一个计数器。

  • <counters> 组件允许你一起管理三个计数器并显示总和。

index.html 文件将在其 template 部分显示 <counters> 组件:

index.html 文件

<html>
  <head>
    <meta charset="utf-8" />
    <script src="img/vue@next"></script>
  </head>
  <body>
    <div id="app"></div>
  </body>
  <script type="module">
    import Counters from "./counters.js";
    var app = Vue.createApp({
      components : {
        Counters:Counters
      },
      template : `
        <counters />
      `,
    });
    var vm = app.mount("div#app");
  </script>
</html>

包含的 counters.js 文件描述了 <counters> 组件。它部分重复了之前章节中解释的内容,并添加了我们现在将描述的新概念。

这些新概念将解释父组件如何通过属性(称为 props)与子组件通信,以及子组件如何通过事件和 $emit() 方法与父组件通信。

这两个概念使得通过允许它们在子组件和父组件之间进行通信,可以在它们之间组装组件成为可能。

使用 $emit() 与父组件通信

让我们首先看看 <counter> 组件文件,它描述了一个与输入字段关联的计数器:

<counter> 组件(counter.js 文件)

const Counter = {
  data() {
    return {
      count : 0,
      old_value : 0
    }
  },
  template : `
    <input type="text" v-model="count" 
       @keydown="verif($event)" 
    @input="calcul()" 
       @focus="focus()" 
       @blur="blur()" />
  `,
  methods : {
    verif(event) {
      if (event.key != "Backspace" && event.key != "Delete" && 
          event.key != "ArrowLeft" && event.key != 
"ArrowRight" &&
          event.key != "Tab") {
        // forbid the key if it is not numeric
        if (event.key < "0" || event.key > "9") 
event.preventDefault();  // key forbidden
      }
      this.old_value = event.target.value;
    },
    calcul() {
this.$emit("sub", this.old_value || 0);  // subtract 
                                               // old value
      this.$emit("add", this.count || 0);      // add new value
    },
    focus() {
      if (this.old_value == "0") this.count = "";
    },
    blur() {
      if (!parseInt(this.count)) {
        this.old_value = 0; 
        this.count = 0;
      }
    }
  },
 emits : ["sub", "add"]    // declare events emitted to 
                            // the parent
}
export default Counter;

<counter>组件已经增加了新的方法,这些方法与新的输入事件相关联。同时,还创建了一个新的响应式变量old_value

  • old_value变量包含在按下键盘上的键之前输入字段中的值。

  • count变量包含在按下键盘上的键之后输入字段中的值。

为什么要有这种区分?因为要计算所有计数器的总和,每次输入按键时,都需要从字段中移除前一个值(在按键之前)并添加新值(在按键之后)。

每个按键都由input事件处理,这里调用calcul()方法。由于与三个计数器的总和相关的计算是在较高级别(在父组件<counters>中)执行的,你必须向这个父组件指示要减去的总和(old_value)和要添加的总数(count)。这是通过发送"sub""add"事件,使用$emit(eventName, value)方法来完成的。

关于$emit(eventName, value)方法

从组件执行$emit(eventName, value)方法会将eventName事件发送到父组件,父组件可以使用@eventName指令来处理它。如果需要,value参数对应于要传输的值。

此外,我们在组件的emits部分中指出了该组件可以向其父组件发出的事件列表。

这种在子组件(这里,<counter>组件)和其父组件(这里,<counters>组件)之间使用事件进行通信的方式是 Vue.js 推荐的方式。

现在让我们看看<counters>组件的描述,它包括三个计数器和你在每个字段中输入时的总计计算:

<counters>组件(counters.js 文件)

import Counter from "./counter.js";
const Counters = {
  data() {
    return {
      total : 0
    }
  },
  components : {
    Counter:Counter
  },
  template : `
  Counter 1 : <counter @add="add($event)" 
      @sub="sub($event)" /> <br>
Counter 2 : <counter @add="add($event)" 
      @sub="sub($event)" /> <br>
Counter 3 : <counter @add="add($event)" 
      @sub="sub($event)" /> <br><br>
      Total : {{total}} <br>
  `,
  methods : {
    add(value) {
      this.total += parseInt(value);
    },
    sub(value) {
      this.total -= parseInt(value);
    }
  },
}
export default Counters;

在使用<counter>子组件时,"add""sub"事件会在<counter>组件的属性中处理。add(value)sub(value)处理方法在父组件中注册,这使得每次在键盘上按下数字键时,总数值都可以改变。

当你在字段中输入时,总计会更新:

![Figure 4.4 – Calculation of the sum of the three countersFigure 4.04 – Calculation of the sum of the three counters

Figure 4.4 – Calculation of the sum of the three counters

我们已经看到了如何使用事件从一个组件向其父组件通信。现在让我们看看如何从组件向其子组件通信。为此,我们使用这里称为 props 的属性。

使用 props 与子组件通信

我们已经看到,从子组件向其父组件传递信息是通过事件完成的。反向通信,即从父组件到子组件,是通过称为 props 的属性完成的。我们已经在上一章的 在组件中使用属性 部分中看到了这些属性的使用。

在这个例子中,我们将改进 <counters> 组件,以便我们告诉它我们想要显示的计数器数量。为此,我们在组件中使用 nb 属性。例如,我们将编写 <counters nb="5" /> 来在页面上显示 5 个计数器。每个计数器都按照之前的形式显示,即 Counter 后跟从 1 开始的索引(见 图 4.5)。

首先,我们将修改 index.html 文件,使用 nb 属性来编写 <counters> 组件。让我们修改之前使用的 index.html 文件:

使用 <counters nb="5" />(index.html 文件)

<html>
  <head>
    <meta charset="utf-8" />
    <script src="img/vue@next"></script>
  </head>
  <body>
    <div id="app"></div>
  </body>
  <script type="module">
    import Counters from "./counters.js";
    var app = Vue.createApp({
      components : {
        Counters:Counters
      },
      template : `
        <counters nb="5" />
      `,
    });
    var vm = app.mount("div#app");
  </script>
</html>

现在,我们将修改 counters.js 文件,将新的 "nb" props 集成到组件中:

<counters> 组件中集成 nb props(counters.js 文件)

import Counter from "./counter.js";
const Counters = {
  data() {
    return {
      total : 0
    }
  },
  components : {
    Counter:Counter
  },
  props : ["nb"],
  computed : {
    NB() {
      var tab = [];
      for(var i = 0; i < this.nb; i++) tab.push(i+1);
      return tab;
    }
  },
  template : `
    <div v-for="i in NB">
Counter {{i}} : <counter @add="add($event)" 
      @sub="sub($event)" />
    </div>
    <br>
    Total : {{total}} <br>
  `,
  methods : {
    add(value) {
      this.total += parseInt(value);
    },
    sub(value) {
      this.total -= parseInt(value);
    }
  },
}
export default Counters;

"nb" props 列在组件的 props 部分。要显示计数器列表,请在 <div> 元素上使用 v-for 指令。

如何使用 v-for 指令

对于 v-for 指令的值,你必须指定一个数组来遍历。为此,我们将 "nb" props 的值转换为一个数组 [1, 2, 3, …, nb]。这是通过一个名为 NB 的计算属性完成的,它返回所需的数组。

使用 <counters nb="5"> 组件指示的计数器数量现在已显示。

![图 4.5 – 显示五个计数器图 4.5 – 显示五个计数器

图 4.5 – 显示五个计数器

我们在这里结束对 Vue.js 组件的研究,这些组件组合在一起形成一个完整的应用程序。

现在,让我们考察 Vue.js 的一个方面,它可以帮助你产生视觉效果,例如,允许使用视觉效果使 HTML 页面上的 HTML 元素出现或消失。

使用视觉效果

视觉效果使得通过将视觉动画引入 HTML 页面,使 HTML 页面更加动态。例如,要删除列表中的一个项目,你可以使用透明度效果使其逐渐消失,而不是直接删除而不使用视觉效果。

使用 Vue.js 可以使用视觉效果,特别是使元素从页面中消失或出现。Vue.js 也可以实现不使 HTML 元素从页面中消失或出现的视觉效果(例如,通过点击使元素移动)。有关这些类型动画的更多详细信息,请参阅 vuejs.org/guide/extras/animation.html。我们在这里不解释这些效果,因为可用的文档已经足够清晰,可以用来使用它们。

在本章接下来的内容中,我们将学习与页面上一或多个元素的出现或消失相关的视觉效果。

我们想要帮助出现或消失(使用视觉效果)的元素必须插入到名为 <transition> 的组件中。Vue.js 使用此组件来产生效果。

此外,Vue.js 使用了 CSS 类的定义,其中描述了效果的 CSS 属性。只需简单地定义 CSS 类的内容(在下一节中描述),Vue.js 就会在适当的时候使用它们来实现效果。

Vue.js 在元素上使用的 CSS 类取决于元素的状态:它应该出现还是消失?根据其状态(可见或不可见),CSS 类会有所不同。

当元素出现时

当 HTML 元素应该出现时,Vue.js 使用的 CSS 类的名称以字符串 "v-enter" 开头。类名随后包含后缀 "-from""-to",这将用于描述效果开始时(使用 "-from")或效果结束时(使用 "-to")的元素 CSS 属性。

Vue.js 使用的 CSS 类

因此,我们将有两个 CSS 类:

  • v-enter-from: 这个 CSS 类描述了元素出现效果的开始时的 CSS 属性。

  • v-enter-to: 这个 CSS 类描述了元素出现效果的结束时的 CSS 属性。

    注意

    注意,在出现效果的开始时,元素是不可见的,但 v-enter-from 类中描述的 CSS 属性会立即应用于它。例如,如果我们将 CSS opacity 属性设置为 1 应用到 v-enter-from 类的 CSS 属性中,元素就会在出现效果开始时立即变得可见。

由于 v-enter-to 类描述了效果结束时元素的 CSS 属性,因此当效果完成时,Vue.js 会从元素中移除该 CSS 类。

因此,我们可以看到 CSS 类 v-enter-fromv-enter-to 用于描述元素在效果期间的 CSS 属性,但在效果之后不再在元素上使用(即在效果持续期间之外)。

出现效果将 v-enter-from 中描述的 CSS 属性推进到 v-enter-to 中描述的属性。为此,Vue.js 使用 v-enter-active 类,该类描述了每个 CSS 属性如何演变。

CSS 类示例内容

让我们看看上述三个 CSS 类(v-enter-fromv-enter-tov-enter-active)的一些示例内容:

v-enter-from 类示例

.v-enter-from {
  opacity: 0;
  background-color:#FFCCCC;
}

在这里,我们指示元素将在效果的开始时不可见(opacity:0)并且具有背景颜色(background-color:#FFCCCC):

v-enter-to 类示例

.v-enter-to {
  opacity: 0.5;
  background-color:black;
}

在这里,我们指示元素将在效果的结束时半透明(opacity:0.5)并且具有黑色背景(background-color:black):

v-enter-active 类示例

.v-enter-active {
  transition: opacity 2s, background-color 2s;
}

在这里,我们指出 CSS 的 opacitybackground-color 属性必须各自演变两秒钟。由于所有指定的 CSS 属性以相同的时间演变,我们可以通过简写形式简化代码。以下是这样做的方法:

v-enter-active 类示例(简写形式)

.v-enter-active {
  transition: all 2s;
}

all 关键字覆盖了所有指定的 CSS 属性。

使用 CSS 类

现在,让我们展示如何使用这些 CSS 类在程序中使用一个按钮来显示具有效果的段落。按钮的作用将是隐藏或显示段落,当段落出现时,效果将在该段落上发生。

这展示了 Vue.js 如何使用 v-enter-fromv-enter-tov-enter-active CSS 类在元素出现在页面上时产生效果:

使用按钮产生出现效果(index.html 文件)

<html>
  <head>
    <meta charset="utf-8" />
    <script src="img/vue@next"></script>
    <style type="text/css">
      .v-enter-from {
        opacity: 0;
        background-color:#FFCCCC;
      }
      .v-enter-to {
        opacity: 0.5;
        background-color:black;
      }
      .v-enter-active {
        transition: opacity 2s, background-color 2s;
      }
    </style>
  </head>
  <body>
    <div id="app"></div>
  </body>
  <script>
    var app = Vue.createApp({
      data() {
        return {
          show: false    // initially hidden
        }
      },
      template : `
     <button @click="show=!show">Produce the 
        effect</button>
        <transition>
          <p v-if="show">
            Paragraph 1
          </p>
        </transition>   
      `,
    });
    var vm = app.mount("div#app");
  </script>
</html>

我们已经描述了 v-enter-fromv-enter-tov-enter-active CSS 类的内容,Vue.js 将使用这些类来产生效果。然后我们插入了 <transition> 元素,从而允许 Vue.js 知道要在哪个元素上应用效果。

段落在启动时被隐藏(因为响应式变量 show 被设置为 false)。点击 show 变量将其设置为 true,这会启动效果。

注意

效果是在段落上启动的,归功于 <transition> 组件,它包括要显示的段落。正是这个 <transition> 组件让 Vue.js 知道要在哪个元素上产生效果。

注意到效果持续了两个秒,正如 CSS transition 属性所指示的,当效果结束时,CSS 类从 <p> 元素中移除,此时它变成了一个正常的段落(没有背景颜色,透明度为 1)。所以,你看到段落在效果结束时(在 v-enter-to 中指示的)具有 0.5 的透明度,然后在 v-enter-to 类被 Vue.js 在效果结束时移除后,突然变为透明度为 1。

注意

因此,最好在 v-enter-to 类中指定元素不再产生效果时的 CSS 值,以使效果更加和谐。

让我们运行前面的程序。当程序启动时,段落被隐藏:

图 4.6 – 程序启动时段落被隐藏

图 4.6 – 程序启动时段落被隐藏

点击 v-enter-fromv-enter-tov-enter-active 类后。

图 4.7 – 点击“产生效果”按钮后,段落逐渐出现

图 4.7 – 点击“产生效果”按钮后,段落逐渐出现

在效果结束前,段落具有 v-enter-to 类中设置的 CSS 属性,因此其背景颜色为黑色,但透明度为 0.5,背景颜色仍然是灰色,段落文本不可见。

图 4.8 – 效果结束前的段落

图 4.8 – 效果结束前的段落

在效果结束时,CSS 类被移除,使得段落以正常的方式出现,黑色且没有背景颜色。

图 4.9 – 出现效果结束时的段落

图 4.9 – 出现效果结束时的段落

当段落出现后,点击按钮将 show 设置为 false

我们已经看到了元素在页面上出现时的不同类和阶段。现在让我们看看当元素从页面上消失时会发生什么。我们会看到元素的出现和消失有很多相似之处。

当元素消失时

当元素应该消失时,Vue.js 使用与之前类似的 CSS 类,将字符串 "enter" 替换为字符串 "leave"

Vue.js 使用的 CSS 类

因此,我们将有以下两个 CSS 类:

  • v-leave-from:这个 CSS 类描述了元素消失效果的开始时的 CSS 属性。

  • v-leave-to:这个 CSS 类描述了元素消失效果结束时的 CSS 属性。

消失效果是将 v-leave-from 中描述的 CSS 属性过渡到 v-leave-to 中描述的属性。效果完成后,v-leave-to 类将从元素的 CSS 类中移除。

为了在这两个类中显示的值之间过渡 CSS 属性,Vue.js 使用 v-leave-active CSS 类,它描述了 CSS 属性的过渡。

CSS 类的示例内容

让我们看看上面提到的三个 CSS 类(v-leave-fromv-leave-tov-leave-active)的一些示例内容:

v-leave-from 类示例

.v-leave-from {
  opacity: 1;
  background-color:#FFCCCC;
}

在这里,我们指示元素在效果开始时将完全可见(opacity:1)并且将具有背景颜色(background-color:#FFCCCC):

v-leave-to 类示例

.v-leave-to {
  opacity: 0;
  background-color:black;
}

在这里,我们指示元素在效果结束时将不可见(opacity:0)并且将具有黑色背景颜色(background-color:black):

v-leave-active 类示例

.v-leave-active {
  transition: opacity 2s, background-color 2s;
}

在这里,我们指示 CSS 的 opacitybackground-color 属性必须各自演变,每个属性持续两秒钟。由于所有指定的 CSS 属性以相同的时间演变,你可以通过简写形式来简化代码:

v-leave-active 类示例

.v-leave-active {
  transition: all 2s;
}

all 关键字覆盖了所有指定的 CSS 属性。

使用 CSS 类

现在我们来展示如何在程序中使用这些 CSS 类,使用一个按钮来隐藏带有效果的段落。这个程序几乎和之前一样,但在这里,当段落消失时,我们产生一个效果:

使用按钮产生消失效果(index.html 文件)

<html>
  <head>
    <meta charset="utf-8" />
    <script src="img/vue@next"></script>
    <style type="text/css">
      .v-leave-from {
        opacity: 1;
        background-color:#FFCCCC;
      }
      .v-leave-to {
        opacity: 0;
        background-color:black;
      }
      .v-leave-active {
        transition: all 2s;
      }
    </style>
  </head>
  <body>
    <div id="app"></div>
  </body>
  <script>
    var app = Vue.createApp({
      data() {
        return {
          show: true   // visible at start
        }
      },
      template : `
        <button @click="show=!show">Produce the effect</button>
        <transition>
          <p v-if="show">
            Paragraph 1
          </p>
        </transition>   
      `,
    });
    var vm = app.mount("div#app");
  </script>
</html>

v-leave-from 类在效果开始时应用。它表示元素是可见的(opacity 为 1)并且具有背景颜色 #FFCCCC(鲑鱼色)。

v-leave-to 类表示当效果结束时 CSS 属性的值。段落变为不可见(opacity 为 0)并且具有黑色背景颜色。但随着元素变得越来越不可见(opacity 趋向于 0),黑色背景颜色也越来越不明显。

如果我们在 CSS 部分中编写 enterleave 类,并且每次点击按钮,我们就会得到段落相关的出现或消失的效果。

这里使用的 CSS 类具有固定的名称,无论使用哪种效果。这不允许使用多个效果,因为所有的视觉效果都会使用相同的 CSS 类名称。

为了这个,Vue.js 允许你给每个效果命名,从而能够使用不同的 CSS 类名称。

为效果使用名称

类型为 "v-enter-xxx""v-leave-xxx" 的类可以被重命名,以表示它们关联的效果。我们只需将字符串 "v-" 替换为效果的名称,然后跟一个 "-"

例如,"v-enter-from" 将被替换为 "fade-enter-from",以给效果命名为 "fade"。然后我们添加 name="fade" 属性到 <transition> 组件中,表示 <transition name="fade">

这允许我们将多个效果集成到我们的应用程序中,通过定义对应每个效果的 CSS 类。

之前的程序,将名为 "fade" 的效果集成到段落中,写法如下:

淡入淡出效果(index.html 文件)

<html>
  <head>
    <meta charset="utf-8" />
    <script src="img/vue@next"></script>
    <style type="text/css">
      .fade-leave-from {
        opacity: 1;
        background-color:#FFCCCC;
      }
      .fade-leave-to {
        opacity: 0;
        background-color:black;
      }
      .fade-leave-active {
        transition: all 2s;
      }
      .fade-enter-from {
        opacity: 0;
        background-color:#FFCCCC;
      }
      .fade-enter-to {
        opacity: 1;
        background-color:black;
      }
      .fade-enter-active {
        transition: opacity 2s, background-color 2s;
      }      
    </style>
  </head>
  <body>
    <div id="app"></div>
  </body>
  <script>
    var app = Vue.createApp({
      data() {
        return {
          show: true
        }
      },
      template : `
        <button @click="show=!show">Produce the 
effect</button>
        <transition name="fade">
          <p v-if="show">
            Paragraph 1
          </p>
        </transition>   
      `,
    });
    var vm = app.mount("div#app");
  </script>
</html>

<transition> 组件只能有一个元素,这个元素将是效果将要发生作用的元素。要包含多个元素,必须使用 <transition-group> 组件,我们将在下面进行解释。

在多个元素上产生效果

<transition> 组件只能包含一个元素。当必须将效果应用于多个元素时,必须创建多个 <transition> 组件或将元素分组在 <transition-group> 组件中。在这个例子中,让我们看看如何使用 <transition-group> 组件:

使用 <transition-group> 组件

<transition-group name="fade">
  <p v-if="show">
    Paragraph 1
  </p>
  <p v-if="show">
    Paragraph 2
  </p>
</transition-group>   

发生效果的元素(这里,两个段落)被分组在一个 <transition-group> 元素中,而不是之前在只有一个段落发生效果时使用的 <transition> 元素。

现在,我们将看看如何编写与一些经典效果相关的 CSS 类。

常用效果的示例

下面是一些效果的描述。通过几行 CSS 代码,你可以轻松地产生经典的段落缩放(缩放效果)、逐渐消失/出现(透明度效果)以及垂直位移(ymove 效果)。你可以自由选择这些效果的名称,并象征性地表示产生的效果。

缩放效果

要使用缩放效果(这里称为 "shrink"),我们使用 CSS font-size 属性。

在效果开始时,段落是正常大小的:

图 4.10 – 消失效果开始时段落是正常大小

图 4.10 – 消失效果开始时段落是正常大小

一旦点击按钮开始效果,段落会减小到消失。

图 4.11 – 段落减小到消失

图 4.11 – 段落减小到消失

一旦段落消失,再次点击按钮后可以重新出现。段落大小会增加,直到达到其正常大小:

处理缩放效果的 CSS 类

.shrink-leave-from {
}
.shrink-leave-to {
  font-size: 0px;
}
.shrink-leave-active {
  transition: all 2s;
}
.shrink-enter-from {
  font-size: 0px;
}
.shrink-enter-to {
}
.shrink-enter-active {
  transition: all 2s;
}

CSS 类 shrink-leave-to 指示,对于消失效果,字体大小将变为 0px,即段落的字体大小减少到 0,使段落变得不可见。

shrink-enter-from CSS 类指示效果从 0px 的字体大小开始,逐渐增长到可见时的正常段落大小。

如果起始类中没有指定 CSS 属性(例如,shrink-leave-from 类不包含 font-size 属性),这意味着将使用该 CSS 属性的当前值在元素中。

同样,如果到达类中没有指定 CSS 属性(例如,shrink-enter-to 类不包含 font-size 属性),这意味着我们正在向效果结束时元素将可见的该 CSS 属性的值前进。

透明度效果

命名为 "fade" 的效果使用 CSS opacity 属性。此效果包括将 CSS opacity 属性从 0 变化到 1(逐渐使元素出现)或从 1 变化到 0(使其消失)。

例如,这是消失效果。段落正在以透明度逐渐减小到 0 的方式消失。当透明度为 0 时,元素将在屏幕上完全不可见。

图 4.12 – 段落的透明度逐渐减小到 0

图 4.12 – 段落的透明度逐渐减小到 0

一旦段落变得不可见,只需再次点击 产生效果 按钮,就可以让它逐渐重新出现:

管理透明度的 CSS 类

.fade-leave-from {
}
.fade-leave-to {
  opacity : 0;
}
.fade-leave-active {
  transition: all 0.5s;
}
.fade-enter-from {
  opacity : 0;
}
.fade-enter-to {
}
.fade-enter-active {
  transition: all 1s;
}

fade-leave-to CSS 类表示将不透明度变为 0。当前的不透明度(值为 1)是起始值。由于不透明度的初始值在 fade-leave-from 中未定义,它将使用元素 CSS 中定义的值(即 1)。

类似地,fade-enter-from 类表示元素出现效果开始时的当前不透明度。不透明度的目标值不需要指定,因为它将使用元素的默认 CSS 值,即 1。

向下移动效果

要管理此效果(在此处称为 "ymove"),我们使用 CSS 属性 transform(设置为 translateY(100px))和 opacity(设置为 0)。这逐渐将元素向下移动 100 像素,逐渐减少其不透明度至 0。元素在向下移动页面的过程中消失。

例如,当元素开始通过减少其不透明度向下滑动时,显示的内容如下:

![Figure 4.13 – 段落通过减少其不透明度向下移动页面Figure 4.13_B17416.jpg

图 4.13 – 段落通过减少其不透明度向下移动页面

随着效果的继续,段落向下移动页面,直到达到效果中指定的 100 像素距离。你越接近这个距离,段落的不透明度就降低得越多,直到变得看不见(不透明度为 0)。

![Figure 4.14 – 在效果结束时,段落几乎看不见Figure 4.14_B17416.jpg

图 4.14 – 在效果结束时,段落几乎看不见

一旦段落消失,点击 产生效果 按钮会使它从屏幕底部逐渐重新出现:

处理向下移动的 CSS 类

.ymove-leave-from {
}
.ymove-leave-to {
  transform: translateY(100px);
  opacity : 0;
}
.ymove-leave-active {
  transition: all 0.5s;
}
.ymove-enter-from {
  transform: translateY(100px);
  opacity : 0;
}
.ymove-enter-to {
}
.ymove-enter-active {
  transition: all 0.5s;
}

ymove-leave-to CSS 类表示我们想要变化的指示 CSS 属性的值。transform 属性可以包含 translateY(100px) 值,表示执行 100 像素的垂直平移(Y)。添加不透明度为 0 使元素通过垂直移动消失。

ymove-enter-from CSS 类允许你在出现效果的开始处指定 CSS 属性的值。元素位于垂直距离 100 像素处,不透明度为 0。CSS 属性将演变到 ymove-enter-to 类中指定的属性,如果没有在此类中指定任何内容,则通常用于元素的 CSS 属性(不透明度为 1 和垂直距离为 0,即正常位置)是我们将在出现效果期间演变到的属性。

CSS 的 transform 属性在产生视觉效果方面非常有用,例如旋转、放大和位移。

这就带我们结束了本章的内容。

摘要

在学习了如何处理事件并在外部事件(例如,点击)发生时采取行动之后,在本章中我们看到了如何使用 Vue.js 创建的组件可以组合成完整的应用程序。我们学到了以下内容:

  • 要从组件与其父组件之间进行通信,我们使用事件。

  • 要从组件与其子组件之间进行通信,我们使用组件的props部分中的属性。

最后,为了产生视觉效果,你只需要编写由 Vue.js 管理的 CSS 类。

在下一章中,我们将看到一个应用程序的例子,它允许我们将前几章中学习到的元素付诸实践。

第五章:第五章:使用 Vue.js 管理列表

在了解了 Vue.js 的基本和高级概念之后,通过本章,让我们通过构建一个应用程序来管理元素列表,来完成我们对 Vue.js 库的学习。

为什么要制作这种类型的应用程序?很简单,因为它允许你执行对页面 HTML 元素的标准操作,例如插入元素、修改它和删除它。

这些是你需要知道如何执行的基本操作,例如,在数据库中管理元素。在本章中,我们将学习如何在屏幕上显示的元素上执行这些操作,在下一部分(我们学习 Node.js 和 MongoDB)中,我们将看到如何同时更新数据库。

本章涵盖了以下主题:

  • 将应用程序拆分为组件

  • 向列表中添加元素

  • 从列表中删除元素

  • 修改列表中的元素

但让我们先来发现我们想要用 Vue.js 创建的应用程序的界面。

技术要求

你可以在 GitHub 上找到本章的代码文件:github.com/PacktPublishing/JavaScript-from-Frontend-to-Backend/blob/main/Chapter%205.zip

显示应用程序屏幕

如前所述,我们将构建一个用于管理元素列表的应用程序。在编写我们应用程序的源代码之前,让我们通过解释它们的顺序来展示应用程序的不同屏幕。

初始时,列表为空。添加元素按钮允许在每次点击时向列表中插入一个新元素。

![图片 5.1 – 启动应用程序时的屏幕图片 5.01_B17416.jpg

图片 5.1 – 启动应用程序时的屏幕

让我们多次点击添加元素按钮(这里,点击了三次):

![图片 5.2 – 点击添加元素按钮三次后的效果图片 5.02_B17416.jpg

图片 5.2 – 点击添加元素按钮三次后的效果

每个插入的元素都有列表中元素的索引(从 1 开始)。在列表项后面插入一个删除按钮和一个修改按钮。

让我们在第二行点击修改按钮。项目文本被替换为一个输入字段,光标闪烁以允许编辑。

![图片 5.3 – 列表中的第二个元素可以更改图片 5.03_B17416.jpg

图片 5.3 – 列表中的第二个元素可以更改

让我们修改输入字段中的文本,输入 New Element 2

![图片 5.4 – 编辑列表项图片 5.04_B17416.jpg

图片 5.4 – 编辑列表项

要使元素的修改反映出来,你必须通过在页面上其他地方点击来离开输入字段。

![图片 5.5 – 考虑到元素的修改图片 5.05_B17416.jpg

图片 5.5 – 考虑到元素的修改

最后,要删除第一和第三个元素,请点击它们对应的删除按钮。

图 5.6 – 删除第一和最后一个元素后

图 5.6 – 删除第一和最后一个元素后

我们已经管理了一个元素列表,并对它执行了基本操作,即插入新元素、修改元素和删除元素。

使用 HTTP 协议

这个应用使用 PHP 服务器来工作,因为只有在使用 HTTP 协议的情况下,JavaScript 模块的导入才能通过 JavaScript 的import语句进行。我们将在下一部分(*第九章**,将 Vue.js 与 Node.js 集成)中看到如何使用 Node.js 服务器来实现这一点,同时将其与 MongoDB 数据库结合使用。

我们已经描述了应用的操作以及各种窗口的顺序。现在让我们看看如何使用 Vue.js 构建这个应用。我们首先解释如何将应用分解为不同的组件。

将应用拆分为组件

当你使用 Vue.js 创建一个应用时,你必须首先问自己你需要哪些组件来构建它。

在我们的情况下,它将是以下内容:

  • 一个将整个应用组合在一起的<GlobalApp>组件。正是这个<GlobalApp>组件将被集成到我们的index.html页面中。它将显示添加元素按钮以及下面的元素列表。

  • 一个显示列表元素的<Element>组件,它将包括元素的文本、删除按钮和修改按钮。

元素列表将与一个名为elements的反应变量相关联,它将是一个数组,包含每个元素的显示文本。这个反应变量将在<GlobalApp>组件中注册。当向列表中添加新元素或删除或修改列表中的元素时,它将被修改。

因此,我们应用的核心文件如下:

  • index.html文件,这是主文件

  • 包含<GlobalApp>组件的global-app.js文件,它被导入到index.html文件中

  • element.js文件,它描述了显示列表中的一个元素(即<Element>组件),包括元素的文本以及删除修改按钮

下面是这些文件的内容:

index.html 文件

<html>
  <head>
    <meta charset="utf-8" />
    <script src="img/vue@next"></script>
  </head>
  <body>
    <div id="app"></div>
  </body>
  <script type="module">
    import GlobalApp from "./global-app.js";
    var app = Vue.createApp({
      components : {
        GlobalApp:GlobalApp
      },
      template : "<GlobalApp />"
    });
    var vm = app.mount("div#app");
  </script>
</html>

index.html文件显示<GlobalApp>组件,这是应用的主组件,我们现在将对其进行描述:

<GlobalApp>组件(global-app.js 文件)

import Element from "./element.js";
const GlobalApp = {
  data() {
    return {
      elements : []
    }
  },
  components : {
    Element:Element
  },
  template : `
    <button>Add Element</button>
    <ul></ul>
  `,
}
export default GlobalApp;

我们发现反应变量elements以及元素的<ul>列表目前为空。

下面是对<Element>组件的描述。目前它是空的,将在以下章节中丰富:

<Element>组件(element.js 文件)

const Element = {
  data() {
    return {
    }
  },
  template : `
  `,
}
export default Element;

使用 HTTP 协议

由于 JavaScript 代码包含模块 import 指令,因此需要使用可通过 HTTP 访问的 Web 服务器来显示与 index.html 对应的 HTML 页面。file 协议在这里不起作用。

让我们在屏幕上显示这段临时代码的结果:

图 5.7 – 使用我们的启动代码显示的结果

图 5.7 – 使用我们的启动代码显示的结果

图 5.7 中,我们看到 <GlobalApp> 组件的渲染,该组件目前仅显示 添加元素 按钮。让我们看看如何处理对该按钮的点击,以便在列表中插入新元素。

向列表中添加元素

我们将从向列表中添加项的功能开始。global-app.js 文件被修改以处理对 global-app.js 文件的点击)。

让我们添加当点击添加元素按钮时应运行的代码:

考虑到点击“添加元素”按钮(global-app.js 文件)

import Element from "./element.js";
const GlobalApp = {
  data() {
    return {
      elements : []
    }
  },
  components : {
    Element:Element
  },
  template : `
    <button @click="add()">Add Element</button>
    <ul>
<li v-for="(element, index) in elements" 
      :key="index">{{element}}</li>
    </ul>
  `,
  methods : {
    add() {
var element = "Element " + (this.elements.length + 
      1);  // "Element X"
      this.elements.push(element);
    }
  }
}
export default GlobalApp;

点击 click 事件,该事件调用在 methods 部分定义的 add() 方法。add() 方法将新元素添加到响应式变量 elements

元素列表在组件模板中更新。目前,我们将使用 <li> 标签来定义要插入的列表元素,但下面,我们将使用 <Element> 组件,该组件将集成 删除修改 按钮。

现在,让我们验证我们对 <GlobalApp> 组件的修改是否有效。为此,多次点击 添加元素 按钮。每次点击都会插入列表项,如以下图所示。

图 5.8 – 添加元素按钮点击

图 5.8 – 添加元素按钮点击

这里插入的元素是一个 HTML <li> 元素。但是,用 Vue.js 组件替换 <li> 元素很有趣,因为它允许使用 Vue.js 的哲学,即最大程度地使用组件。让我们将这个新组件命名为 <Element>,它将替换 <li> 元素。

使用 组件

接下来,让我们使用 <Element> 组件,而不是之前的 <li> 元素。

<GlobalApp> 组件被修改以集成 <Element> 组件:

在列表中使用 组件(global-app.js 文件)

import Element from "./element.js";
const GlobalApp = {
  data() {
    return {
      elements : []
    }
  },
  components : {
    Element:Element
  },
  template : `
    <button @click="add()">Add Element</button>
    <ul>
      <Element v-for="(element, index) in elements" 
      :key="index" :text="element" />
    </ul>
  `,
  methods : {
    add() {
      var element = "Element " + (this.elements.length + 
      1);
      this.elements.push(element);
    }
  }
}
export default GlobalApp; 

要在列表项中显示的文本作为属性(通过 props)传递给 <Element> 组件,该组件将在其模板中显示它。我们使用 text 属性(或任何其他属性名称)来做到这一点。

<Element> 组件被修改以考虑传递的 text 属性并显示列表元素。在文本之后插入两个按钮 删除修改

使用文本属性和按钮(element.js 文件)

const Element = {
  data() {
    return {
    }
  },
  template : `
    <li> 
      <span> {{text}} </span>
<button> Remove </button> 
      <button> Modify </button>
    </li>
  `,
  props : ["text"],
}
export default Element;

让我们检查结果是否与之前的结果相同(添加了 删除修改 按钮)。

图 5.9 – 在列表中使用  组件

图 5.9 – 在列表中使用 组件

点击列表中的 移除修改 按钮目前不起作用,但将在接下来的章节中实现。

移除修改 按钮并排放置,没有间隔。让我们添加一些 CSS 代码来更好地在屏幕上布局它们。

使用 CSS 代码更改列表的外观

在处理列表中的按钮点击之前,让我们使用一些 CSS 以更美观的方式显示列表项。

CSS 代码直接在 index.html 文件中指示:

使用 CSS 代码显示列表(index.html 文件)

<html>
  <head>
    <meta charset="utf-8" />
    <script src="img/vue@next"></script>
    <style type="text/css">
      li {
        margin-top:10px;
      }
      ul button {
        margin-left:10px;
      }
    </style>
  </head>
  <body>
    <div id="app"></div>
  </body>
  <script type="module">
    import GlobalApp from "./global-app.js";
    var app = Vue.createApp({
      components : {
        GlobalApp:GlobalApp
      },
      template : "<GlobalApp />"
    });
    var vm = app.mount("div#app");
  </script>
</html>

我们可以看到,列表的外观现在更加令人愉悦。

图 5.10 – 使用 CSS 代码改进的元素列表

图 5.10 – 使用 CSS 代码改进的元素列表

显示的列表现在有了我们想要的样式!我们现在必须管理对 移除修改 按钮的点击。让我们从 移除 按钮开始。

从列表中删除元素

现在,让我们处理对 elements 的点击。

注意

事实上,变量 elements 是响应式的,任何对这个变量的修改都将导致列表的重新显示。

要做到这一点,在每次点击时,在 <Element> 组件中定义的 remove() 方法上点击:

考虑到对移除按钮的点击(element.js 文件)

const Element = {
  data() {
    return {
    }
  },
  template : `
    <li> 
      <span> {{text}} </span>
      <button @click="remove()"> Remove </button> 
      <button> Modify </button>
    </li>
  `,
  props : ["text"],
  methods : {
    remove() {
      // process the click on the Remove button
    },
  },
}
export default Element;.

在本章后面将讨论点击 移除 按钮涉及的过程。

注意

要处理对 elements 的点击,但由于这位于父组件 <GlobalApp> 中,我们必须向这个父组件发送一个事件,请求它从变量 elements 中移除元素。

要指明要删除的元素,必须通过其索引来引用。为此,我们需要在创建 <Element> 组件时指明元素的索引。因此,我们在这个组件中创建了一个新的属性(命名为 "index")。因此,remove() 方法向 <GlobalApp> 父组件发送一个 "remove" 事件,在参数中指明要从列表中删除的元素的索引。

<Element> 组件变为以下形式:

处理移除按钮的点击(element.js 文件)

const Element = {
  data() {
    return {
    }
  },
  template : `
    <li> 
      <span> {{text}} </span>
      <button @click="remove()"> Remove </button> 
      <button> Modify </button>
    </li>
  `,
  props : ["text", "index"],
  methods : {
    remove() {
      // process the click on the Remove button
      this.$emit("remove", { index : this.index });
    },
  },
  emits : ["remove"]
}
export default Element;

<GlobalApp> 组件被修改以处理接收当点击 移除 按钮时发送的 "remove" 事件:

处理“remove”事件的接收(global-app.js 文件)

import Element from "./element.js";
const GlobalApp = {
  data() {
    return {
      elements : []
    }
  },
  components : {
    Element:Element
  },
  template : `
    <button @click="add()">Add Element</button>
    <ul>
      <Element v-for="(element, index) in elements" 
      :key="index" :text="element" 
           :index="index"
               @remove="remove($event)"
      />
    </ul>
  `,
  methods : {
    add() {
      var element = "Element " + (this.elements.length + 
      1);
      this.elements.push(element);
    },
    remove(params) {
      var index = params.index;
this.elements.splice(index, 1);  // delete element in 
                                       // array
    }
  }
}
export default GlobalApp;

我们在 <Element> 组件中指出了新的属性 index,这将允许知道元素在列表中的索引。

让我们在列表中添加三个项目(见 图 5.11),然后点击第二行的 移除 按钮见 图 5.12):

图 5.11 – 向列表中添加三个元素

图 5.11 – 向列表中添加三个元素

点击 移除 按钮后,我们将看到以下内容:

图 5.12 – 从列表中删除元素 2

图 5.12 – 从列表中删除元素 2

通过点击 删除 按钮,元素 2 已从列表中删除。现在让我们看看如何通过点击 修改 按钮来管理元素的修改。

修改列表中的元素

修改列表元素分为几个步骤:

  1. 点击 <span> 元素后) 转换为初始化为元素文本的 HTML <input> 元素。

  2. 然后我们处理输入字段的退出,通过检索字段中输入的值,然后通过用包含新内容的 <span> 元素替换输入字段来处理。

  3. 最后,我们通过允许输入控件在点击 修改 按钮后自动获得焦点来改进输入。

让我们深入探讨这些不同的步骤。

元素转换为 元素

第一步是将 <span> 元素转换为 <input> 元素,这将允许修改元素的文本。为此,我们将在 <Element> 组件中添加一个新的响应式变量(命名为 "input")。它表示是否将文本显示为 <span> 元素(如果 inputfalse)或者是否显示 <input> 输入字段(如果 inputtrue)。默认情况下,input 变量设置为 false(文本显示)。当点击 修改 按钮时,它将变为 true

元素转换为 元素(element.js 文件)

const Element = {
  data() {
    return {
      input : false   // display element text by default
    }
  },
  template : `
    <li> 
      <span v-if="!input"> {{text}} </span>
      <input v-else type="text" :value="text" />
      <button @click="remove()"> Remove </button> 
      <button @click="input=true"> Modify </button>
    </li>
  `,
  props : ["text", "index"],
  methods : {
    remove() {
      // process the click on the Remove button
      this.$emit("remove", { index : this.index });
    },
  },
  emits : ["remove"]
}
export default Element;

注意

使用 v-ifv-else 指令来显示元素的文本作为 <span> 元素或 <input> 元素。

在向列表中插入三个项目后,让我们编辑第二个项目:

图 5.13 – 编辑列表中的第二个项目

图 5.13 – 编辑列表中的第二个项目

现在我们需要展示如何离开输入字段并重新显示文本作为列表元素。

离开输入字段

一旦编辑控件被修改,必须检索输入的值以显示它而不是编辑控件。为此,在 <Element> 组件中,我们使用 blur 事件,这表示我们已经离开了输入字段。

在处理此事件的过程中,检索输入字段的值,通过名为 "modify" 的事件将其传输给父 <GlobalApp> 组件,例如。当处理接收到的 modify 事件时,<GlobalApp> 组件在 elements 变量中修改元素值。

注意

在父组件中定位的响应式变量的修改必须通过向父组件发送一个事件来完成,父组件将需要处理该事件。

最后,通过在 <Element> 组件中重新定位响应式变量 inputfalse,将输入字段转换为文本。

<Element> 组件的修改方式如下所示:

考虑到输入字段的输出(element.js 文件)

const Element = {
  data() {
    return {
      input : false
    }
  },
  template : `
    <li> 
      <span v-if="!input"> {{text}} </span>
      <input v-else type="text" :value="text" 
       @blur="modify($event)" />
      <button @click="remove()"> Remove </button> 
      <button @click="input=true"> Modify </button>
    </li>
  `,
  props : ["text", "index"],
  methods : {
    remove() {
      // process the click on the Remove button
      this.$emit("remove", { index : this.index });
    },
    modify(event) {
var value = event.target.value;    // value entered 
                                         // in the field
      this.input = false;                // delete input field
this.$emit("modify", { index : this.index, value : 
      value });   // update element in list
    }
  },
  emits : ["remove", "modify"]
}
export default Element;

<GlobalApp> 组件也被修改,以处理接收 "modify" 事件并因此修改显示的列表:

处理修改事件(global-app.js 文件)

import Element from "./element.js";
const GlobalApp = {
  data() {
    return {
      elements : []
    }
  },
  components : {
    Element:Element
  },
  template : `
    <button @click="add()">Add Element</button>
    <ul>
      <Element v-for="(element, index) in elements" 
      :key="index" :text="element" 
        :index="index"
        @remove="remove($event)" @modify="modify($event)"
      />
    </ul>
  `,
  methods : {
    add() {
      var element = "Element " + (this.elements.length + 
      1);
      this.elements.push(element);
    },
    remove(params) {
      var index = params.index;
      this.elements.splice(index, 1);
    },
    modify(params) {
      var index = params.index;
      var value = params.value;
      this.elements[index] = value;  // new element value
    }
  }
}
export default GlobalApp;

下图显示了编辑第二个列表项后的结果。

![Figure 5.14 – 编辑列表项Figure 5.14 – Editing a list item

Figure 5.14 – 编辑列表项

我们可以对程序进行的最后一次改进是在点击修改按钮后直接将焦点放在输入字段上。让我们看看如何进行。

将焦点放在输入字段上

将焦点放在输入字段需要使用定义在文档对象模型DOM)中的focus()方法。DOM 是在浏览器中实现的 JavaScript 语言中的内部 API。

Vue.js 使得可以在 Vue.js 定义的组件和 DOM 使用的 HTML 元素之间建立关系。为此,我们使用ref属性,它使得这两个系统之间可以建立对应关系。

注意

这个ref属性可以用于我们组件模板中定义的每个 HTML 元素。但应该仅在必要的情况下使用,例如在这里,使用 DOM 中定义的focus()方法,否则将无法访问。

一旦ref属性已经定位(在这里,是在允许输入的<input>元素上),剩下的就是使用它来将焦点放在输入字段上。那么问题是:我们应该在组件的哪个方法中调用focus()方法?

我们必须使用一个确保输入字段已创建的方法。组件中编写的模板必须转换为 HTML 代码并集成到浏览器的内存中(在 DOM 中),然后才能显示。因此,我们看到有一个转换过程发生,这个过程需要一些时间来执行。

Vue.js 定义了一系列方法,当使用组件时,这些方法会自动被调用。在上一章中,我们看到了一个名为created()的方法。还有其他方法,特别是mounted()updated()方法。

这里是这三个方法的具体细节:

  • 当创建组件时,会调用created()方法。这是第一个被调用的方法。

  • 当组件转换为 HTML 元素并集成到 DOM 中时,会调用mounted()方法。因此,我们可以在这个方法中使用 DOM API 访问 HTML 元素。

  • 当组件中发生修改时,会调用updated()方法。例如,当在<input>元素上点击后,该元素变为<span>元素(当离开输入字段时)。

我们可以看到,updated()方法是我们可以在其中执行将焦点放在输入字段上的处理的方法。但是,由于这个方法在转换为输入字段或简单文本时都会被调用,因此需要检查与ref属性中指示的引用相关联的<input>元素是否存在。否则,将在控制台中发生可见的错误:

在输入字段出现时立即将其聚焦(element.js 文件)

const Element = {
  data() {
    return {
      input : false
    }
  },
  template : `
    <li> 
      <span v-if="!input"> {{text}} </span>
      <input v-else type="text" :value="text" 
       @blur="modify($event)" ref="refInput" />
      <button @click="remove()"> Remove </button> 
      <button @click="input=true"> Modify </button>
    </li>
  `,
  props : ["text", "index"],
  methods : {
    remove() {
      // process the click on the Remove button
      this.$emit("remove", { index : this.index });
    },
    modify(event) {
      var value = event.target.value;
      this.input = false;
      this.$emit("modify", { index : this.index, value : 
      value });
    }
  },
  emits : ["remove", "modify"],
  updated() {
// check that the ref="refInput" attribute exists, and 
    // if so, give focus to the input field
    if (this.$refs.refInput) this.$refs.refInput.focus();  
  }
}
export default Element;

当在模板中使用ref属性时,Vue.js 将其存储在组件的内部$refs变量中。因此,如果我们在一个组件模板中写入了ref="refInput",我们可以使用this.$refs.refInput来访问相应的 HTML 元素。

让我们检查(见下图)当点击修改按钮时,编辑控件是否直接获得焦点。

![图 5.15 – 输入字段直接获得焦点

![Figure 5.15 – The input field gets the focus directly

图 5.15 – 输入字段直接获得焦点

这章内容就到这里结束了。

摘要

本章以及其中讨论的示例表明,在不离开页面的情况下,交互式管理 HTML 页面上的元素是非常容易的。

在这里,我们首先将应用分解为不同的组件,然后我们将它们组装起来,通过事件和props属性使它们进行通信。通过这个完整的示例,我们学习了如何管理元素列表以执行主要操作,这些操作包括元素的插入、修改和删除。

在接下来的几章中,我们将看到如何使用 Node.js 将我们的应用程序连接到 MongoDB 数据库,从而能够将列表中的元素存储在数据库中。我们将在下一章学习如何与 node.js 模块一起工作。

第三部分:服务器端的 JavaScript

本部分是关于在 Node.js 服务器中使用 JavaScript。它解释了如何使用 Express(使用 MVC 模式快速创建基于 Node.js 的 Web 应用程序)和 MongoDB 数据库等模块。

我们通过构建一个单页应用程序(这个原则被称为单页应用)来结束我们的学习,该应用程序在客户端使用 Vue.js 编写,在服务器端使用 Node.js、Express 和 MongoDB。本书的目的是让您了解如何制作这种类型的应用程序。

本节包括以下章节:

  • 第六章, 创建和使用 Node.js 模块

  • 第七章, 使用 Express 与 Node.js

  • 第八章, 使用 MongoDB 与 Node.js

  • 第九章, 集成 Vue.js 与 Node.js

第六章:第六章:创建和使用 Node.js 模块

模块是 Node.js 的核心。它们对应于 JavaScript 文件,并可以在我们的应用程序中使用。Node.js 服务器的程序将包含一组模块,即 JavaScript 文件。

有三种类型的模块:

  • 我们为我们的应用程序编写的模块。

  • Node.js 内部的模块和可以直接使用的模块。

  • 可以使用名为npm(npm 代表 Node.js 包管理器)的实用程序从互联网上下载的模块。这个npm实用程序与 Node.js 本身一起安装。

在本章中,我们将学习如何创建和使用这些不同类型的模块。

无论使用哪种类型的模块,require(moduleName)指令(见下文)都允许将名为moduleName的模块包含到当前文件中。模块的功能将随后可访问。

本章涵盖了以下主题:

  • 使用我们自己的模块

  • 使用内部 Node.js 模块

  • 使用 npm 下载的模块

让我们先看看如何使用 Node.js 创建和使用我们自己的模块。

技术要求

你可以在 GitHub 上找到本章的代码文件:github.com/PacktPublishing/JavaScript-from-Frontend-to-Backend/blob/main/Chapter%206.zip

创建和使用我们自己的模块

在这个例子中,我们使用了两个模块,每个模块对应一个 JavaScript 文件:

  • 第一个模块(这里命名为test.js)将是我们的应用程序的主文件,我们将在命令窗口中使用node test.js命令执行该文件。

  • 第二个模块(这里命名为module1.js)将是我们在主test.js模块中想要使用的模块。module1.js模块将被丰富以展示其功能如何在外部模块中访问(因此将在主test.js模块中使用)。

让我们继续创建这两个模块。

创建一个模块

下面是两个文件的内容,module1.jstest.js

module1.js 文件

console.log("module1.js is loaded");

该模块目前有一个简单的console.log()语句。然后模块将被丰富。主模块test.js如下所示:

test.js 文件

var mod1 = require("./module1.js");  
// or require("./module1") without specifying the .js extension
console.log("mod1 =", mod1);

在这里,我们使用require(moduleName)指令,它允许我们将moduleName模块加载到内存中。任何使用moduleName模块功能的行为都需要事先执行require(moduleName)指令。

require(moduleName)指令返回对已加载到内存中的模块的引用。这个引用存储在一个变量中(这里,mod1),然后允许访问模块中描述的功能(目前没有)。

test.js文件是加载其他模块的主文件。因此,在命令窗口中使用node test.js指令执行的是这个test.js文件。

![图 6.1 – 使用 require(module)模块]

](https://github.com/OpenDocCN/freelearn-html-css-js-zh/raw/master/docs/js-fe-be/img/Figure_6.01_B17416.jpg)

图 6.1 – 使用 require(module)模块

我们可以在这里看到,主模块 test.js 的执行调用了 require("./module1.js") 指令的调用,这执行了 module1.js 文件的内容,因此显示文本是在 module1.js 模块中指定的 console.log() 语句。

在加载 module1.js 之后,mod1 变量被初始化,我们随后将能够访问模块后来导出的功能。

在向 module1.js 模块添加功能之前,让我们看看如何使用 node_modules 目录来管理模块的位置。node_modules 目录被 Node.js 用于定位它没有路径的模块。使用此目录简化了在用 require(moduleName) 指令将它们加载到内存中时编写模块名称。

使用 node_modules 目录

注意,之前的 require(moduleName) 语句需要指定模块的访问路径,例如,使用 "./" 来指示当前目录。

然而,如果模块在 node_modules 目录中,则不需要指定路径,因为我们确信模块在 node_modules 目录内(而且更不应该指定)。node_modules 目录可以位于主应用程序目录中(称为 local node_modules 目录)或由 Node.js 创建的专用目录中(称为 global node_modules 目录:在这种情况下,它是在 Node.js 安装期间自动创建的)。

注意

如果在 node_modules 目录(本地或全局)中找不到模块,并且没有指定模块的访问路径,则在用 require(moduleName) 指令加载模块时将发生错误。

现在,我们将在主文件 test.js 所在的当前目录中创建一个 node_modules 目录。让我们将 module1.js 文件转移到这个目录,并使用 require("module1.js") 语句而不指定模块的路径。您也可以不指定 JavaScript 文件的扩展名来写 require("module1")

包含位于 node_modules 目录中的模块 1(test.js 文件)

var mod1 = require("module1.js");  // or require("module1")
console.log("mod1 =", mod1);

module1.js 文件必须位于本地创建的 node_modules 目录中,而 test.js 文件则保持在当前目录中,如下所述:

root/

|— node_modules/

│ |— module1.js

|— test.js

图 6.2 – 模块从 node_modules 目录加载

图 6.2 – 模块从 node_modules 目录加载

我们可以看到,模块确实被 Node.js 找到了,因为 Node.js 在当前目录中创建的 node_modules 目录中寻找它。

现在,让我们看看如何使用与模块关联的 package.json 文件允许模块的文件在目录中分组。

使用 package.json 文件

node_modules 目录(无论位于应用程序目录还是 Node.js 安装目录)可以包含许多文件,有时一个模块可以由许多文件和目录组成。将模块与 node_modules 目录中的目录关联起来会更容易。

让我们在 node_modules 目录内创建 module1 目录。module1 目录包含 module1.js 文件,但也可能包含与此模块相关的其他文件和目录。

文件系统在此显示:

root/

|— node_modules/

| |— module1/

│ |— module1.js

|— test.js

注意

require(moduleName) 语句中指示的 moduleName 在此情况下表示包含模块文件的 目录 名称。

但是,由于在加载模块时必须知道我们首先需要使用目录中的哪个文件(因为在这个目录中可能有多个文件),我们在 "main" 键中的 package.json 文件中指示了这个对应关系。

package.json 文件是一个位于每个 Node.js 模块目录中的 JSON 格式的文本文件。

现在,我们将在 module1 模块目录中创建 package.json 文件,并在该文件中用 "module1.js" 的值指示 "main" 键。

文件系统如下:

root/

|— node_modules/

| |— module1/

│ |— module1.js

│ |— package.json

|— test.js

位于 node_modules/module1 目录中的 package.json 文件(package.json 文件)

{
  "main" : "module1.js"
}

我们在 "main" 键中指示,在执行 require("module1") 指令时必须加载 module1.js 文件:

包含位于 node_modules/module1 目录中的 module1(test.js 文件)

var mod1 = require("module1"); //"module1" is the directory name
console.log("mod1 =", mod1);

注意

请注意,在这种情况下,require("module1") 语句中的模块名是 node_modules 目录中包含模块的目录名称。因此,我们在这里不能写成 require("module1.js") 的形式,这会导致错误。

我们现在可视化 test.js 文件的执行:

图 6.3 – 包含 package.json 文件的模块

图 6.3 – 包含 package.json 文件的模块

如果主模块文件命名为 index.js,则 package.json 文件中的 "main" 键是可选的。在所有其他情况下,必须在 package.json 中指示 "main" 键。

我们知道如何运行一个模块,但到目前为止,模块包含一个简单的 console.log() 语句。让我们看看如何向模块添加功能,然后使用它们。

向模块添加功能

新创建的 module1.js 模块是可访问的,但目前不提供任何功能。让我们看看如何添加一些功能。

在模块中导出多个函数

例如,让我们创建一个名为 add(a, b) 的函数,它返回 ab 的和:

在 module1.js 文件中定义的 add(a, b) 函数

console.log("module1 is loaded");
function add(a, b) {
  return a+b;
}
module.exports = { 
add : add     // make the add() function accessible 
                // outside the module
}; 

要将函数导出至模块外部(并使其对模块用户可用),您只需将其嵌入到每个模块中由 Node.js 定义的 module.exports 对象中。module.exports 对象中定义的每个键都将是一个模块外部的可访问函数。

因此,我们可以在模块中定义几个函数,这些函数将通过 module.exports 对象可访问。

test.js 文件中使用 add(a, b) 函数的方式如下:

在 test.js 文件中使用 add() 函数(test.js 文件)

var mod1 = require("module1");
console.log("mod1 =", mod1);
var total = mod1.add(2, 3);      // call of the add() function 
                                 // defined in module1
console.log("mod1.add(2, 3) = ", total);  // displays 5

以下显示结果如下:

图 6.4 – 添加到模块中的 add() 函数

图 6.4 – 添加到模块中的 add() 函数

让我们在模块中添加第二个函数。例如,函数 mult(a, b),它返回 a*b

如果我们在模块中添加 mult(a, b) 函数,它将写成如下所示:

mult(a, b) 函数添加到模块(module1.js 文件)

console.log("module1 is loaded");
function add(a, b) {
  return a+b;
}
function mult(a, b) {
  return a*b;
}
module.exports = {
  add : add,
  mult : mult
}

现在,我们将在 test.js 文件中使用两个函数 add()mult()。这验证了模块可以向使用它的其他模块提供多种功能:

使用模块的 add() 和 mult() 函数(test.js 文件)

var mod1 = require("module1");
console.log("mod1 =", mod1);
var total = mod1.add(2, 3);
console.log("mod1.add(2, 3) = ", total);      // 2 + 3 = 5
var total = mod1.mult(2, 3);
console.log("mod1.mult(2, 3) = ", total);     // 2 * 3 = 6

以下显示结果如下:

图 6.5 – 使用模块的两个函数

图 6.5 – 使用模块的两个函数

现在让我们看看如何通过使用模块中的所谓主函数来改进模块概念。

允许一个函数成为模块的主函数

通常,模块希望将一个函数设为主函数(模块中定义的其他函数是次要函数)。这允许以简化的形式访问此主函数。

假设(如前所述)module1 提供了 add(a, b) 函数和 mult(a, b) 函数。我们希望 add() 函数成为模块的主函数,这意味着我们可以将其作为 mod1(2, 3) 而不是 mod1.add(2, 3) 在模块外部使用。mult(a, b) 函数将保持以 mod1.mult(2, 3) 的形式可访问。

注意

注意,在模块中只能定义一个主函数。

在这种情况下,只需在 module.exports 对象中指定即可:

add() 函数作为主模块函数(module1.js 文件)可访问

console.log("module1 is loaded");
function add(a, b) {
  return a+b;
}
function mult(a, b) {
  return a*b;
}
// first define the main function
module.exports = add;  // the add() function defined outside 
                       // the module, is made main
// then define the secondary functions
module.exports.mult = mult;   // and the mult() function 
                              // becomes usable as well

注意

module.exports 对象中按此顺序分配值很重要(首先定义主函数,然后是次要函数)。如果您以其他方向进行分配(module.exports.mult 首先分配,然后 module.exports),则最后分配的 module.exports 将会覆盖已定位在 module.exports.mult 中的值。

此外,我们不能再将 module.exports 作为对象分配,因为如果我们写成 module.exports = { mult : mult },这将删除之前分配的值。

我们现在使用模块的方式如下:

使用具有主函数的 module1.js 模块(test.js 文件)

var mod1 = require("module1");
console.log("mod1 =", mod1);
var total = mod1(2, 3);          // instead of mod1.add(2, 3)
console.log("mod1(2, 3) = ", total);
var total = mod1.mult(2, 3);
console.log("mod1.mult(2, 3) = ", total);

以下显示结果如下:

![图 6.6 – 使用具有主函数的模块图片

图 6.6 – 使用主函数的模块

注意

注意,我们不再使用 mod1 变量作为对象,而是现在将其作为函数使用。在 mod1(a, b) 的调用中,a 和 b 进行相加,因此,在 require(moduleName) 指令中,将变量命名为 "add" 而不是 "mod1" 更为合适。

我们看到了如何创建和使用我们自己的模块。现在让我们看看如何使用内部 Node.js 模块。

使用内部 Node.js 模块

Node.js 已经有内部模块。它们也可以使用之前看到的 require(moduleName) 指令来使用。

让我们来看一个内部模块的例子。例如,Node.js 系统中的 "fs" 模块。"fs" 名称是文件系统的简称。此模块允许您与 Node.js 的内部文件系统进行交互。

现在,我们将使用 "fs" 模块来读取文件的内容。

读取文件内容

让我们使用 "fs" 模块来读取当前目录(test.js 文件所在的目录)中名为 file1.txt 的文件。以下是该文件的内容:

file1.txt 文件(位于 test.js 文件所在的目录中)

This is the content
of the file file1.txt
located in
the current directory.

使用 "fs" 模块并显示文件内容的程序如下:

读取并显示文件内容(test.js 文件)

var fs = require("fs");
var data = fs.readFileSync("file1.txt");
console.log("File content:");
console.log(data);

我们使用 "fs" 模块中定义的 readFileSync() 方法。它将文件内容返回到相应的变量中,然后将其显示。

![图 6.7 – 使用 "fs" 模块显示文件内容图片

图 6.7 – 使用 “fs” 模块显示文件内容

文件内容以十六进制字符的形式显示。接下来,让我们将文件内容以字符串形式显示。

将文件内容以字符串形式显示

文件内容以字节缓冲区(参见 图 6.7)的形式显示。Node.js 使操作字节流变得容易。也可以通过在 readFileSync(name, options) 方法的第二个参数(options)中指定 {encoding: "utf-8"} 选项,直接以字符串形式查看文件内容:

将文件内容以字符串形式显示(test.js 文件)

var fs = require("fs");
var data = fs.readFileSync("file1.txt", { encoding : "utf-8" });
console.log("File content:");
console.log(data);

结果现在以字符串形式显示(参见以下图):

![图 6.8 – 以字符串形式显示文件内容图片

图 6.8 – 以字符串形式显示文件内容

文件内容被显示。然而,程序等待文件内容被检索以便显示。通过使用 readFile() 方法而不是 readFileSync() 方法,可以在等待文件时不会阻塞程序。

使用非阻塞文件读取

如果你观察之前的readFileSync()方法,你会看到文件内容作为方法调用的返回值被渲染。这意味着在文件读取过程中,Node.js 程序会被阻塞(即使只是几毫秒)。在我们的小型程序中,这并不明显,但在文件读取由成千上万的并发用户执行的情况下(例如,在服务器上),这将减慢对服务器的访问速度。

对于此,Node.js 为所有类似此的阻塞功能提供了一个非阻塞版本的方法。而不是返回方法的结果(如之前所述),我们使用一个作为方法参数的回调函数。在读取文件的情况下,因此我们将使用在"fs"模块中定义的readFile(name, options, callback)方法。读取文件的结果将通过回调函数作为参数传递。

让我们使用非阻塞形式的文件读取,使用readFile()方法代替readFileSync()方法:

使用readFile()方法读取文件(test.js 文件)

var fs = require("fs");
console.log("File content:");
fs.readFile("file1.txt", { encoding : "utf-8" }, function(error, data) {
  console.log(data);
});
console.log("The readFile() method was called");

注意

回调函数使用errordata参数(按此顺序),分别对应可能出现的错误消息(如果没有错误则为null),以及如果已读取,则为文件内容。readFile()方法的第二个参数optionsreadFileSync(name, options)方法的选项类似。

结果在此显示:

图 6.9 – 使用非阻塞的方法显示文件内容

图 6.9 – 使用非阻塞的readFile()方法显示文件内容

我们可以在上述显示的结果中检查到readFile()方法确实是非阻塞的。确实,在调用readFile()方法之后指示的文本即使在文件尚未被读取和显示的情况下也会在控制台显示,这是使用阻塞方法readFileSync()所不可能的。

注意

因此,我们可以看到,在 Node.js 内部模块的使用非常简单,只需使用require(moduleName)指令,然后调用该指令返回的对象上的方法。

我们已经看到了如何创建和使用自己的模块,以及如何使用 Node.js 的内部模块。

现在,让我们看看如何使用npm命令使用互联网上可用的模块。

使用 npm 下载的模块

除了 Node.js 内部的模块,还可以使用 Node.js 提供的npm实用工具从互联网导入模块。

为了此,npm命令(在命令解释器中)通过指定允许你对导入的模块执行相应操作的参数来使用。

使用 npm 命令

这里是npm命令的一些常见用法:

  • npm install moduleName:在本地 node_modules 目录中安装指定的模块。该模块将仅对当前应用程序可用,而对其他应用程序不可用(除非再次安装)。

  • npm install moduleName -g:在全局 node_modules 目录中安装指定的模块。-g 选项允许你指定此模块可以被其他应用程序访问,因为它安装在 Node.js 的 node_modules 目录中(全局)。

  • 使用 npm link moduleName:可能的情况是,全局(使用 -g 选项)安装的模块不可访问(在 require(moduleName) 语句期间出现模块加载错误)。在这种情况下,需要运行 npm link moduleName 命令。

  • npm ll:列出已存在于本地 node_modules 目录中的模块。

  • npm ll -g:列出已存在于全局 node_modules 目录中的模块。

  • npm start:根据 "scripts" 键中指定的命令启动 Node.js 应用程序,然后是 package.json 文件的 "start" 键。例如,如果你在 package.json 文件中指定 "scripts": { "start": "node test.js" },你可以输入 npm start 而不是 node test.js 来运行 test.js 文件。通常使用 npm start 来启动 Node.js 应用程序。这将在 uninstall 而不是 install 的情况下启动应用程序。

以一个例子来说明,让我们在 test.js 文件所在的目录中创建以下 package.json 文件:

package.json 文件(位于与 test.js 相同的目录中)

{
  "scripts" : {
    "start" : "node test.js"
  }
}

然后使用 npm start 命令来启动程序:

![图 6.10 – 使用 npm start 启动 Node.js 应用程序图片

图 6.10 – 使用 npm start 启动 Node.js 应用程序

我们可以看到,npm start 命令使得执行 test.js 程序成为可能。由于上述机制,npm start 命令通常用于启动 Node.js 程序。这将在 uninstall 而不是 install 的情况下启动应用程序。

现在我们来看看如何通过使用 npm 下载来使用其他开发者编写的模块。

使用 npm 下载的模块

让我们来看一个使用 npm 的例子。在这里,我们将使用 npm 来安装名为 colors 的模块。它允许你在控制台中显示彩色文本。

在 node_modules 本地目录中安装 colors 模块

我们使用 npm install colors 命令。以下图显示了 "colors" 模块的安装结果。

![图 6.11 – 使用 npm 安装 colors 模块图片

图 6.11 – 使用 npm 安装 colors 模块

一旦模块通过 npm 安装,你可以看到模块的 colors 目录已经将自己插入到应用程序的 node_modules 本地目录中。

使用 colors 模块的功能

了解模块提供的功能的一种方法是通过显示 require(moduleName) 指令返回的对象的内容来获得概览:

显示由 require("colors") 返回的 colors 对象的内容(test.js 文件)

var colors = require("colors");
console.log("colors = ", colors);

图 6.12 – 显示 colors 模块的内容

图 6.12 – 显示 colors 模块的内容

例如,让我们使用模块中列出的最后一种方法,即 random() 方法。它允许您将字符字符串转换为每个字符都有随机颜色的字符串:

使用 colors 模块的 random() 方法(test.js 文件)

var colors = require("colors");
console.log(colors.random("First text in random colors"));
console.log(colors.random("Second text in random colors"));

注意

使用 random() 方法,通过在变量名前加上 require("colors") 返回的变量名,即模块名,来使用它。

下面的图示显示了每个显示的字符都是随机颜色的效果:

图 6.13 – 使用 colors 模块

图 6.13 – 使用 colors 模块

我们在这里看到了与 Node.js 一起使用的三种模块类型:

  • 为我们自己的需求编写的模块

  • Node.js 中现有的内部模块,例如允许访问 Node.js 内部文件系统的 fs 模块

  • 可以使用 npm 命令下载的模块,例如上面使用的 colors 模块

剩下的就是将这些不同类型的模块用于我们的程序中。我们将在稍后讨论这一点。

这就结束了本章的内容。

概述

在本章中,我们学习了如何使用 Node.js 创建和使用模块,这些模块是使用 Node.js 创建的程序的基本组件。

不论模块是由我们自己创建的,是 Node.js 的内部模块,还是通过 npm 下载的模块,其使用方式在所有情况下都是相同的。我们使用 require(moduleName) 指令,并通过变量返回的值来访问模块的功能。

接下来,我们将研究 Express 模块,这是与 Node.js 一起使用的主要模块之一,它允许我们根据目前广泛使用的 MVC 模式规则轻松地构建我们的应用程序。

第七章:第七章:使用 Node.js 的 Express

在上一章中,我们看到了 Node.js 服务器的程序是由不同的模块组成的。许多模块是由 Node.js 开发者创建的,可以使用npm实用工具(参见第六章创建和使用 Node.js 模块)插入到我们的程序中。其中有一个模块被称为Express。它是 Node.js 中最常用的模块之一,因为它允许你根据模型-视图-控制器MVC)模型来结构化服务器程序。

在本章中,我们将研究如何使用 Express 模块在尊重 MVC 模型特性的同时创建 Node.js 应用程序。

这里是我们将要讨论的主题:

  • 使用 Node.js 的http模块

  • 安装 Express 模块

  • Express 使用的 MVC 模式

  • 使用 Express 的路线

  • 使用 Express 显示视图

Node.js 将其内部模块集成了使用 Node.js 内部的http模块创建 Web 服务器的可能性。我们首先解释如何使用这个http模块,然后我们将看到外部 Express 模块对更易于创建基于 MVC 模型的 Web 应用的贡献。

技术要求

你可以在 GitHub 上找到本章的代码文件:github.com/PacktPublishing/JavaScript-from-Frontend-to-Backend/blob/main/Chapter%207.zip

使用 Node.js 的 http 模块

http模块是 Node.js 的内部模块。因此,我们可以通过使用require("http")指令直接在我们的程序中访问它。使用这个模块,你可以创建基于 HTTP 协议的 Web 服务器,并在互联网浏览器中显示网页。

为了创建基于 HTTP 的 Web 服务器,我们使用http.createServer(callback)方法。作为参数指示的回调函数的形式是callback(req, res),其中req对应于接收到的请求,res对应于要发送给浏览器的响应。根据接收到的请求,将发送相应的响应。

注意

req参数中,除了其他内容外,还有接收到的请求的 URL,因此可以通过res参数根据这个请求返回正确的响应给浏览器。

让我们看看以下程序中如何使用createServer()方法:

使用 http 模块创建 Web 服务器(test.js 文件)

var http = require("http");
var server = http.createServer(function(req, res) {
  // display the received request in the console
  console.log("Request received:", req.url);
  // indicate that the response is HTML in utf-8
  res.setHeader("Content-type", "text/html; charset=utf-8");
  // we always send the same response, regardless of the 
  // request received
  res.write("<h1>")
  res.write("Good morning all");
  res.write("</h1>");
  res.end();
});
// make the server listen on port 3000 (for example)
server.listen(3000);
console.log("\nThe server was started on port 3000\n");
console.log("You can make a request on:");
console.log("http://localhost:3000");

createServer()方法返回一个对象,这里通过名为server的变量使用,我们在其上指示等待来自端口3000(在server.listen(port)方法中指示的端口)的请求。这意味着每次通过浏览器访问形式为http://localhost:3000的 URL 时,先前启动的程序(使用node test.js命令)将被激活,并在浏览器中显示结果。

注意

使用 server.listen(port) 方法是强制性的,因为仅使用 http.createServer() 方法创建服务器是不够的。此服务器还必须监听(使用 server.listen(port ))来自连接到此服务器的浏览器(在此处使用类似 http://localhost:3000 的 URL)发送的 HTTP 请求。此处使用端口号 3000,但也可以使用其他端口号(前提是此端口号未被另一个服务器使用,这会导致访问冲突,不知道端口号上的请求是针对哪个服务器的)。

我们使用 res.write(string) 指令向浏览器发送响应。您必须使用 res.end() 指令完成响应的发送,这意味着浏览器已接收到所有要显示的元素(服务器等待接收 res.end() 指令以显示所有发送的元素)。

注意

使用 res.setHeader() 方法设置 HTTP 头字段。在此处,将 "Content-type" 设置为 "text/html; charset=utf-8"

让我们通过输入命令 node test.js 启动前面的程序。程序显示一条消息,然后等待端口号 3000 上的 HTTP 请求:

![Figure 7.1 – 在端口号 3000 上等待的 HTTP 服务器

![Figure 7.01_B17416.jpg]

图 7.1 – 在端口号 3000 上等待的 HTTP 服务器

要测试程序,请在浏览器中显示以 http://localhost:3000 开头的 URL。当 HTTP 请求使用端口号 3000(服务器正在监听的端口号)时,createServer(callback) 方法中指示的回调函数被激活,然后响应被发送到浏览器。

让我们在浏览器中输入 URL http://localhost:3000(见下图):

![Figure 7.2 – 在浏览器中查看 URL http://localhost:3000

![Figure 7.02_B17416.jpg]

图 7.2 – 在浏览器中查看 URL http://localhost:3000

无论在浏览器中指定的 URL 是什么(使用端口号 3000),浏览器中的显示都保持不变。要使不同 URL 的显示不同,必须在回调函数中使用 req.url 的值来考虑,它包含输入的 URL,并根据接收到的请求返回不同的字符串。

使用 Express 模块可以轻松管理接收到的不同请求,并根据输入的 URL 显示不同的结果。

安装 Express 模块

由于 Express 模块是通过 npm 安装的,我们输入 npm install express 命令来安装它。

![Figure 7.3 – 使用 npm 安装 Express 模块

![Figure 7.03_B17416.jpg]

.

图 7.3 – 使用 npm 安装 Express 模块

Express 模块现在已安装。

注意

与 Express 相关的一个实用工具也有助于创建我们 Web 应用的架构。这就是 "express-generator" 模块(此模块之前包含在 Express 中,但现在已从其中分离出来,因此在此处上传)。

让我们也使用npm install express-generator -g命令安装"express-generator"模块。我们使用-g选项,以便在这个模块中定义的express命令可以从任何目录访问。

图 7.4 – 使用 npm 安装"express-generator"模块

图 7.4 – 使用 npm 安装“express-generator”模块

注意

你可以通过输入命令express -h来验证安装是否正确。如果模块安装正确,窗口中将显示express命令的帮助信息(否则将显示错误)。

一旦安装了这两个模块,你就可以创建一个基于 Express 的第一个 Web 应用程序。

要完成这个任务,请输入express apptest命令来创建名为apptest的应用程序。你应该看到以下结果:

图 7.5 – 使用 Express 创建 apptest 应用程序

图 7.5 – 使用 Express 创建 apptest 应用程序

此命令创建一个包含运行应用程序的基本文件的apptest目录。然后你必须输入显示末尾指示的三个命令:cd apptestnpm installnpm start

一旦输入了这些命令,打开浏览器并显示 URL http://localhost:3000

这是你将看到的内容:

图 7.6 – 使用 Express 创建的默认应用程序主页

图 7.6 – 使用 Express 创建的默认应用程序主页

如果我们查看apptest目录中创建的应用程序的源文件,我们会看到app.jspackage.json文件,以及binnode_modulespublicroutesviews目录。这些目录是描述 Express 使用的 MVC 架构的目录,我们将在下面解释。

Express 使用的 MVC 模式

MVC 模型是一个应用程序架构模型,允许应用程序被分解为不同的部分:模型、视图和控制器:

  • 模型对应于应用程序操作的数据。通常,这是来自数据库的数据。Node.js 与 MongoDB 数据库紧密相连,这将在下一章中探讨。

  • 视图对应于数据的可视化,例如,输入表单和显示列表。每个显示对应于一个将位于应用程序的views目录中的视图。

  • 控制器允许在不同视图之间进行导航,取决于数据。为此,我们使用路由(实际上是 URL)来指示要执行的处理。routes目录描述了应用程序使用的路由(以及为每个路由执行的处理)。

因此,我们可以看到 MVC 模型使得处理、显示和数据分离成为可能。这种分割在 Web 项目中广泛使用,也是 Express 提出的。

让我们先看看 Express 中路由是如何工作的。这对应于 MVC 模型中的控制器部分。

使用 Express 的路由

路由指示基于请求的 URL 要执行的处理。与使用 Node.js 的http模块和createServer(callback)方法时我们编写的内容相比,这包括根据接收到的req请求编写callback(req, res)函数的内容。

路由在app.js文件中描述,这是 Express 创建的主要文件。让我们检查其内容。

app.js 文件的初始内容

要了解 Express 中的路由如何工作,请打开位于主应用程序目录中的app.js文件,你将看到此文件的内容,如下所示:

app.js 文件

var createError = require('http-errors');
var express = require('express');
var path = require('path');
var cookieParser = require('cookie-parser');
var logger = require('morgan');
var indexRouter = require('./routes/index');
var usersRouter = require('./routes/users');
var app = express();
// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'jade');
app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));
app.use('/', indexRouter);
app.use('/users', usersRouter);
// catch 404 and forward to error handler
app.use(function(req, res, next) {
  next(createError(404));
});
// error handler
app.use(function(err, req, res, next) {
  // set locals, only providing error in development
  res.locals.message = err.message;
  res.locals.error = req.app.get('env') === 'development' ? 
  err : {};
  // render the error page
  res.status(err.status || 500);
  res.render('error');
});
module.exports = app;

此文件描述了使用 Express 构建的应用程序的工作方式。它使用app变量,这是express()函数调用的返回值,象征着应用程序。在这个app对象上,多次使用use()方法,这使得可以为服务器接收到的每个请求添加要执行的处理。

例如,app.use(logger("dev"))会在服务器接收到的每个请求上触发logger()函数。这就是为什么服务器控制台在每次向服务器发送请求时都会显示浏览器中请求的 URL。

通过在浏览器中显示 URLhttp://localhost:3000http://localhost:3000/users,我们在服务器控制台中获得了以下内容。

图 7.7 – 服务器控制台中 URL 的显示

图 7.7 – 服务器控制台中 URL 的显示

现在,让我们看看服务器控制台中显示的行的含义。

可能存在不同类型的路由

在前面的图中,你会注意到每个 URL 前都显示了单词GETGET /GET /users

单词GET表示通过 HTTP 请求的GET类型访问 URL//usersGET类型是在访问的 URL 显示在浏览器地址栏时使用的,例如,当你直接输入它或点击页面上的链接时。

注意

其他类型的 HTTP 请求也存在。它们使得不必在浏览器地址栏中显示相应的 URL,从而将其隐藏起来。例如,如果从数据库中删除记录的 URL 在浏览器地址栏中可见,则只需刷新页面即可继续从数据库中删除记录。因此,其他类型的 HTTP 请求允许隐藏当前 URL。

除了GET之外的其他类型的 HTTP 请求主要是PUTPOSTDELETE类型请求。这些类型的请求在程序中用于表示要对一个或多个数据(称为资源)执行的操作:

  • GET表示读取资源。

  • POST表示创建资源。

  • PUT表示更新资源。

  • DELETE表示删除资源。

虽然存在多种类型的 HTTP 请求,但这些都是主要的。它们用于操作资源,允许创建(POST)、更新(PUT)、删除(DELETE)和读取(GET)。

注意

路由是将 HTTP 请求与 URL 相关联。例如,GET /users 路由将 /users URL 与 HTTP GET 请求关联起来,而 DELETE /users 路由将相同的 /users URL 与 HTTP DELETE 请求关联起来。尽管这些路由使用相同的 URL,但由于 HTTP 请求不同,它们是不同的路由。

既然我们已经看到了使用的不同类型的 HTTP 请求,让我们看看 Express 如何在内部使用它们。

分析 app.js 文件中定义的路由

app.use() 方法也用于定义新的路由,即定义每个新 URL 使用时(与关联的请求类型)将执行的处理。

使用 app.use(url, callback) 方法定义在指定 URL 激活时将执行的处理。由于此处未指定请求类型,因此将激活回调函数中指示的所有类型的请求处理。要指示请求类型,必须使用类似于 app.use() 的方法。这些是 app.get()app.put()app.post()app.delete() 方法。

注意

形式为 callback(req, res, next) 的回调函数将响应返回给浏览器。next() 参数对应于在回调结束时需要调用以继续在下一个回调函数中处理(如果要执行的处理由多个回调函数处理)的函数。

app.js 中已定义的路由是 //users,这使得能够运行与这些路由关联的过程。这些路由是示例,用于展示如何在 app.js 文件中实现路由。处理指令定义在 indexRouterusersRouter 函数中。这些函数是用于返回指令 require('./routes/index')require('./routes/users') 的变量。因此,路由处理是在 routes 目录中定义的 index.jsusers.js 文件中完成的。

让我们打开这两个文件并分析其内容:

index.js 文件(路由目录)

var express = require('express');
var router = express.Router();
/* GET home page. */
router.get('/', function(req, res, next) {
  res.render('index', { title: 'Express' });
});
module.exports = router;

users.js 文件(路由目录)

var express = require('express');
var router = express.Router();
/* GET users listing. */
router.get('/', function(req, res, next) {
  res.send('respond with a resource');
});
module.exports = router;

这些文件中的每一个都使用 router.get(url, callback) 方法,这意味着路由与 / 相关联(它将与 app.js 文件中给出的 URL 连接),然后是形式为 callback(req, res, next) 的回调函数。next 参数对应于如果必须在后续的回调函数中继续处理(如果存在这样的函数,这里的情况就是这样)则需要调用的函数。

在每个回调函数中执行的处理包括发送响应,该响应将在浏览器中显示。在这里,我们使用 res.send()res.render() 方法,这些方法允许发送响应:

  • res.send() 方法类似于 res.end()(在 Node.js 的 "http" 模块中定义),但它还允许您指明您正在使用 HTML,并且必须在处理过程中使用 res.send() 方法,否则将发生错误。

  • res.render() 方法允许显示外部文件(称为视图)。视图是用一种特殊语言编写的,这取决于视图的格式。默认情况下,Express 使用的视图是 JADE 文件,但也可以使用其他格式。

在这里,res.render() 方法显示的视图对应于位于 views 目录中的 index.jade 文件。其内容如下:

index.jade 文件(视图目录)

extends layout
block content
  h1= title
  p Welcome to #{title}

此文件使用一种特定的语法编写,称为 JADE。Express 将在将文件发送到浏览器(只能解释 HTML)之前将其转换为 HTML 代码。

注意

Express 允许使用各种语法编写与视图关联的文件。最常见的是 JADEEJS

我们将在本章的 使用 Express 显示视图 部分中探索 JADE 语法。

注意,app.js 文件允许您配置与视图关联的目录以及视图中所使用的语法。以下是来自 app.js 文件的相应说明:

配置视图(app.js 文件)

app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'jade');

我们已经描述了 app.js 文件中已经列出的路由。让我们看看如何在这个文件中创建新路由。

app.js 文件中添加新路由

app.js 文件中添加新路由可以通过直接在 app.js 文件中编写处理程序或创建一个位于 routes 目录中的外部文件来完成。

警告

任何对 app.js 文件的修改都需要通过执行 npm start 命令来重新启动服务器;否则,修改不会被考虑。

让我们看看这两种创建新路由的方法。

直接在 app.js 文件中添加路由处理

让我们添加一个由 app.get() 方法定义的路由 /clients 被激活:

添加 GET /clients 路由

app.use('/', indexRouter);
app.use('/users', usersRouter);
app.get("/clients", function(req, res, next) {
  res.send("<h1>Client list</h1>");
});

结果显示在以下图中(图 7.8)。

创建一个外部文件来定义路由处理

我们使用与 app.js 文件中定义的 GET /GET /users 路由相同的原理。我们在 routes 目录中创建 clients.js 文件,该文件将通过 clientsRouter = require("./routes/clients") 语句包含在 app.js 文件中。路由在 app.js 中通过 app.use("/clients", clientsRouter) 语句定义。

描述在路由上执行的处理过程的 clients.js 文件如下:

clients.js 文件(路由目录)

var express = require('express');
var router = express.Router();
router.get('/', function(req, res, next) {
  res.send("<h1>Client list</h1>");
});
module.exports = router;

在这两种情况下,结果都是相同的,如以下图所示。

图 7.8 – 显示 GET /clients 路由

图 7.8 – 显示 GET /clients 路由

我们使用 Express 中定义的路由系统研究了 MVC 模型中的控制器部分。现在让我们看看 Express 如何帮助我们管理 MVC 模型的视图部分。

使用 Express 显示视图

视图是一个外部文件,用于描述你想要查看的显示。已经创建了特定的语法来编程视图,例如,JADE 或 EJS 语法。

res.render(name, obj) 方法用于使用在 obj 对象中提供的任何属性显示名为 name 的视图。视图是一个使用 JADE 语法或其他语法定义在 views 目录中的文件。

Express 的一个特性是允许你使用所需的语法创建视图。JADE 语法是 Express 作为标准提供的,但可以通过 npm 添加其他语法支持库。

因此,JADE 语法是 Express 默认使用的语法。它使得可以用标签的名称替换 HTML 标签(例如 <h1> 简单地变为 h1),并且代码中标签的缩进使得可以指定它们的嵌套。也不再需要关闭之前打开的标签,因为缩进允许你看到标签的嵌套。

注意

完整的 JADE 文档可以在jade-lang.com/找到。

让我们使用 JADE 来显示之前的客户列表。我们在 views 目录中创建 clients.jade 视图,并在 clients.js 中指示在访问 GET /clients 路由时显示此视图:

clients.js 文件(路由目录)

var express = require('express');
var router = express.Router();
router.get('/', function(req, res, next) {
  res.render("clients");   // display clients.jade view 
// (.jade extension is enabled by 
                           // default)
});
module.exports = router;

注意,如果你没有指定视图文件的扩展名(例如,通过写入 res.render("clients")),将使用 app.js 指令中指定的扩展名 app.set('view engine' , 'jade')

如果,另一方面,你指定了视图文件的扩展名,即使它与 app.js 中配置的不同,它也将用于显示视图。视图 clients.jade 如下所示:

clients.jade 文件(视图目录)

h1 Client list
ul
  li Bill Clinton
  li Barack Obama
  li Joe Biden

注意标签的缩进。ul 标签与 h1 标签处于同一级别,否则它会被视为 h1 标签的一部分。li 标签向右移动以显示它们属于前面的 ul 标签。偏移量至少为字符数。由于偏移量,我们不使用 HTML 中的关闭标签。

由于其中一个路由文件已被修改,让我们使用 npm start 重启服务器。

注意

编辑与视图相关的文件不需要重启服务器,这与 app.js 文件和路由目录中的文件不同。

服务器重启后,再次显示 URL http://localhost:3000

图 7.9 – 使用 JADE 语法显示的视图

图 7.9 – 使用 JADE 语法显示的视图

在这个例子中,客户列表直接输入到 JADE 视图中。更好的做法是使用res.render(name, obj)方法的第二个参数作为参数传递。然后clients.js文件变成以下内容:

clients.js 文件(路由目录)

var express = require('express');
var router = express.Router();
router.get('/', function(req, res, next) {
 res.render("clients", { 
    clients : [
      { firstname : "Bill", lastname : "Clinton" },
      { firstname : "Barack", lastname : "Obama" },
      { firstname : "Joe", lastname : "Biden" },
    ]
  });
});
module.exports = router;

res.render("clients", obj)方法的obj参数是一个包含客户列表的对象。

clients.jade视图使用传递的对象如下:

clients.jade 文件(视图中目录)

h1 Client list
ul
  li #{clients[0].lastname + " " + clients[0].firstname}
  li #{clients[1].lastname + " " + clients[1].firstname}
  li #{clients[2].lastname + " " + clients[2].firstname}

传入参数的obj对象在 JADE 视图中使用,这里通过其clients属性来使用。

JADE 语法

可以在 JADE 视图中使用 JavaScript 语句,通过将它们包围在#{}中来使用。这两个标记之间的一切都将被视为 JavaScript 代码。

你还可以使用 JADE 允许的语法简化,通过在每个li标签后直接写上=符号。这意味着该行之后的所有内容都必须按 JavaScript 解释。我们可以在这里使用这种简化写法。

让我们按照以下方式编写clients.jade视图:

clients.jade 文件(视图中目录)

h1 Client list
ul
  li= clients[0].lastname + " " + clients[0].firstname
  li= clients[1].lastname + " " + clients[1].firstname
  li= clients[2].lastname + " " + clients[2].firstname

在视图中列出clients数组的每个元素,你也可以使用 JADE 语法的each语句来遍历 JavaScript 数组执行循环。

因此,clients.jade视图变成了以下内容:

clients.jade 文件(视图中目录)

h1 Client list
ul
  each client in clients
    li= client.lastname + " " + client.firstname

视图的编写被简化了,但你需要真正注意行的缩进,否则视图将不会显示。

![图 7.10 – 由 each 语句显示的客户列表图片 7.10_B17416.jpg

图 7.10 – 由 each 语句显示的客户列表

在这个例子中,我们看到 JADE 语法使得在应用程序的视图中显示数据列表变得很容易。

有了这个,我们就到了本章的结尾。

摘要

Express 模块通过允许(多亏了它使用的 MVC 模型)你分离路由管理、显示视图和数据管理,使得高效地构建应用程序成为可能。

我们已经解释了如何使用 Express 默认提供的 JADE 语法编写应用程序的视图。其他语法,例如 EJS 语法,也可以通过通过npm下载来获得。

我们还看到了 Express 创建的app.js文件的重要性,以及 HTTP 请求如GETPOSTPUTDELETE的使用。我们将在第九章将 Vue.js 与 Node.js 集成中看到这些 HTTP 请求在构建一个 MEVN 应用程序(代表 MongoDB, Express, Vue.js, Node.js)中的重要性,该应用程序操作 MongoDB 数据库。

事实上,数据管理通常使用MongoDB数据库来完成,我们将在下一章中探讨其使用。

第八章:第八章:使用 Node.js 与 MongoDB

MongoDB 是传统上与 Node.js 关联的数据库。它是一种 NoSQL 类型的数据库,这意味着不会使用 SQL 来访问其中包含的信息。

MongoDB 是一种面向文档的数据库,我们在其中存储所谓的 文档;也就是说,任何类型的数据结构,例如写在一张纸上的信息(然后相当于一个文档)。几张纸,因此对应几个文档,形成所谓的 集合

例如,一个文档可以是客户的姓名、姓氏和地址。来自多个客户的聚合信息被称为集合。

在本章中,我们将研究如何结合 Node.js 使用 MongoDB 来存储、读取、删除或更新数据库中的信息。

插入、搜索、更新或删除数据是可以在数据库中执行的主要操作。因此,在本章中,我们将看到如何使用 MongoDB 数据库执行这些操作。

本章涵盖以下主题:

  • 安装 MongoDB 和 mongoose 模块

  • 连接到 MongoDB 数据库

  • 创建文档

  • 搜索文档

  • 更新文档

  • 删除文档

让我们先安装 MongoDB 和 mongoose 模块,这将允许在 Node.js 程序中使用 MongoDB。

技术要求

您可以在 GitHub 上找到本章的代码文件:github.com/PacktPublishing/JavaScript-from-Frontend-to-Backend/blob/main/Chapter%208.zip

安装 MongoDB

MongoDB 数据库独立于 Node.js,需要单独安装。为此,请访问网站 www.mongodb.com/docs/manual/administration/install-community/。下载适合您系统的版本。

安装 MongoDB 后,通过在命令解释器中输入 mongo -h 命令来验证安装是否正确。mongo 命令位于 MongoDB 的 Server/x.x/bin 目录中,其中 x.x 是安装的 MongoDB 版本号。

注意

在撰写本文时,mongo 工具在安装 MongoDB 时直接可用。然而,这个工具可能很快就会作为单独的版本提供,并被称为 mongosh。在这种情况下,请从 www.mongodb.com/docs/mongodb-shell/install/ 下载此工具。

mongo 命令将被等效的 mongosh 命令替换。这两个命令的工作方式相同。

在安装 MongoDB 之后,我们将探讨 mongo(或 mongosh)实用工具。mongo 实用工具使得查看数据库集合的内容变得容易,而无需编写程序行。因此,它对于检查,例如,文档是否已正确插入到集合中,或其删除是否成功,非常有用。让我们看看如何使用 mongo 实用工具。

使用 mongo 实用工具

mongo 实用工具使您能够轻松查看数据库及其包含的集合。只需在命令解释器中输入mongo命令即可启动 mongo 实用工具。程序随后将等待数据库访问命令,或输入exit命令以退出。

这里是 mongo 实用工具中可用的主要命令列表:

  • show dbs: 这显示了现有数据库的列表。只有当数据库包含至少一个集合时,数据库才会在此处可见。

  • db=connect("mydb_test"): 这是连接到数据库mydb_test。然后db变量将用于访问数据库集合。

  • show collections: 这显示了连接数据库的集合。如果集合包含至少一个文档,则将存在集合。

  • db.clients.find(): 这显示了clients集合中的所有文档。

  • db.clients.find({name:"Clinton"}): 这列出了clients集合中名为Clinton的文档。

  • db.clients.find().sort({name:1}): 这按name字段的升序对文档进行排序。使用{name:-1}进行降序排序。

  • db.clients.count(): 这计算在clients集合中找到的文档数量。

  • db.clients.renameCollection("clients2"): 这将clients集合重命名为clients2

  • db.clients.drop(): 这将删除clients集合(所有文档都将被删除)。

  • db.dropDatabase(): 这将删除连接的数据库(所有集合都将被移除)。

其他命令存在,特别是用于在集合中插入、更新或删除文档的命令。但由于这些操作是通过 mongoose 模块执行的,我们将使用 mongoose 模块来描述它们。

安装 mongoose 模块

为了建立 MongoDB 和 Node.js 之间的关系,已经创建了几个 npm 模块。目前最广泛使用的是mongoose模块。通过输入npm install mongoose命令,它被安装在本目录的node_modules目录中。

![图 8.1 – 安装 mongoose 模块图片 8.01_B17416.jpg

图 8.1 – 安装 mongoose 模块

一旦通过 npm 下载了 mongoose,我们将检查它是否可用于我们的程序。让我们显示程序的 mongoose 版本。我们将此代码片段写入test.js文件:

显示 mongoose 版本(test.js 文件)

var mongoose = require("mongoose");
console.log("mongoose version =", mongoose.version);

让我们使用node test.js命令来运行前面的程序:

![图 8.2 – 检查 mongoose 是否可访问图片 8.02_B17416.jpg

![图 8.2 – 检查 mongoose 是否可访问警告如果你加载 mongoose 模块时遇到错误,那可能是因为你全局安装了它(使用 -g 选项)。在这种情况下,只需在终端中输入 npm link mongoose 命令来消除错误。mongoose 模块将允许我们使用 MongoDB 数据库创建文档、搜索它们、更新它们或销毁它们。这些是在数据库上可以执行的经典操作。但要能够执行这些操作,首先需要连接到数据库。# 连接到 MongoDB 数据库访问 MongoDB 的所有操作都需要与它建立连接。现在让我们看看如何与 MongoDB 建立连接。mongoose.connect(url) 指令将 mongoose 模块连接到 url 参数指定的数据库。url 参数的形式为 "mongodb://localhost/mydb_test",用于连接到本地服务器上的 mydb_test 数据库。当第一个文档被插入到数据库中时,数据库实际上将被创建(并且可以通过执行 mongo 实用工具的 show dbs 命令来看到):连接到 mydb_test 数据库(test.js 文件)jsvar mongoose = require("mongoose");``````jsmongoose.connect("mongodb://localhost/mydb_test");``````jsconsole.log("Connecting to mydb_test database in progress...");让我们运行之前的程序:图 8.3 – 数据库连接

图片

图 8.3 – 数据库连接

为了知道数据库连接是否真正建立,mongoose 在 mongoose.connection 对象上发送 open 事件(如果连接成功)或 error 事件(如果连接失败)。

接下来,我们将考虑这两个事件并将它们集成到之前的程序中。这是通过在 mongoose.connection 对象上定义的 on(event, callback) 方法来完成的:

注意

on(event, callback) 方法用于处理事件的接收并将其与回调函数中描述的处理相关联。

在数据库连接中使用 open 和 error 事件(test.js 文件)

var mongoose = require("mongoose");
mongoose.connect("mongodb://localhost/mydb_test");
mongoose.connection.on("error", function() {
 console.log("mydb_test database connection error")
});
mongoose.connection.on("open", function() {
 console.log("Successful connection to mydb_test 
 database");
});
console.log("Connecting to mydb_test database in progress...");

让我们运行之前的程序:

图 8.4 – 成功连接到数据库

图片

图 8.4 – 成功连接到数据库

我们已经看到了如何连接到数据库。因此,我们将在数据库的集合中创建文档。

在 MongoDB 中创建文档

一旦访问了数据库,你就可以在其中创建文档。

文档将被插入到一个 集合 中。因此,集合将把一组文档组合在一起。数据库因此将是一组集合,每个集合包含文档。

为了能够插入文档,mongoose 要求我们描述这些文档的结构。为此,我们将使用模式和模型。

使用模式和模型描述文档结构

要访问数据库中的文档,必须通过模式和模型来描述这些文档。

定义

模式 允许你定义存储在集合中的文档的结构。结构是根据 MongoDB 数据类型定义的。

模型 是方案作为 JavaScript 类的表示。它将方案链接到 MongoDB 集合。

让我们看看如何创建方案和模型。

创建方案

方案使用 Node.js 内部对象类定义文档的字段。以下是一些类:

  • String: 这定义了一个字符字符串。

  • Number: 这定义了一个数值字段。

  • Boolean: 这定义了一个布尔值。

  • Array: 这定义了一个数组。

  • Buffer: 这定义了一个字节缓冲区。

  • Date: 这定义了一个日期。

  • Object: 这定义了一个 JavaScript 对象。

使用 mongoose.Schema(format) 方法来定义与文档关联的模式。format 参数是一个 JavaScript 对象,它将文档中的每个字段与表示它的类型(在上面的列表中)关联起来。

让我们创建定义客户端的方案。客户端以其 lastnamefirstnameaddress 为特征。所有这些字段都是 String 类型:

定义与客户端关联的模式(test.js 文件)

var mongoose = require("mongoose");
mongoose.connect("mongodb://localhost/mydb_test");
var clientSchema = mongoose.Schema({
 lastname : String,
 firstname : String,
 address : String
});

现在让我们解释如何从方案创建模型。

创建模型

然后使用该模式来定义与文档关联的模型。该模型对应于一个 JavaScript 类,该类将用于在集合中创建文档。

mongoose.model(collection, schema) 方法返回与方案关联的 JavaScript 类。这个类被称为模型。

使用此类创建的文档将被插入到指定的 collection 中。在插入文档之前,集合可能不存在。一个集合至少需要包含一个文档。

摘要

方案指定了存储在集合中的文档的格式,而模型是用于创建每个此类文档的 JavaScript 类。我们使用 mongoose.model(collection, schema) 方法调用将文档方案与集合关联起来。这返回一个 JavaScript 类,然后可以用来生成单个文档实例。

让我们创建 Client 类,它将创建存储在 clients 集合中的客户端。根据模型名称命名集合是传统的,应使用小写且为复数形式:

从方案创建 Client 模型(test.js 文件)

var mongoose = require("mongoose");
mongoose.connect("mongodb://localhost/mydb_test");
var clientSchema = mongoose.Schema({
 lastname : String,
 firstname : String,
 address : String
});
// creation of the Client class associated with the clients 
// collection
var Client = mongoose.model("clients", clientSchema);

Client 类现在可用于创建将插入到 clients 集合中的文档。

创建文档

创建集合中的文档有两种方法。这些是 doc.save(callback) 实例方法和 create(doc, callback) 类方法。让我们看看这两种在集合中创建文档的方法。

让我们首先使用 doc.save(callback) 实例方法。

使用 doc.save(callback) 实例方法

客户端文档是通过之前创建的类(通过 var client = new Client())在内存中创建的,然后通过 client.save() 方法保存到 clients 集合中。

回调函数允许在文档完成插入到集合后进行处理。这在需要等待文档插入到数据库后再继续处理时特别有用:

使用 save() 实例方法保存文档(test.js 文件)

var mongoose = require("mongoose");
mongoose.connect("mongodb://localhost/mydb_test");
var clientSchema = mongoose.Schema({
 lastname : String,
 firstname : String,
 address : String
});
// creation of the Client class associated with the clients 
// collection
var Client = mongoose.model("clients", clientSchema);
// create the document in memory
var c = new Client({lastname :"Clinton", firstname:"Bill", address:"Washington"});
console.log("Before the save() statement");
// save the document in the database (clients collection)
c.save(function(err) {
if (!err) console.log("The client is inserted into the 
  collection");
});
console.log("After the save() statement");

回调函数接受 err 参数,它对应于可能出现的错误消息(否则,它是 null)。

我们得到以下结果:

图 8.5 – 使用 doc.save() 实例方法

图 8.5 – 使用 doc.save() 实例方法

使用控制台显示的跟踪信息,我们可以看到消息 The client is inserted into the collection 在其他消息之后显示,这意味着插入文档不会阻塞其他任务(即,其他任务可以在等待数据库插入时完成)。

save() 方法也可以用作 Promise 对象(参见 第二章探索 JavaScript 的高级概念)。为此,我们随后使用 then(callback) 方法,可能还会跟随着 catch(callback) 方法来处理调用 save() 方法时出现的错误情况。

在这种情况下,我们编写以下内容:

使用 save() 方法作为 Promise 对象

c.save().then(function(doc) {
  console.log(doc);
  console.log("The client is inserted into the collection");
}).catch(function(err) {
  console.log(err);  // display the error
});

现在,让我们看看使用 create(doc, callback) 类方法创建文档的另一种方法。

使用 create(doc, callback) 类方法

类方法意味着我们可以使用该方法而不需要实例化一个对象,这与需要创建类对象(使用 c = new Client())的实例方法不同。

要创建与 {lastname:"Obama", firstname:"Barack", address:"Washington"} 标识的客户关联的文档,我们会编写以下内容:

使用 Client.create(doc, callback) 类方法保存文档(test.js 文件)

var mongoose = require("mongoose");
mongoose.connect("mongodb://localhost/mydb_test");
var clientSchema = mongoose.Schema({
 lastname : String,
 firstname : String,
 address : String
});
// creation of the Client class associated with the clients 
// collection
var Client = mongoose.model("clients", clientSchema);
console.log("Before the create() statement");
// save the document in the database (clients collection)
Client.create({lastname:"Obama", firstname:"Barack", address:"Washington"}, function(err, doc) {
console.log("The client is inserted into the collection", 
  doc);
});
console.log("After the create() statement");

create(doc, callback) 类方法通过在它前面加上 JavaScript 类的名称(这里,Client 类)来使用。

要保存的文档以 JavaScript 对象(JSON 格式)的形式编写,但也可以是使用 c = new Client() 实例化的对象。

形式为 callback(err, doc) 的回调函数在文档保存在数据库后执行。如果你想在确定文档已保存在集合中时执行某个过程,这个回调函数很有用。

注意

注意,create(doc, callback) 方法的回调函数 callback(err, doc) 有两个参数 errdoc,分别对应可能的错误和保存在数据库中的文档。

让我们运行前面的程序:

图 8.6 – 使用 Client.create() 类方法

图 8.6 – 使用 Client.create() 类方法

保存的文档具有与模型关联的格式中指示的字段(在此处为 lastnamefirstnameaddress 字段),但也包含 _id__v 字段,这些字段由 MongoDB 自动添加:

  • _id 字段是 MongoDB 用于为集合中的每个文档提供唯一标识符的字段。它扮演着主键的角色。

  • __v 字段是由 mongoose 添加的字段,与文档版本号相关联。我们在此处不会使用它。

save() 实例方法一样,create(doc) 类方法也可以用作 Promise 对象。为此,我们不在 create(doc) 方法中使用 callback 参数,而是在 create(doc) 方法调用之后使用 then(callback)catch(callback) 方法。

例如,我们也可以编写以下内容:

将 create() 方法用作 Promise 对象

Client.create({lastname:"Obama", firstname:"Barack", address:"Washington"}).then(function(doc) {
  console.log("The client is inserted into the collection", 
  doc);
});

在前面的示例中,我们已经将两个文档插入到 clients 集合中。让我们使用 mongo 工具显示插入的文档并验证集合中存在的文档。

使用 mongo 工具查看插入的文档

要显示插入的文档,请使用 mongo 工具并输入以下命令:

  1. db=connect("mydb_test") 用于连接到数据库

  2. show collections 用于显示已存在的集合

  3. db.clients.find() 用于显示 clients 集合中的文档

图 8.7 – 使用 mongo 工具查看文档

图 8.7 – 使用 mongo 工具查看文档

因此,我们检查 clients 集合中的两个文档确实存在。

让我们看看如何使用 mongoose 模块方法来搜索它们。

在 MongoDB 中搜索文档

一旦文档已插入到集合中,就可以使用 find() 类方法进行搜索。

注意

find() 方法是一个类方法,这意味着它通过在类名前加上与模型关联的类名来使用,例如,Client.find()

find(conditions, callback) 方法用于在关联的模型集合中执行搜索,然后在回调函数中检索搜索结果,该回调函数作为参数指示。

让我们深入探讨一下参数:

  • conditions 参数是一个 JavaScript 对象,用于指定搜索条件。如果没有指定条件,则不进行任何指示(或指示一个空对象 {})。

  • 回调函数的形式为 callback(err, results),其中 err 是错误消息(否则为 null),results 是包含搜索结果的数组(如果没有找到则为空数组 [])。

此外,还有一个 findOne(conditions, callback) 类方法,它允许你根据相同的原则找到满足搜索条件的第一个文档。回调函数的形式为 callback(err, result),其中 result 是找到的第一个文档。

注意

如果你正在寻找单个文档,例如通过其标识符 _idfindOne(conditions, callback) 方法将很有用。

你也可以使用 find(conditions)findOne(conditions) 方法,而不必指定回调函数作为参数。为此,我们使用 then(callback)catch(callback) 方法来处理找到的文档或发生错误时的事件。我们还可以使用 exec(callback) 方法,如以下章节所述。

让我们现在看看如何编写在两个方法 find()findOne() 中使用的 conditions 参数。

编写搜索条件

conditions 参数中,我们指明一个对象,其属性是集合中文档的字段,相关值是字段所寻求的值,形式为 {field1:value1, field2:value2...},例如,{lastname:"Clinton", firstname:"Bill"}

可以使用其他属性作为关键字来表示条件。它们以 $ 符号开头,例如:$or$exists$type$where$gt$lt

注意

可以在此处找到可能的关键字列表:docs.mongodb.com/manual/reference/operator/query/

这里有一些条件示例:

  • { }: 集合中的所有文档。你也可以写 find(),它与 find({}) 等价。

  • { lastname: "Clinton" }: 所有姓氏为 Clinton 的文档。

  • { lastname: "Clinton", firstname: "Bill" }: 所有姓氏为 Clinton 且名字为 Bill 的文档。

  • { $or: [{ lastname: "Clinton"}, { firstname: "Jimmy" }] }: 所有姓氏为 Clinton 或名字为 Jimmy 的文档。

  • { lastname: /obama/i }: 所有姓氏包含字符串 obama(不区分大小写)的文档(正则表达式)。

  • { address: { $exists: true} }: 所有 address 字段存在的文档,无论其类型(字符串、对象等)。

  • { address: { $exists: true, $type: 2 } }: 所有 address 字段存在且类型为 2(字符串)的文档。

  • {"address.city": "Washington" }: 包含 address 字段且该字段本身有一个城市字段,其值为 Washington 的所有文档。

  • {lastname:{$type:2}, $where:"this.lastname.match(/^Clinton|carter$/i)"}: 所有姓氏为字符串(类型 = 2)且姓氏以 Clinton 或以 carter 结尾(不区分大小写)的文档。你必须指明姓氏是字符字符串,否则你可能会在不符合此格式的姓名上遇到错误。

  • {lastname: { $gt: "J", $lt: "S" }}: 所有姓氏大于 "J" 且小于 "S" 的文档。

  • {lastname: { $in:["Clinton", "Carter", "Obama"] }} : 所有姓氏为 ClintonCarterObama 的文档。

一旦表达出搜索条件,就必须检索和显示找到的结果。让我们看看如何做。

检索和显示结果

无论表达的条件是什么,都可以在 find() 方法的关联回调函数中检索相应的结果,形式为 callback(err, results)。我们还将看到可以使用 exec(callback) 方法来检索结果。

让我们看看这两种检索搜索结果的方法。

使用 find(conditions, callback) 方法的回调参数

让我们查找所有姓氏为 Clinton 或名字为 Barack 的客户。结果将在回调函数中显示:

显示姓氏为 “Clinton” 或名字为 “Barack” 的客户(test.js 文件)

var mongoose = require("mongoose");
mongoose.connect("mongodb://localhost/mydb_test");
var clientSchema = mongoose.Schema({
 lastname : String,
 firstname : String,
 address : String
});
// creation of the Client class associated with the clients 
// collection
var Client = mongoose.model("clients", clientSchema);
Client.find({ $or : [ { lastname : "Clinton" }, { firstname : "Barack"} ] }, function(err, clients) {
  console.log(clients);
});

我们将获得以下图中显示的结果:

图 8.8 – 使用 find(conditions, callback) 显示搜索结果

img/Figure_8.08_B17416.jpg

图 8.8 – 使用 find(conditions, callback) 显示搜索结果

回调函数可以在之前的 find() 方法中表达,或者在 find() 方法之后使用的 exec() 方法中指定。现在让我们来考察第二种可能性。

使用 exec(callback) 方法

另一种检索结果的方法是在 find(conditions) 方法之后使用 exec(callback) 方法。在这里使用 find(conditions) 方法时没有在其参数中指定回调函数,因为回调函数已在 exec(callback) 方法中指定。

这种方法的优点是,我们可以在 find() 方法和 exec() 方法之间插入新方法。例如,如果我们想添加额外的条件,即 lastname 字段必须等于 Clinton,我们可以编写以下内容:

将搜索条件添加为姓氏为 “Clinton”(test.js 文件)

var mongoose = require("mongoose");
mongoose.connect("mongodb://localhost/mydb_test");
var clientSchema = mongoose.Schema({
 lastname : String,
 firstname : String,
 address : String
});
// creation of the Client class associated with the clients 
// collection
var Client = mongoose.model("clients", clientSchema);
Client.find({ $or : [ { lastname : "Clinton" }, { firstname : "Barack"} ] })
.where("lastname")
.eq("Clinton")
.exec(function(err, clients) {
  console.log(clients);
});

注意

where(field)eq(value) 等方法可以在 find() 方法之后链式使用。调用 exec() 方法时,搜索将有效执行。其他使用可能性在此描述:mongoosejs.com/docs/api/query.html#query_Query-where

您也可以使用 exec(callback) 方法,而不必指定回调函数作为参数。为此,我们使用 then(callback)catch(callback) 方法来处理找到的文档或发生错误时进行的处理。

我们可以这样写:

将 exec() 方法作为 Promise 对象使用

Client.find({ $or : [ { lastname : "Clinton" }, { firstname : "Barack"} ] })
.where("lastname")
.eq("Clinton")
.exec()
.then(function(clients) {
  console.log(clients);  // display the clients
})
.catch(function(err) {
  console.log(err);  // display the error
});

结果将在以下图中显示。

图 8.9 – 使用  方法

img/Figure_8.09_B17416.jpg

图 8.9 – 使用 exec(callback) 方法

我们已经学会了如何创建文档,然后搜索它们。现在让我们看看如何更新它们。

在 MongoDB 中更新文档

可以修改集合中的一个或多个文档。分别使用 updateOne()updateMany() 类方法来修改找到的第一个文档或所有找到的文档。

这两种方法具有类似的参数:

  • updateMany(conditions, update, callback) 表示根据指示的 conditions 在指定的文档上修改 update 对象中指示的数据。更新后,将调用形式为 callback(err, response) 的回调函数。

  • updateOne(conditions, update, callback) 表示根据指示的 conditions 修改第一个找到的文档上的 update 对象中指示的数据。更新后,将调用形式为 callback(err, response) 的回调函数。

  • 在这两种方法中,conditionsupdate 参数是必须的。

    警告

    如果方法中没有回调函数,你必须随后使用 then()exec() 方法,否则更新不会完成。

让我们修改 Clinton 的地址,现在将是 纽约

使用 updateOne() 修改 "Clinton" 的地址(test.js 文件)

var mongoose = require("mongoose");
mongoose.connect("mongodb://localhost/mydb_test");
var clientSchema = mongoose.Schema({
 lastname : String,
 firstname : String,
 address : String
});
// creation of the Client class associated with the clients 
// collection
var Client = mongoose.model("clients", clientSchema);
Client.updateOne({ lastname : "Clinton" }, { address : "New York" }, function(err, response) {
  console.log("response =", response);
});

在这里,我们使用回调函数来显示函数返回的 response 参数的内容。我们得到以下结果:

![图 8.10 – 更新文档]

![img/Figure_8.10_B17416.jpg]

图 8.10 – 更新文档

注意

response.modifiedCount 字段表示修改的文档数量。

如果你不想在更新结束时执行任何处理,你可以省略回调函数,但在此情况下,你必须随后使用 then()exec() 方法,否则更新将不会发生。

让我们使用 exec() 方法执行更新:

使用 exec() 方法执行更新(test.js 文件)

var mongoose = require("mongoose");
mongoose.connect("mongodb://localhost/mydb_test");
var clientSchema = mongoose.Schema({
 lastname : String,
 firstname : String,
 address : String
});
// creation of the Client class associated with the clients 
// collection
var Client = mongoose.model("clients", clientSchema);
Client.updateOne({ lastname : "Clinton" }, 
                 { address : "New York" })
.exec();    // exec() mandatory!

一旦你知道如何创建、搜索和修改文档,你只需要知道如何删除它们。让我们看看如何操作。

MongoDB 中的文档删除

updateOne()updateMany() 类似,存在两个类方法,即 deleteOne(conditions, callback)deleteMany(conditions, callback),允许你删除满足表达条件的第一个文档(deleteOne())或所有文档(deleteMany())。

此外,实例方法 doc.remove(callback) 还使得在文档在内存中时删除 doc 文档成为可能。

让我们使用 deleteOne() 方法从集合中删除 Clinton,然后显示集合的新内容:

使用 deleteOne() 删除客户端 "Clinton"(test.js 文件)

var mongoose = require("mongoose");
mongoose.connect("mongodb://localhost/mydb_test");
var clientSchema = mongoose.Schema({
 lastname : String,
 firstname : String,
 address : String
});
// creation of the Client class associated with the clients 
// collection
var Client = mongoose.model("clients", clientSchema);
Client.deleteOne({ lastname : "Clinton" }, function(err, response) {
  console.log("After Clinton's removal");
  console.log("response = ", response);
  Client.find(function(err, clients) {
    console.log("clients = ", clients);
  });
});

updateOne()updateMany() 方法类似,存在触发数据库更新的回调函数。如果你没有指定回调函数,你必须在这种情况下使用 then()exec() 方法,否则更新将不会发生。

结果如下图所示:

![图 8.11 – 使用 deleteOne() 删除 "Clinton" 客户端]

![img/Figure_8.11_B17416.jpg]

图 8.11 – 使用 deleteOne() 删除 “Clinton” 客户端

deleteOne()(或 deleteMany())方法的回调函数返回的 response 对象指示 deletedCount 字段,其中包含删除的文档数量。

我们已经依次研究了在 MongoDB 数据库中对文档的四种可能操作,即插入、搜索、修改和删除文档。至此,本章内容结束。

摘要

由于使用了 mongoose 等外部模块,MongoDB 的数据管理相对简单,所有数据库上的可能操作都容易实现。

当安装 MongoDB 时可用的 mongo 工具使得查看集合及其包含的文档变得容易。

使用 MongoDB 数据库对于构建客户端-服务器应用程序和维护用户信息至关重要。

剩下的就是看看如何将用 Vue.js 开发的客户端与用 Node.js 开发的服务器端进行互连。我们将在下一章中看到这一点。我们将构建一个 100% 的 JavaScript 应用程序,以展示其简单和高效。

第九章:第九章:集成 Vue.js 与 Node.js

在本章中,我们将学习如何使用 Express(根据 MVC 模型)来结构化服务器代码并将 Vue.js 应用程序集成到 Node.js 服务器中,并使用 MongoDB 来存储信息。

对于这一点,我们将使用第五章中构建的列表管理应用程序的例子,即使用 Vue.js 管理列表。但在这里,我们将使用 Node.js 服务器,并将列表项存储在 MongoDB 数据库中。这将允许它们在必要时重新显示。

最后,我们将获得一个完全由 JavaScript(客户端和服务器端)制成的客户端-服务器应用程序。

本章涵盖以下主题:

  • 显示应用程序屏幕

  • 使用 Express 构建应用程序

  • MongoDB 数据库结构

  • 安装 Axios 库

  • 在列表中插入新元素

  • 显示列表元素

  • 修改列表中的元素

  • 从列表中删除元素

应用程序使用与第五章**使用 Vue.js 管理列表中已经使用的相同屏幕。我们将在下面重复它们,以便您更容易理解。

技术要求

您可以在 GitHub 上找到本章的代码文件:github.com/PacktPublishing/JavaScript-from-Frontend-to-Backend/blob/main/Chapter%209.zip

显示应用程序屏幕

在这里,我们可视化应用程序的屏幕,允许以下操作:

  • 显示已存在的列表(最初为空)

  • 在列表末尾插入新元素

  • 修改列表中的元素

  • 从列表中删除项目

    注意

    访问列表的 URL 是http://localhost:3000。这里使用的服务器是运行Express模块的 Node.js 服务器。使用的数据库是MongoDB

初始时,列表为空。页面上只存在添加元素按钮(见以下图示):

图 9.1 – 空项目列表

图 9.1 – 空项目列表

点击添加元素按钮多次会创建多个带有文本元素 X以及删除修改按钮的行(这里我们点击了添加元素按钮三次):

图 9.2 – 向列表中添加三个项目

图 9.2 – 向列表中添加三个项目

接下来,让我们修改第二个元素。一个输入字段出现在项目文本的位置。让我们在输入字段中输入New Element 2来替换显示在输入字段中的文本:

图 9.3 – 编辑列表中的第二个项目

图 9.3 – 编辑列表中的第二个项目

通过点击输入字段外的地方,输入字段消失,元素的文本被修改:

图 9.4 – 第二个列表项已更改

图 9.4 – 第二个列表项已更改

最后,让我们从列表中移除第一和第三项:

Figure 9.5 – 移除了第一和第三列表项

图 9.5 – 移除了第一和第三列表项

现在,当我们刷新之前的窗口时,我们看到列表重新显示了New Element 2,从而表明所做的修改确实已存储在数据库中。当我们用 Vue.js 仅在第五章中创建此应用时并非如此,即使用 Vue.js 管理列表,因为列表元素没有保存在数据库中:

Figure 9.6 – 新的列表显示:列表被保留

图 9.6 – 新的列表显示:列表被保留

要创建此应用,我们当然会使用我们在第五章中编写的 Vue.js 程序,即使用 Vue.js 管理列表。但必须对其进行修改,以便此应用能在带有 Express 模块的 Node.js 服务器上运行,并且显示的数据存储在 MongoDB 数据库中。

在此处,我们将指出之前在第五章中编写的<GlobalApp><Element>组件的文件,即使用 Vue.js 管理列表,以解释接下来对它们的修改。

下面是<GlobalApp>组件:

<GlobalApp>组件(global-app.js 文件)

import Element from "./element.js";
const GlobalApp = {
  data() {
    return {
      elements : []
    }
  },
  components : {
    Element:Element
  },
  template : `
    <button @click="add()">Add Element</button>
    <ul>
      <Element v-for="(element, index) in elements" 
       :key="index" 
        :text="element" :index="index"
        @remove="remove($event)" @modify="modify($event)"
      />
    </ul>
  `,
  methods : {
    add() {
      var element = "Element " + (this.elements.length + 1);
      this.elements.push(element);
    },
    remove(params) {
      var index = params.index;
      this.elements.splice(index, 1);
    },
    modify(params) {
      var index = params.index;
      var value = params.value;
      this.elements[index] = value;
    }
  }
}
export default GlobalApp;

下面是<Element>组件:

<Element>组件(element.js 文件)

const Element = {
  data() {
    return {
      input : false
    }
  },
  template : `
    <li> 
      <span v-if="!input"> {{text}} </span>
      <input v-else type="text" :value="text" 
       @blur="modify($event)" ref="refInput" />
      <button @click="remove()"> Remove </button> 
      <button @click="input=true"> Modify </button>
    </li>
  `,
  props : ["text", "index"],
  methods : {
    remove() {
      // process the click on the Remove button
      this.$emit("remove", { index : this.index });
    },
    modify(event) {
      var value = event.target.value;
      this.input = false;
      this.$emit("modify", { index : this.index, value : 
      value });
    }
  },
  emits : ["remove", "modify"],
  updated() {
    // check that refInput exists, and if so, give focus to 
    // the input field
    if (this.$refs.refInput) this.$refs.refInput.focus();  
  }
}
export default Element;

允许您包含<GlobalApp>组件的index.html文件如下:

index.html 文件

<html>
  <head>
    <meta charset="utf-8" />
    <script src="img/vue@next"></script>
    <style type="text/css">
      li {
        margin-top:10px;
      }
      ul button {
        margin-left:10px;
      }
    </style>
  </head>
  <body>
    <div id="app"></div>
  </body>
  <script type="module">
    import GlobalApp from "./global-app.js";
    var app = Vue.createApp({
      components : {
        GlobalApp:GlobalApp
      },
      template : "<GlobalApp />"
    });
    var vm = app.mount("div#app");
  </script>
</html>

要创建此应用,我们首先创建 Node.js 应用,该应用将托管用 Vue.js 编写的 JavaScript 代码。为此,使用express命令创建应用。应用将被命名为list(例如),因此我们需要输入express list命令来创建此应用,如以下章节所述。

使用 Express 构建应用

让我们先使用 Express 创建应用。为此,输入express list命令,这将创建名为list的应用。此应用将通过 URL http://localhost:3000 访问,如第七章中所述,即使用 Express 与 Node.js

在当前目录下输入express list命令:

Figure 9.7 – 使用 Express 创建应用列表

图 9.7 – 使用 Express 创建应用列表

通过输入指示的命令启动服务器,即:cd listnpm install,然后是npm start

通过在浏览器中输入 URL http://localhost:3000 启动应用。

我们展示了 Express 标准创建的基本应用(见图 9.8)。

如果在加载 Express 模块时发生错误,你可以输入npm link express命令以在应用程序中定位 Express 模块。如果加载 mongoose 模块时发生错误,你可以输入npm link mongoose命令。

如果一切顺利,你将获得以下结果:

图 9.8 – 使用 Express 创建的标准应用程序

图 9.8 – 使用 Express 创建的标准应用程序

目前的目标是可视化我们使用 Vue.js 创建的列表管理应用程序。它由三个文件组成:

  • 启动时需要查看的index.html文件

  • 描述应用程序主<GlobalApp>组件的global-app.js文件

  • 描述与显示元素行对应的<Element>组件的element.js文件

Express 应用程序的主目录(list目录)包括一个包含imagesjavascriptsstylesheets子目录的public子目录。

让我们将三个文件index.htmlglobal-app.jselement.js直接放入public目录下,位于根目录之下。

注意

修改public目录中的文件不需要重启服务器。另一方面,修改 Express 应用程序的app.js文件则需要使用npm start命令重启服务器。

让我们在浏览器中再次查看 URL http://localhost:3000。我们将在第五章使用 Vue.js 管理列表中构建的 Vue.js 应用程序现在将显示出来。按钮点击也将开始工作。

唯一的区别是,我们的 Vue.js 应用程序运行在 Node.js 服务器上,而不是像在第五章使用 Vue.js 管理列表中那样运行在其他应用程序服务器上。

图 9.9 – 在 Node.js 服务器上运行的应用程序

图 9.9 – 在 Node.js 服务器上运行的应用程序

然而,如果显示的页面被刷新,之前显示的列表将被删除,因为目前数据库中显示的信息没有持久化。

现在,我们将看到我们的应用程序如何与 Node.js 服务器和 MongoDB 数据库交互。

MongoDB 数据库结构

为了构建我们的应用程序,我们将在数据库服务器上执行数据读取和更新。例如,每次点击elements集合。实际上,elements集合中的每个文档都将代表屏幕上列表中显示的元素文本。

注意

要访问 MongoDB 数据库,你首先需要安装 mongoose 模块(见上一章),这允许你在 JavaScript 中操作数据库文档。

要执行此操作,请在 Express 应用程序的主目录(list目录)中输入npm install mongoose命令。

elements集合将用于在 MongoDB 中存储列表项。elements集合中的一个文档将包含与其text属性关联的文本。每个文档还将有一个_id属性,MongoDB 将为每个插入集合的文档分配一个唯一的值。

注意

数据库的结构使用listSchema模式描述,该模式将与用于创建elements集合文档的List模型相关联。

Express 的app.js文件被修改以包含这些定义:

将 List 模型添加到使用 MongoDB 的元素集合(app.js 文件)

var createError = require('http-errors');
var express = require('express');
var path = require('path');
var cookieParser = require('cookie-parser');
var logger = require('morgan');
var indexRouter = require('./routes/index');
var usersRouter = require('./routes/users');
var mongoose = require("mongoose");
mongoose.connect("mongodb://localhost/mydb_test"); // we 
// connect 
// to 
                                                   // mydb_test
var listSchema = mongoose.Schema({
 text : String     // text associated with the list item
});
// association of the List model with the elements collection
var List = mongoose.model("elements", listSchema);
var app = express();
// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'jade');
app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));
app.use('/', indexRouter);
app.use('/users', usersRouter);
// catch 404 and forward to error handler
app.use(function(req, res, next) {
  next(createError(404));
});
// error handler
app.use(function(err, req, res, next) {
  // set locals, only providing error in development
  res.locals.message = err.message;
  res.locals.error = req.app.get('env') === 'development' ? 
  err : {};
  // render the error page
  res.status(err.status || 500);
  res.render('error');
});
module.exports = app;

然后app.js文件将被丰富以定义将更新数据库的新路由。这些路由将通过使用app.use()方法(如在第七章章 7使用 Node.js 中的 Express)中解释的那样创建。以下各节将描述这些路由的创建。

注意

多亏了我们创建的List模型,我们将能够访问List.create()List.find()等方法,以操作 MongoDB 数据库的elements集合中的文档。

为了在客户端(这里指浏览器)和服务器(这里指 Node.js 服务器)之间创建交互,以便更新包含元素列表的数据库,我们在这里使用Axios JavaScript 库

安装 Axios 库

我们可以看到,目前我们可以操作在 HTML 页面上显示的列表项,但我们还不能在服务器上的数据库中更新它们。

为了做到这一点,Vue.js 程序必须能够与 Node.js 服务器通信。这可以通过使用 Axios(见github.com/axios/axios)这样的 JavaScript 库来实现。你所要做的就是将库包含在 HTML 页面中(这里将在index.html文件中),以便能够使用其功能。

注意

Axios 库是一个允许浏览器和服务器使用Ajax 技术进行通信的库。这项技术允许浏览器和服务器在保持同一 HTML 页面的情况下交换信息,这正是我们在这里想要的。这被称为单页应用(SPA)(当应用由单个 HTML 页面组成时)。

让我们在index.html文件中包含 Axios 库(使用<script>标签),并显示axios.VERSION变量的值,该变量包含库的版本号。这验证了 Axios 库是可以访问的:

包含 Axios 库并显示版本号(index.html 文件)

<html>
  <head>
    <meta charset="utf-8" />
    <script src="img/vue@next"></script>
    <script src="https://unpkg.com/axios/dist/
    axios.min.js"></script>
    <style type="text/css">
      li {
        margin-top:10px;
      }
      ul button {
        margin-left:10px;
      }
    </style>
  </head>
  <body>
    <div id="app"></div>
  </body>
  <script type="module">
   console.log("axios.VERSION = " + axios.VERSION); 
    // display Axios version number
    import GlobalApp from "./global-app.js";
    var app = Vue.createApp({
      components : {
        GlobalApp:GlobalApp
      },
      template : "<GlobalApp />"
    });
    var vm = app.mount("div#app");
  </script>
</html>

我们简单地在index.html文件中添加了 Axios 库(使用<script>标签)和显示 Axios 库版本号的指令,这允许我们在之后检查 Axios 库是否可访问。

让我们在浏览器中再次显示页面(使用 URL http://localhost:3000)。

我们在控制台中收到一条消息,指示使用的 Axios 版本号(见下图),从而表明我们有权访问 Axios 库的功能:

图 9.10 – 显示 Axios 版本号

图 9.10 – 显示 Axios 版本号

现在,让我们看看如何使用 Axios 与服务器交互并更新数据库文档。

目标当然是,当然,通过修改我们已编写的 Vue.js 代码以使用 Axios 库,从而与 Node.js 服务器进行通信,最大限度地利用我们已有的 Vue.js 代码。

因此,我们将修改以下文件(除了之前修改的 index.html 文件以包含 Axios 库之外):

  • 用于调用 Axios 库的 global-app.js 文件

  • 用于将 element.js 文件适配到数据库结构的文件

  • 用于执行数据库查询的 app.js 文件

我们已经看到了如何在程序中安装和使用 Axios。现在让我们看看如何使用它将元素插入到数据库中。

在列表中插入新元素

让我们看看如何通过点击添加元素按钮将新元素存储到列表中。

与此元素相关的文本必须以元素 X的形式传输到服务器。我们将在稍后看到如何在点击修改按钮后修改此文本。

<GlobalApp> 组件中定义的 add() 方法用于将新元素插入到显示的列表中。将需要添加使用 Axios 库的指令,以便也将此新元素插入到 MongoDB 的 elements 集合中。

在开始使用 Axios 之前,修改使用 Vue.js 编写的 JavaScript 程序是有用的。为此,我们将使用在创建 <Element> 组件时的新属性,用 element 属性替换 textindex 属性。

将文本和索引属性替换为元素属性

在创建元素时,我们目前使用元素的文本和索引,然后它们在 <Element> 组件中使用,以显示它(及其文本)或修改或删除它(使用其索引)。

在屏幕上显示的列表中通过索引识别元素在之前是有意义的,但如果我们想修改或删除数据库中的元素,这就不再适用了。这是因为 MongoDB 集合的文档不是通过它们的索引来识别的,而是通过它们的标识符 _id

我们不是在 <Element> 组件中传递 textindex 参数,而是通过只传递 element 参数来简化,该参数是一个 { text, _id } 对象。element.text 字段允许您检索要显示的文本,而 element._id 字段允许您访问元素的唯一标识符(例如索引,每个元素都是唯一的)。

我们修改 global-app.jselement.js 文件以考虑这一点。

这些文件以下将进行修改,但将再次修改以考虑与数据库的连接:

global-app.js 文件

import Element from "./element.js";
const GlobalApp = {
  data() {
    return {
      elements : []  // array of object { text, _id }
                     // (_id = document id in MongoDB)
    }
  },
  components : {
    Element:Element
  },
  template : `
    <button @click="add()">Add Element</button>
    <ul>
     <Element v-for="(element, index) in elements" 
       :key="index" :element="element"
        @remove="remove($event)" @modify="modify($event)"
      />
    </ul>
  `,
  methods : {
    add() {
      var text = "Element " + (this.elements.length + 1);
this.elements.push({text:text, 
      _id:this.elements.length});  
// to modify to retrieve the real 
                         // _id provided by MongoDB
    },
    remove(params) {
      var id = params.id;
    // remove the element with this id from the elements 
      // array
      this.elements = this.elements.filter(
      function(element) {
        if (element._id == id) return false;
        else return true;
      });
    },
    modify(params) {
      var id = params.id;
      var value = params.value;
    // modify the text of the element with this id in the 
      // elements array
      this.elements = this.elements.map(function(element) {
        if (element._id == id) {
          element.text = value;
          return element;
        }
        else return element;
      });
    }
  }
}
export default GlobalApp;

关于前面的代码,可以做出以下说明:

  • 现在的 elements 变量现在变成了一个 { text, _id } 对象的数组。为此,我们在 add() 方法中写入指令 this.elements.push({text:text, _id:this.elements.length}),通过将一个 {text, _id} 形式的对象插入到 elements 数组中来实现。

  • _id 属性的值在这里是临时的:实际上,你必须检索 MongoDB 在文档保存到数据库时提供的标识符。

  • 每个 <Element> 组件是通过传递一个表示 { text, _id } 对象的 element 属性来构建的(在模板中)。

  • remove() 方法必须从列表中删除具有传递的标识符的元素。为此,我们使用 JavaScript 的 filter() 方法来保留除具有此标识符的元素之外的所有元素。

  • 同样,modify() 方法必须修改具有此标识符的列表元素的值。我们使用 JavaScript 的 map() 方法来返回一个新元素数组,其中具有此标识符的元素值被修改。

element.js 文件变为以下内容:

element.js 文件

const Element = {
  data() {
    return {
      input : false
    }
  },
  template : `
    <li> 
      <span v-if="!input"> {{element.text}} </span>
<input v-else type="text" :value="element.text" 
@blur="modify($event)" 
                    ref="refInput" />
      <button @click="remove()"> Remove </button> 
      <button @click="input=true"> Modify </button>
    </li>
  `,
  props : ["element"],
  methods : {
    remove() {
      // process the click on the Remove button
      this.$emit("remove", { id : this.element._id });
    },
    modify(event) {
      var value = event.target.value;
      this.input = false;
      this.$emit("modify", { id : this.element._id, value : 
      value });
    }
  },
  emits : ["remove", "modify"],
  updated() {
    // check that refInput exists, and if so, give focus to 
    // the input field
    if (this.$refs.refInput) this.$refs.refInput.focus();  
  }
}
export default Element;

由于用于创建 <Element> 组件的属性名为 element,并且对应于一个 { text, _id } 对象,我们使用 element.textelement._id 来显示文本并使用元素的标识符(而不是索引)。

你可以检查程序仍然可以工作,即使尚未建立与服务器建立数据库插入的连接。

注意

我们已经修改了 Vue.js 程序的代码,以便适应 MongoDB 数据库的使用。

现在我们来解释 Axios 库如何允许客户端和服务器相互通信,以便更新 MongoDB 数据库。

Axios 库在客户端和服务器之间通信的描述

现在我们使用 Axios 将元素插入到数据库中。

Axios 提供了四种主要方法用于在浏览器和服务器之间进行通信,使用 JavaScript 语言。我们在这里使用的是 Node.js 服务器,但 Axios 允许你与任何类型的服务器交互。这四种方法是与可以执行的 HTTP 请求类型相关的:GETPOSTPUTDELETE

  • axios.get(url, options): 这允许你执行一个 GET 类型的请求。

  • axios.post(url, options): 这允许你执行一个 POST 类型的请求。

  • axios.put(url, options): 这允许你执行一个 PUT 类型的请求。

  • axios.delete(url, options): 这允许你执行一个 DELETE 类型的请求。

options 参数允许你指定将允许服务器执行其处理的附加参数。例如,在我们的应用程序中,我们将在此参数中指示我们想要存储在数据库中的列表元素文本。

注意

所有这些方法都返回一个 Promise 对象,这然后允许你使用 then(callback) 方法继续操作。callback(response) 函数用于在请求发出后检索和分析服务器的响应。

第二章 的末尾研究了 Promise 对象,探索 JavaScript 的高级概念

在每种情况下,我们都需要处理客户端发送的 Axios 请求(在 <GlobalApp> 组件关联的 global-app.js 文件中),然后在服务器端(在接收 Axios 发起的查询的 app.js 文件中)考虑它。

现在让我们看看 POST 请求如何允许我们将元素插入到数据库中。

使用 Axios 和 POST 类型请求(客户端)

现在让我们看看如何使用 axios.post() 方法在列表创建新元素后将其插入到 elements 集合中。

注意

我们在这里使用 POST 请求来插入项目,但其他类型的请求也会按相同方式工作。然而,使用 POST 请求在这里是有意义的,因为它遵循使用 REpresentational State Transfer (REST) 请求的官方建议。

虽然每个文件只添加了几行,但每次都会在下面显示完整的代码,这样你可以看到更改的位置:

在数据库中添加新元素(客户端 global-app.js 文件)

import Element from "./element.js";
const GlobalApp = {
  data() {
    return {
      elements : []  // array of object { text, _id }
                     // (_id = document id in MongoDB)
    }
  },
  components : {
    Element:Element
  },
  template : `
    <button @click="add()">Add Element</button>
    <ul>
      <Element v-for="(element, index) in elements" 
       :key="index" :element="element"
        @remove="remove($event)" @modify="modify($event)"
      />
    </ul>
  `,
  methods : {
    add() {
      var text = "Element " + (this.elements.length + 1);
      axios.post("/list", {text:text})     // pass object 
// {text:text} to 
                                           // server
      .then((response) => {
this.elements.push({text:text, 
        _id:response.data.id});
      });
    },
    remove(params) {
      var id = params.id;
      // remove the element with this id from the elements 
      // array
      this.elements = this.elements.filter(
      function(element) {
        if (element._id == id) return false;
        else return true;
      });
    },
    modify(params) {
      var id = params.id;
      var value = params.value;
      // modify the text of the element with this id in the 
      // elements array
      this.elements = this.elements.map(function(element) {
        if (element._id == id) {
          element.text = value;
          return element;
        }
        else return element;
      });
    }
  }
}
export default GlobalApp;

axios.post("/list", {text:text}) 方法激活服务器上的 /list URL,使用 POST 类型的请求。text 参数传递给服务器,以便将其存储在 elements 集合中。

作为对服务器调用的回报,服务器返回一个包含在 data.id 中的文档标识符的 response 对象。然后,该标识符和元素文本存储在 elements 数组中。因为 elements 数组是 Vue.js 的响应式变量,其更新会导致列表在浏览器中重新显示。

注意

注意在 then(callback) 方法中编写的回调函数。我们使用 => 形式(即不使用 function 关键字)来保留回调函数中的 this 值。如果你使用 function 关键字,则 this 的值将是 undefined,你将无法通过 this.elements 访问 elements 变量,这会导致错误。

POST 请求是由客户端(浏览器)发出的,因此现在必须由服务器处理,以便将新元素插入到集合中。让我们研究如何进行。

POST 类型请求处理(服务器端)

现在让我们看看服务器如何处理接收到的POST请求。它必须在数据库的elements集合中创建一个新的文档。服务器的app.js文件被修改以考虑/list URL 上的POST请求:

在数据库中添加新元素(服务器端 app.js 文件)

var createError = require('http-errors');
var express = require('express');
var path = require('path');
var cookieParser = require('cookie-parser');
var logger = require('morgan');
var indexRouter = require('./routes/index');
var usersRouter = require('./routes/users');
var mongoose = require("mongoose");
mongoose.connect("mongodb://localhost/mydb_test");
var listSchema = mongoose.Schema({
 text : String
});
var List = mongoose.model("elements", listSchema);
var app = express();
// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'jade');
app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));
app.use('/', indexRouter);
app.use('/users', usersRouter);
// creating a new element in the list
app.post("/list", function(req, res) {
  var text = req.body.text;
  List.create({text:text}, function(err, doc) {
res.json({id:doc._id});  // send the MongoDB identifier 
                             // in the response
  });
});
// catch 404 and forward to error handler
app.use(function(req, res, next) {
  next(createError(404));
});
// error handler
app.use(function(err, req, res, next) {
  // set locals, only providing error in development
  res.locals.message = err.message;
  res.locals.error = req.app.get('env') === 'development' ? 
  err : {};
  // render the error page
  res.status(err.status || 500);
  res.render('error');
});
module.exports = app;

使用app.post("/list", callback)方法接收并处理插入新元素到elements集合的请求。

Axios text参数中发送的文本在服务器上接收在req.body.text变量中。通过List.create()类方法更新elements集合,我们传递text参数。在create()方法相关的回调函数中,我们检索创建的文档的标识符doc._id

我们将此标识符作为 JSON 对象{ id : doc._id }返回给浏览器。我们使用res.json()方法进行此操作。此服务器返回在调用先前看到的axios.post()方法(global-app.js文件)时的then(callback)方法中处理。

如果你运行前面的程序,你会看到包含元素 X的行在页面上依次插入。但没有任何说明表明数据库已被更新。让我们使用 MongoDB 中可用的工具来验证正确的插入操作。

验证数据库中插入操作的正确性

要验证数据库中的插入操作,只需使用mongo实用程序,然后输入命令db.elements.find()以查看显示的插入文档(假设我们已使用命令db=connect("mydb_test")连接了mydb_test数据库)。

假设已插入三个列表项,我们得到以下结果:

图 9.11 – 使用 mongo 实用程序查看元素集合的内容

图 9.11 – 使用 mongo 实用程序查看元素集合的内容

下一步是检索数据库中存储的信息以显示列表项。列表应在页面首次显示时查看,并在插入、修改或删除操作时更新。

显示列表元素

在本节中,我们处理页面的首次显示。插入操作已在前面介绍,修改和删除操作将在以下章节中介绍。

注意

要在应用程序启动时显示列表,你必须使用组件的created()方法或mounted()方法,这些方法在 Vue.js 组件创建时被调用。

要检索元素列表,我们将使用 HTTP GET请求。

使用 Axios 进行 GET 类型请求(客户端)

这里,我们将向服务器发送一个带有/list URL 的GET类型请求。使用axios.get("/list")指令执行此请求。我们可以在created()mounted()方法中使用此指令。这里,我们选择在created()方法中使用它:

获取项目列表,客户端(global-app.js 文件)

import Element from "./element.js";
const GlobalApp = {
  data() {
    return {
      elements : []  // array of object { text, _id }
                     // (_id = document id in MongoDB)
    }
  },
  components : {
    Element:Element
  },
  template : `
    <button @click="add()">Add Element</button>
    <ul>
      <Element v-for="(element, index) in elements" 
       :key="index" :element="element"
        @remove="remove($event)" @modify="modify($event)"
      />
    </ul>
  `,
  methods : {
    add() {
      var text = "Element " + (this.elements.length + 1);
      axios.post("/list", {text:text})
      .then((response) => {
        console.log(this.elements);
        this.elements.push({text:text, 
        _id:response.data.id});
      });
    },
    remove(params) {
      var id = params.id;
      // remove the element with this id from the elements 
      // array
      this.elements = this.elements.filter(
      function(element) {
        if (element._id == id) return false;
        else return true;
      });
    },
    modify(params) {
      var id = params.id;
      var value = params.value;
      // modify the text of the element with this id in the 
      // elements array
      this.elements = this.elements.map(function(element) {
        if (element._id == id) {
          element.text = value;
          return element;
        }
        else return element;
      });
    }
  },
  created() {
    axios.get("/list")
    .then((response) => {
      this.elements = response.data.elements.map(
       function(element) {
         return {_id : element._id, text : element.text }
      });
    });
  }
}
export default GlobalApp;

axios.get("/list") 方法向服务器发送请求,然后在 then(callback) 方法中处理接收到的响应。和之前一样,接收到的 response 对象包含 data 属性,其中包含服务器返回的数据(elements 字段 – 见下文)。

由于服务器发送了 elements 集合的所有文档字段,我们通过 map() 方法过滤接收到的列表,以仅保留 _idtext 字段(我们因此删除了与版本号相关的 __v 字段,这在这里是不必要的)。

现在我们来看看如何在 Node.js 服务器端处理 GET 请求。

GET 类型请求处理(服务器端)

GET /list 请求通过在 app.js 文件中定义的 app.get("/list") 方法被 Node.js 服务器接收。处理将包括读取 elements 集合的内容,并以 JSON 形式将其返回到浏览器的 elements 属性中。返回集合中的每个项目都有 _idtext__v(文档的版本号)字段:

获取项目列表,服务器端(app.js 文件)

var createError = require('http-errors');
var express = require('express');
var path = require('path');
var cookieParser = require('cookie-parser');
var logger = require('morgan');
var indexRouter = require('./routes/index');
var usersRouter = require('./routes/users');
var mongoose = require("mongoose");
mongoose.connect("mongodb://localhost/mydb_test");
var listSchema = mongoose.Schema({
 text : String
});
var List = mongoose.model("elements", listSchema);
var app = express();
// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'jade');
app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));
app.use('/', indexRouter);
app.use('/users', usersRouter);
// creating a new element in the list
app.post("/list", function(req, res) {
  var text = req.body.text;
  console.log(text);
  List.create({text:text}, function(err, doc) {
    res.json({id:doc._id});
  });
});
// retrieving list of elements
app.get("/list", function(req, res) {
  List.find(function(err, elements) {
    res.json({elements:elements});
  });
});
// catch 404 and forward to error handler
app.use(function(req, res, next) {
  next(createError(404));
});
// error handler
app.use(function(err, req, res, next) {
  // set locals, only providing error in development
  res.locals.message = err.message;
  res.locals.error = req.app.get('env') === 'development' ? 
  err : {};
  // render the error page
  res.status(err.status || 500);
  res.render('error');
});
module.exports = app;

使用 List.find() 类方法读取 elements 集合。我们向浏览器返回 { elements : elements } 对象,我们之前已经看到了它的使用。

每次启动应用程序时都会显示项目列表。只需使用 npm start 重新启动服务器,然后重新显示页面 URL,http://localhost:3000

![图 9.12 – 应用程序启动时显示元素列表

![图 9.12 – 应用程序启动时显示元素列表

图 9.12 – 应用程序启动时显示元素列表

我们已经看到了如何插入元素和检索元素列表。接下来,让我们看看如何修改列表中的元素。

修改列表中的元素

在这里,我们展示了如何修改列表中的元素,并将此修改保存在数据库中。将使用 PUT 类型请求来完成此操作。

使用 Axios 进行 PUT 类型请求(客户端)

使用 axios.put("/list", options) 方法执行对服务器的 PUT 类型请求。我们在 options 参数中向服务器传输修改元素的新的文本及其在数据库中的标识符。标识符和新的文本将允许在服务器上更新项目:

修改元素,客户端(global-app.js 文件)

import Element from "./element.js";
const GlobalApp = {
  data() {
    return {
      elements : []  // array of object { text, _id } 
                     // (_id = document id in MongoDB) 
    }
  },
  components : {
    Element:Element
  },
  template : `
    <button @click="add()">Add Element</button>
    <ul>
      <Element v-for="(element, index) in elements" 
      :key="index" :element="element"
        @remove="remove($event)" @modify="modify($event)"
      />
    </ul>
  `,
  methods : {
    add() {
      var text = "Element " + (this.elements.length + 1);
      axios.post("/list", {text:text})
      .then((response) => {
        console.log(this.elements);
        this.elements.push({text:text, 
        _id:response.data.id});
      });
    },
    remove(params) {
      var id = params.id;
      // remove the element with this id from the elements 
      // array
      this.elements = this.elements.filter(
      function(element) {
        if (element._id == id) return false;
        else return true;
      });
    },
    modify(params) {
      var id = params.id;
      var value = params.value;
      // modify the text of the element with this id in the 
      // elements array
      this.elements = this.elements.map(function(element) {
        if (element._id == id) {
          element.text = value;
          return element;
        }
        else return element;
      });
// modify the text of the element having this 
      // identifier
      axios.put("/list", {text:value, id:id});       
    }
  },
  created() {
    axios.get("/list")
    .then((response) => {
      this.elements = response.data.elements.map(
      function(element) {
        return {_id : element._id, text : element.text }
      });
    });
  }
}
export default GlobalApp;

这里不应该使用 then(callback) 方法,因为服务器不会向浏览器返回任何信息。

现在我们来看看服务器端 PUT 请求的管理。

PUT 类型请求处理(服务器端)

服务器在 app.js 文件中处理 PUT /list 请求。处理包括对具有此标识符的集合文档进行更新,使用从浏览器接收到的文本:

修改元素,服务器端(app.js 文件)

var createError = require('http-errors');
var express = require('express');
var path = require('path');
var cookieParser = require('cookie-parser');
var logger = require('morgan');
var indexRouter = require('./routes/index');
var usersRouter = require('./routes/users');
var mongoose = require("mongoose");
mongoose.connect("mongodb://localhost/mydb_test");
var listSchema = mongoose.Schema({
 text : String
});
var List = mongoose.model("elements", listSchema);
var app = express();
// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'jade');
app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));
app.use('/', indexRouter);
app.use('/users', usersRouter);
// creating a new element in the list
app.post("/list", function(req, res) {
  var text = req.body.text;
  console.log(text);
  List.create({text:text}, function(err, doc) {
    res.json({id:doc._id});
  });
});
// retrieving list of elements
app.get("/list", function(req, res) {
  List.find(function(err, elements) {
    res.json({elements:elements});
  });
});
// modifying an element in the list
app.put("/list", function(req, res) {
  var id = req.body.id;
  var text = req.body.text;
  List.updateOne({_id:id}, {text:text}).exec();
  // don't forget exec()!
  res.send();  // close the connection to the browser
});
// catch 404 and forward to error handler
app.use(function(req, res, next) {
  next(createError(404));
});
// error handler
app.use(function(err, req, res, next) {
  // set locals, only providing error in development
  res.locals.message = err.message;
  res.locals.error = req.app.get('env') === 'development' ? 
  err : {};
  // render the error page
  res.status(err.status || 500);
  res.render('error');
});
module.exports = app;

文本和标识符从服务器的 req.body.textreq.body.id 变量中检索。具有此标识符的文档在数据库中使用新文本进行更新。List.updateOne() 类方法允许修改此文档,但由于它之后没有使用回调函数,因此必须使用 exec() 方法才能在数据库中执行更新。

注意处理结束时的 res.send() 指令。它关闭了浏览器与服务器之间的连接。如果连接没有关闭,浏览器将等待服务器的响应,如果服务器没有向浏览器发送任何内容,则服务器将永远不会返回响应。

让我们通过解释如何从列表中删除一个项目来结束。

从列表中删除一个元素

最后,我们将学习如何从列表中删除一个元素。为此将使用 DELETE 类型请求。

使用 Axios 进行 DELETE 类型请求(客户端)

axios.delete("/list", options) 方法用于在服务器上触发 DELETE 类型请求。options 参数必须指示要从集合中删除的元素的标识符。

然而,与之前的 axios.get()axios.put()axios.post() 调用不同,axios.delete("/list", options) 调用要求将 options 参数写入 data 属性(因此写作 { data : options })。如果您不遵循此约定,它将不会工作。

下面是使用 Axios 库执行 DELETE 请求的说明:

在客户端删除一个元素(global-app.js 文件)

import Element from "./element.js";
const GlobalApp = {
  data() {
    return {
      elements : []  // array of object { text, _id } 
                     // (_id = document id in MongoDB)
    }
  },
  components : {
    Element:Element
  },
  template : `
    <button @click="add()">Add Element</button>
    <ul>
      <Element v-for="(element, index) in elements" 
      :key="index" :element="element"
        @remove="remove($event)" @modify="modify($event)"
      />
    </ul>
  `,
  methods : {
    add() {
      var text = "Element " + (this.elements.length + 1);
      axios.post("/list", {text:text})
      .then((response) => {
        console.log(this.elements);
        this.elements.push({text:text, 
        _id:response.data.id});
      });
    },
    remove(params) {
      var id = params.id;
      // remove the element with this id from the elements 
      // array
      this.elements = this.elements.filter(
      function(element) {
        if (element._id == id) return false;
        else return true;
      });
      axios.delete("/list", { data : {id:id} });    
// the options must be written in the data 
            // property
    },
    modify(params) {
      var id = params.id;
      var value = params.value;
      // modify the text of the element with this id in the 
      // elements array
      this.elements = this.elements.map(function(element) {
        if (element._id == id) {
          element.text = value;
          return element;
        }
        else return element;
      });
      axios.put("/list", {text:value, id:id});   
            // modify the text of the element having this 
            // identifier
    }
  },
  created() {
    axios.get("/list")
    .then((response) => {
      this.elements = response.data.elements.map(
      function(element) {
        return {_id : element._id, text : element.text }
      });
    });
  }
}
export default GlobalApp;

如前所述,我们使用 axios.delete(/list", options) 方法中的 options 参数的格式 { data : options },以确保选项通过 DELETE 方法正确传递。

现在我们来检查服务器在接收到 DELETE 请求时执行的处理。

DELETE 类型请求处理(服务器端)

服务器使用 app.delete("/list, callback) 方法接收 DELETE /list 请求。回调函数使用请求中传递的标识符从 elements 集合中删除相应的文档:

在服务器端删除一个元素(app.js 文件)

var createError = require('http-errors');
var express = require('express');
var path = require('path');
var cookieParser = require('cookie-parser');
var logger = require('morgan');
var indexRouter = require('./routes/index');
var usersRouter = require('./routes/users');
var mongoose = require("mongoose");
mongoose.connect("mongodb://localhost/mydb_test");
var listSchema = mongoose.Schema({
 text : String
});
var List = mongoose.model("elements", listSchema);
var app = express();
// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'jade');
app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));
app.use('/', indexRouter);
app.use('/users', usersRouter);
// creating a new element in the list
app.post("/list", function(req, res) {
  var text = req.body.text;
  console.log(text);
  List.create({text:text}, function(err, doc) {
    res.json({id:doc._id});
  });
});
// retrieving list of elements
app.get("/list", function(req, res) {
  List.find(function(err, elements) {
    res.json({elements:elements});
  });
});
// modifying an element in the list
app.put("/list", function(req, res) {
  var id = req.body.id;
  var text = req.body.text;
  List.updateOne({_id:id}, {text:text}).exec();
res.send();  // close the connection to the browser
});
// remove an element from the list
app.delete("/list", function(req, res) {
  var id = req.body.id;
  console.log(req.body.id);
  List.deleteOne({_id:id}).exec();   // don't forget exec()!
  res.send();  // close the connection to the browser
});
// catch 404 and forward to error handler
app.use(function(req, res, next) {
  next(createError(404));
});
// error handler
app.use(function(err, req, res, next) {
  // set locals, only providing error in development
  res.locals.message = err.message;
  res.locals.error = req.app.get('env') === 'development' ? 
  err : {};
  // render the error page
  res.status(err.status || 500);
  res.render('error');
});
module.exports = app;

使用 List.deleteOne({_id:id}) 方法从集合中删除具有此标识符的文档。由于我们在 List.deleteOne() 方法中未使用回调函数,因此我们调用 exec() 方法以在数据库中执行删除。

此外,注意处理结束时的 res.send() 指令。它关闭了浏览器与服务器之间的连接。如果连接没有关闭,浏览器将等待服务器的响应,如果服务器没有向浏览器发送任何内容,则服务器将永远不会返回响应。在这种情况下,您会通过在列表中多次点击 删除 按钮并重新加载列表看到意外的结果。

我们已经看到了如何使用 MongoDB 通过像 Axios 这样的库在列表中插入、修改和删除元素,这个库允许浏览器中的 JavaScript 代码与为服务器编写的 JavaScript 代码进行通信。现在,这把我们带到了本章和本书的结尾。

摘要

通过这个完整的示例,我们看到了如何在客户端(这里,使用 Vue.js)和服务器端(使用 Node.js 和 MongoDB)使用 JavaScript。

使用单一语言进行所有开发简化了学习,并确保整个应用程序中的一致性极高。

此外,像 Vue.js 这样的工具,允许创建可重用组件,以及基于 MVC 模型的 Express 和 mongoose 等模块,使得在客户端和服务器端正确架构 JavaScript 代码成为可能。

我们还看到了 Axios 库如何使得客户端和服务器之间的通信成为可能。

您现在拥有创建可靠、健壮和结构良好的客户端和服务器应用程序所需的一切,全部使用 JavaScript。

感谢

感谢您,亲爱的读者,购买并阅读这本书。它是为了帮助和指导您而写的。我们希望它对您有很大的帮助。

如果是这样的话,我们请求您做出一个非常小但极其重要的贡献——通过您所能利用的方式让我们的书为他人所知,希望它能够继续帮助像您这样的人。非常感谢!

posted @ 2025-09-29 10:35  绝不原创的飞龙  阅读(5)  评论(0)    收藏  举报