六、JavaScript 生态系统

学习目标

在本章结束时,你将能够做到以下几点:

  • 比较不同的 JavaScript 生态系统
  • 解释服务器端 JavaScript 的基本概念
  • 构建一个 Node.js 和 Express 服务器
  • 建立一个 React 前端网站
  • 将前端框架与后端服务器相结合

最后一章详细介绍了 JavaScript 生态系统,并教学生如何使用 Node.js 的不同特性和部分,以及 Node 包管理器(NPM)。

简介

第 5 章,函数式编程中,我们介绍了函数式编程范例。 我们讨论了面向对象编程和函数式编程,讨论了两者之间的区别,并概述了为什么我们应该使用函数式编程。 在第二部分中,我们讨论了函数式编程的关键概念,并演示了如何将它们应用于 JavaScript 代码。

JavaScript 生态系统在过去 10 多年里得到了巨大的发展。 JavaScript 不再是一种用于在基本 HTML 网页上添加动画等功能的编程语言。 JavaScript 现在可以用来构建完整的后端 web 服务器和服务、命令行界面、移动应用和前端网站。 在本章中,我们将介绍 JavaScript 生态系统,讨论使用 Node.js 用 JavaScript 构建 web 服务器,并讨论使用 React 框架使用 JavaScript 构建网站。

JavaScript 生态系统

有四个主要类别的 JavaScript 的生态系统,我们将讨论:前端,命令行界面,移动和【显示】后台。

  • 前端 JavaScript 用于面向用户的网站。
  • 命令行界面(CLI) JavaScript 用于构建命令行任务,以帮助开发人员。
  • 移动开发 JavaScript 用于构建手机应用。
  • 后端 JavaScript 用于构建 web 服务器和服务。

作为一种最初是为了在浏览器中嵌入简单应用而创建的语言,JavaScript 已经取得了长足的进步。

前端 JavaScript

前端 JavaScript用于创建复杂、动态的面向用户的网站。 像 Facebook、谷歌 Maps、Spotify 和 YouTube 等网站都严重依赖 JavaScript。 在前端开发中,使用 JavaScript 来操作 DOM 和处理事件。 通过将每个浏览器的 DOM 操作 API 包装成一个标准化的 API,已经创建了许多 JavaScript 库(如 jQuery),以提高 JavaScript DOM 操作的效率和易用性。 最流行的 DOM 操作库是 jQuery,在第 3 章,DOM 操作和事件处理中对此进行了讨论。 JavaScript 框架也被创建来更无缝地将 DOM 操作和事件与 HTML 设计方面集成在一起。 两个最常见的前端开发 JavaScript 框架是AngularJSReact。 AngularJS 是由谷歌构建并维护的,React 是由 Facebook 构建并维护的。

Facebook 和谷歌管理各自框架的 bug 修复和版本发布。 React 将在本章后面的章节中进行更详细的讨论。

命令行界面

命令行集成(CLI) JavaScript 中的 CLI 程序通常用于诸如检查代码、启动服务器、构建版本、编译代码、最小化文件以及安装开发依赖项和包等任务。 JavaScript CLI 程序通常是用 Node.js 编写的。 Node.js 是一个跨平台的环境,允许开发人员在浏览器之外执行 JavaScript 代码。 Node.js 将在本章后面的章节中进行更详细的讨论。 许多开发人员在日常开发中依赖 CLI 实用工具。

移动开发

使用 JavaScript 的移动开发正迅速成为主流。 自智能手机兴起以来,移动开发者已经成为热门商品。 虽然 JavaScript 无法在大多数移动操作系统上运行,但已有框架允许 JavaScript 和 HTML 内置到 Android 和 IOS 手机应用中。 最常见的 JavaScript 移动开发框架有 Ionic、React Native 和 Cordova/PhoneGap。 这些框架都允许您编写 JavaScript 来构建应用的框架和逻辑,然后将 JavaScript 编译为本地移动操作系统代码。 移动开发框架非常强大,因为它们允许我们用首选的 JavaScript 构建完整的移动应用。

后台开发

使用 JavaScript 的后端开发通常使用 Node.js 完成。 Node.js 可以用来构建强大的 web 服务器和服务。 如本主题前面所述,本章后面的章节将更详细地讨论 Node.js 及其用于后端服务器开发的应用。

JavaScript 生态系统非常广泛。 几乎可以用 JavaScript 编写任何类型的程序。 尽管有许多框架和现代 JavaScript 的功能,但重要的是要记住,框架并不能取代对核心 JavaScript 的良好理解。 框架很好地包装了核心 JavaScript,使我们能够执行强大的任务,比如从它构建移动和桌面应用,但如果 JavaScript 的核心原则和异步编程不能很好地理解,我们的应用可能会出现缺陷。

Node.js

Node.js(简称 Node)由 Ryan Dahl 于 2009 年开发,是目前最流行的浏览器外 JavaScript 引擎。 Node 是一个开源、跨平台的 JavaScript 运行时环境,基于 Chrome 的 V8 JavaScript 引擎。 它用于在浏览器外运行非客户端应用的 JavaScript 代码。

和谷歌在 Chrome 中的 V8 JavaScript 引擎一样,Node.js 使用单线程、事件驱动的异步架构。 它允许开发人员使用 JavaScript 的事件驱动编程风格来构建 web 服务器、服务和 CLI 工具。 正如在第二章,异步 JavaScript中所讨论的,JavaScript 是一种非阻塞和事件驱动的编程语言。 JavaScript 的异步特性(单线程事件循环),以及 Node 的轻量级设计,使我们可以构建非常可伸缩的网络应用,而无需担心线程。

请注意

正如在第 2 章,异步 JavaScript中讨论的,JavaScript 是单线程的。 在单个线程上运行的同步代码正在阻塞。 cpu 密集型操作将阻塞事件,如 I/O 文件系统操作和网络操作。

设置 Node.js

Node.js 可从 Node.js 网站下载,网址为https://nodejs.org/en/。 有两个版本可供下载:Long - Term Support (LTS)版本和当前版本。 我们建议您下载 LTS 版本。 当前版本具有可用的最新特性,可能不是完全没有 bug。 请务必按照操作系统的具体说明安装 Node。 可以下载所有三个主要操作系统的安装文件,Node.js 可以与许多包管理器一起安装。 js 的安装调试超出了本书的范围。 但是,可以通过谷歌搜索轻松找到安装技巧和调试技巧。

请注意

Node.js 的下载链接如下:https://nodejs.org/en/download/

下载并安装 Node.js 后,就可以在终端上使用node命令运行它。 不带参数地执行该命令将运行 Node.js 终端。 JavaScript 代码可以直接输入到终端,就像浏览器的调试控制台一样。 重要的是要注意,终端实例之间没有状态转移。 当运行 Node.js 命令行的终端实例关闭时,所有的计算将停止,Node.js 命令行进程使用的所有内存将被释放回 OS。 要使用 Node.js 运行 JavaScript 代码文件,只需在node命令后直接添加文件路径。 例如,以下命令将运行位于./path/to/file位置的文件,文件名为my_file.js:node ./path/to/file/my_file.js

节点包管理器

Node.js 是一个开源平台。 Node 最大的资产之一是可用的开源第三方库,称为模块。 Node 使用Node Package Manager (NPM)处理应用使用的第三方模块的安装和管理。 NPM 通常是用 Node.js 安装的。 要测试在安装 Node 时是否正确安装了 NPM,请打开终端窗口并运行npm -v命令。 如果正确安装了 NPM,将在终端中打印当前版本的 NPM。 如果 NPM 没有和 Node 一起安装,你可能需要重新运行 Node.js 安装程序。

请注意

本节未涉及的所有功能的 NPM 文档可以在https://docs.npmjs.com/上找到。

第一章,介绍 ECMAScript 6中,我们学习了 ES6 模块。 区分 ES6 模块和 Node.js 模块是非常重要的。 Node.js 模块创建于 ES6 和普通 JavaScript 对模块的支持之前。 虽然 Node.js 模块和 ES6 模块的用途相同,但它们并不遵循相同的技术规范。 Node.js 模块和 ES6 模块的加载、解析和构建方式是不同的。 Node.js 模块同步地从磁盘加载,同步地解析,同步地构建。 在加载模块之前,不能运行其他代码。 不幸的是,ES6 模块的加载方式不同。 它们是从磁盘异步加载的。 两种不同的模块加载方法是不兼容的。 在写这本书的时候,Node.js 对 ES6 模块的支持还处于测试阶段,默认情况下没有启用。 可以启用对 ES6 模块的支持,但我们建议您使用标准节点模块,直到完全支持 ES6 模块发布。

NPM 包是通过命令行npm install命令安装的。 您可以使用此命令向项目添加特定的包,或者安装所有缺失的依赖项。 如果安装命令没有指定参数,npm将在当前目录中查找package.json文件。 在package.json文件中,有一个dependencies字段,该字段包含为 Node.js 项目安装的所有依赖项。 NPM 将运行依赖项列表,并检查列表中指定的每个包是否已安装。 packages.json中的依赖项列表看起来类似于下面的代码片段:

"dependencies": {
 "amqplib": "^0.5.2",
 "body-parser": "^1.18.3",
 "cookie-parser": "^1.4.3",
 "express": "^4.16.3",
 "uuid": "^3.3.2"
}
代码片段 6.1:package.json 中的依赖项列表

package.json中的 dependencies 字段列出了该项目已经安装的 NPM 模块,以及版本号。 在这个片段中,我们有amqplib模块安装在0.5.2或更高版本,body-parser模块安装在1.18.3或更高版本,以及其他几个。 NPM 模块遵循语义版本控制。 版本号由三个用句点分隔的数字表示。 第一个数字是主要版本。 主版本号的增加表示破坏向后兼容性的主要更改。 第二个数字是次要版本。 对小版本号的更改表明新特性的发布不会破坏向后兼容性。 最后的数字是补丁号。 增加补丁号表示 bug 修复或功能的小更新。 补丁数量的增加并不包括新的功能,也不会破坏向后兼容性。

请注意

更多关于语义版本控制的信息可以在https://www.npmjs.com/上找到。

如果要安装模块,可以在install(例如,npm install express)之后添加参数npm install命令。 参数可以是包名、Git 存储库、tarball或者文件夹。 如果参数是一个包,NPM 将搜索它的注册包列表,并安装与该名称匹配的包。 如果参数是 Git 存储库,NPM 将尝试从 Git 存储库下载并安装文件。 如果没有提供适当的访问凭据,安装可能会失败。

请注意

请参阅 NPM 文档,了解如何从私有的 git 存储库安装包。

如果参数是一个 tarball, NPM 将解包这个 tarball 并安装文件。 可以通过指向 tarball 的 URL 或通过本地文件安装 tarball。 最后,如果指定的参数是本地机器上的一个文件夹,NPM 将尝试从指定的文件夹安装一个 NPM 包。

在使用 NPM 安装包时,考虑如何安装包是很重要的。 默认情况下,包安装在本地项目范围内,而不是保存为项目依赖项。 如果你正在安装一个 NPM 包,你想保存在package.json作为项目依赖项,你必须在安装命令的包名后面包含--save-s参数(例如,npm install express -s)。 这个参数告诉 NPM 将依赖项保存在package.json中,以便在以后使用npm install命令安装它。

NPM 包可以安装在两个作用域:全局作用域局部作用域。 安装在本地作用域中的包只能在安装它们的 Node.js 项目中使用。 安装在全局作用域中的包或全局包可以被任何 Node.js 项目使用。 默认情况下,包是在本地安装的。 如果需要全局安装,可以在包名(例如:npm install express -g)后的npm install命令中添加-g--global标志。

安装包的位置并不总是显而易见的,但如果不确定,可以遵循以下一般经验法则。 如果你打算在一个带有require()功能的项目中使用该包,请在本地安装该包。 如果您计划在命令行上使用该包,请全局安装该包。 如果您仍然不能决定并需要在项目和命令行中使用这个包,那么您可以简单地将它安装在两个位置。

加载和创建模块

Node.js 使用CommonJS样式的模块规范作为加载和使用模块的标准。 CommonJS 是一个旨在为浏览器之外的 JavaScript 指定一个 JavaScript 生态系统的项目。 CommonJS 为 Node.js 采用的模块定义了一个规范。 模块允许开发人员封装功能,并且只向其他 JavaScript 文件公开所封装功能的所需部分。

在 Node.js 中,我们使用 require 函数将模块导入到代码中(require('module'))。 require函数可以加载任何有效的 JavaScript 文件,NPM 模块或 JSON 文件。 我们将使用require函数来加载为我们的项目安装的任何 NPM 包。 要加载一个模块,只需将模块名作为参数传入require函数,并将返回的对象保存到一个变量中。 例如,我们可以用以下代码加载 NPM 模块body-parser:const bodyParser = require( 'body-parser' ); 这将把导出的函数和变量加载到bodyParser对象中。 require 函数还可以用于加载 JavaScript 文件和 JSON 文件。 要加载这些文件中的一个,只需将文件路径传递给require函数,而不是模块名。 如果没有提供文件扩展名,Node.js 将在默认情况下查找 JavaScript 文件。

请注意

目录也可以通过 require 函数加载。 如果提供的是一个目录而不是一个 JS 文件,require 函数将在指定的目录中查找一个名为index.js的文件并加载该文件。 如果无法找到该文件,则会抛出一个错误。

要创建一个模块,也就是 Node.js 模块,我们使用module.exports属性。 在 Node.js 中,每个 JavaScript 文件都有一个全局变量对象module。 对象中的exports字段定义将从模块中导出哪些项。 当使用require()函数导入模块时,require()的返回值是在模块的module.exports字段中设置的值。 模块通常导出一个函数或对象,该对象为导出的每个函数或变量提供属性。 下面的代码片段显示了一个导出模块的示例:

module.exports = {
  exportedVariable,
  exportedFn
}
const exportedVariable = 10;
function exportedFn( args ){ console.log( 'exportedFn' ) ;}
代码片段 6.2:导出 Node.js 模块

练习 32:导出和导入 NPM 模块

要构建、导出和导入 NPM 模块,请执行以下步骤:

  1. 为我们的模块创建一个 JavaScriptmodule.js
  2. 设置一个对象的module.exports 属性。
  3. 给值为An exported constant!的对象添加exportedConstant字段
  4. exportedFunction字段添加到对象中,并将其值设置为一个函数,该函数记录到控制台的文本An exported function!
  5. 为我们的主代码创建一个index.js文件。
  6. 使用require函数从module.js中导入模块,并将其保存到ourModule变量中。
  7. ourModule开始记录exportedString的值。
  8. ourModule调用exportedFunction函数。

编码

module.js
module.js
module.exports = {
 exportedString: 'An exported string!',
 exportedFunction(){ console.log( 'An exported function!' ) }
};
代码片段 6.3:将代码导出为模块

https://bit.ly/2M3SIsT

index.js
const ourModule = require('./module.js');
console.log( ourModule.exportedString );
ourModule.exportedFunction();
代码段 6.4:将代码导出为模块

https://bit.ly/2RwOIXP

结果

Figure 6.1: Test values output

图 6.1:测试值输出

您已经成功构建、导出和导入了 NPM 模块。

基本的 Node.js 服务器

Node.js 最常见的应用是 web 服务器。 Node.js 使得构建高效且可伸缩的 web 服务器变得非常容易,因为开发者无需担心线程问题。 在本节中,我们将演示在 Node.js 中创建基本 web 服务器所需的代码。

Node.js 服务器可以设置为 HTTP、HTTPS 或 HTTP2 服务器。 对于本例,我们将创建一个基本的 HTTP 服务器。 Node.js 通过 HTTP 模块提供 HTTP 服务器的基本功能。 使用 require 语句导入 HTTP 模块。 如下代码片段所示:

const http = require( 'http' );
代码片段 6.5:加载 HTTP 模块

这行代码将导入模块'HTTP'中包含的功能,并将其保存在变量http中供以后使用。 现在我们已经加载了 HTTP 模块,我们需要选择主机名和端口来运行服务器。 因为这个服务器将只在我们的计算机上本地运行,我们可以使用机器的内部本地网络的 IP 地址 localhost('127.0.0.1')作为我们的主机名地址。 我们可以在其他应用尚未使用的任何网络端口上运行本地服务器。

您可以选择任何有效的端口号,但是很少有程序默认使用端口8000,所以在本演示中使用的是8000。 在代码中添加一个变量以包含端口号和主机名。 下面的代码片段显示了到这一点的完整代码:

const http = require('http');
const hostname = '127.0.0.1';
const port = 8000;
片段 6.6:简单服务器的常量

现在我们已经为服务器设置了所有的基本参数,我们可以编写代码来创建和启动服务器。 HTTP 模块包含一个名为createServer()的函数,它返回一个服务器对象。 这个函数可以接受一个可选的回调函数,作为 am HTTP 请求监听器。 当任何 HTTP 请求进入服务器时,所提供的回调方法将被调用。 我们需要使用请求侦听器回调调用createServer函数,以便服务器能够正确响应入站 HTTP 请求。 这是通过如下代码片段所示的代码行完成的:

const server = http.createServer((req, res) => {  res.statusCode = 200;  res.setHeader('Content-Type', 'text/plain');  res.end('Welcome to my server!\n');});
代码片段 6.7:创建一个简单的服务器

在前面的代码片段中,我们调用 createserver函数,并将返回的服务器保存到server变量中。 我们传递一个回调到createServer()。 这个回调函数有两个参数:reqresreq参数表示传入的 HTTP 请求,res参数表示服务器的 HTTP 响应。 在回调的第一行代码中,我们将响应状态代码设置为200。 响应中的200状态码表示对服务器的 HTTP 请求成功。 在状态代码后面的一行中,我们将响应中的Content-Type标头设置为text/plain。 这一步告诉响应,传递给它的数据将是纯文本。 在回调的最后一行,我们调用res.end()函数。 这个函数将传递给它的数据附加到响应中,然后关闭响应并将其发送回请求者。 在这个代码片段中,我们将Welcome to my server!字符串传递给end()函数。 响应附加了字符串,并将文本发送回请求者。 我们的服务器现在用这个处理程序处理所有对它的 HTTP 调用。

让我们的迷你服务器启动并运行的最后一步是在服务器对象上调用.listen()函数。 listen功能用于在指定的porthostname上启动 HTTP 服务器。 一旦服务器开始监听,它就可以接受 HTTP 请求。 下面的代码片段显示了如何让服务器在指定的hostname上监听指定的port:

server.listen( port, hostname, () => {  console.log('Server running at http://${hostname}:${port}/');});
片段 6.8:服务器开始侦听主机名和端口

前面的代码片段显示了如何调用server.listen()函数。 传递给函数的第一个参数是服务器将公开的端口号。 第二个参数是访问服务器的主机名。 在本例中,端口计算结果为8000,主机名计算结果为127.0.0.1(计算机的本地网络)。 在这个例子中,我们的服务器将监听127.0.0.1:8000。 传递给.listen()的最后一个参数是一个回调函数。 一旦服务器在指定的端口和主机名上开始监听 HTTP 请求,就会调用提供的回调函数。 在前面的代码片段中,回调函数只是打印可以从本地访问服务器的 URL。 你可以在浏览器中输入这个 URL,然后加载一个网页。

练习 33:创建基本 HTTP 服务器

要构建基本的 HTTP 服务器,执行以下步骤:

  1. 导入http模块。
  2. 为主机名和端口设置变量,分别赋值为127.0.0.18000,
  3. 使用http.createServer创建服务器。
  4. 提供一个回调到createServer函数,接受参数reqres
  5. 将响应状态码设置为200
  6. 设置响应内容类型为text/plain
  7. My first server!响应请求
  8. 使用server.listen功能使服务器监听指定的端口和主机。
  9. 提供一个回调到listen函数,该函数记录Server running at ${server uri}的日志。
  10. 启动服务器并加载已登录的 web 页面。

编码

index.js
const http = require( 'http' );
const hostname = '127.0.0.1';
const port = 8000;
const server = http.createServer( ( req, res ) => {
 res.statusCode = 200;
 res.setHeader( 'Content-Type', 'text/plain' );
 res.end( 'My first server!\n' );
} );
server.listen( port, hostname, () => console.log( 'Server running at http://${hostname}:${port}/' ) );
片段 6.9:简单的 HTTP 服务器

https://bit.ly/2sihcFw

结果

Figure 6.2: Returning the new cart array

图 6.2:返回新的购物车数组

Figure 6.3: Returning the new cart array

图 6.3:返回新的购物车数组

您已经成功构建了一个基本的 HTTP 服务器。

溪流和管道

流数据可能是 Node.js 中最复杂和最容易被误解的方面之一。 流也是 Node.js 提供的最强大的特性之一。 流只是数据的集合,就像标准数组或字符串一样。 主要的区别是,对于流,所有的数据可能不能在同一时间可用。 你可以把它想象成 YouTube 或 Netflix 上的流媒体视频。 在开始观看之前,您不需要下载整个视频。 视频提供商(YouTube 或 Netflix)将视频分成小块发送或“流”到你的电脑。 您可以开始观看视频的一部分,而无需等待任何其他部分首先加载。 流非常强大,因为它们允许服务器和客户端不需要一次将整个大数据集加载到内存中。 在 JavaScript 服务器中,流对于内存管理至关重要。

Node.js 中的许多内置模块都依赖于流。 这些模块包括 HTTP 模块(http)中的请求和响应对象,文件系统模块 (fs)文件加密模块(crypto),以及子进程模块(child_process)。 在 Node.js 中,流有四种类型:可读可写双工转换。 理解它们的作用是非常简单的。

流的类型

数据从可读流中消费。 它们抽象了源的加载和分块。 数据由可读流一次一个块地呈现供消费(使用)。 在数据块被消费之后,它被流释放,并呈现下一个数据块。 可读流中不能有消费者推入的数据。 可读流的一个例子是 HTTP 请求体。

可读流有两种模式:流动暂停。 这些模式决定了流的数据流。 当流处于流动模式时,数据将自动从底层流系统读取并提供给消费者。 当流处于暂停模式时,不会自动从底层系统读取数据。 消费者必须显式地使用stream.read()函数从流请求数据。 所有可读流都以暂停模式开始,可以通过附加data事件处理程序、调用stream.resume()calling stream.pipe()来切换为流。 本节稍后将讨论事件处理程序和流管道。 可使用stream.pause()方法或stream.unpipe()方法将可读流从流动切换到暂停。

可写流是数据可以写入或推入的流。 可写流抽象了源的组合和处理。 数据被呈现给流供提供者使用。 流每次只消耗一个数据块,直到被告知停止。 在流消费了一个数据块并适当地处理它之后,它将消费或请求可用的下一个数据块。 可写流的一个例子是文件系统模块函数createWriteStream,它允许我们将数据流写入磁盘上的文件。

双工流是既可读又可写的流。 数据可以由提供者以块的形式推入流,也可以由使用者以块的形式从流消费。 双工流的一个例子是网络套接字,例如 TCP 套接字。

转换流是一种双工流,允许数据块在通过流时发生突变。 转换流的一个例子是 Node.js 的ZLib模块中的gzip方法,该方法使用gzip压缩方法来压缩数据。

流以块的形式加载数据,而不是一次全部加载,所以为了有效地使用流,我们需要一些方法来确定数据是否已经被流加载。 在 Node.js 中,流是EventEmitter原型的实例。 当关键事件发生时,流会触发事件,比如错误或数据可用性。 事件监听器可以通过.on().once()方法附加到流上。 可读流和可写流都有用于数据处理、错误处理和蒸汽管理的事件。

下表显示了可用的事件及其目的:

可写流事件:

Figure 6.4: Writeable Stream Events

图 6.4:可写流事件

可读流事件:

Figure 6.5: Readable Stream Events

图 6.5:可读流事件

请注意

这些事件监听器可以附加到流以处理数据流和管理流的状态。 完整的文档可以在 Node.js 网站的 Stream API 下找到。

现在您已经了解了流的基础知识,我们必须实现它们。 可读流遵循一个基本的工作流。 通常,将调用返回可读流的方法。 一个例子是文件系统 API 函数createReadStream(),,它创建一个可读的流,将文件从磁盘上流出去。 在可读流返回后,我们可以通过附加data事件处理程序从流中提取数据。 下面的代码片段显示了一个例子:

const fs = require( 'fs' );
fs.createReadStream( './path/to/files.ext' ).on( 'data', data => { 
  console.log( data );  
} );
代码片段 6.10:使用可读流

在上面的例子中,我们导入了fs模块并调用createReadStream函数。 这个函数返回一个可读的流。 然后将事件监听器附加到data事件。 这将流置于流动模式,并且每次数据块准备就绪时,将调用提供的回调。 在这个例子中,我们的回调只是记录可读流放弃的数据。

就像可读流一样,可写流也遵循一个非常标准的工作流。 可写流最基本的工作流程是首先调用返回可写流的方法。 这方面的一个例子是fs模块函数createWriteStream。 创建可写流后,可以使用stream.write()函数对其进行写入。 这个函数将把传入它的数据写到流中。 下面的代码片段显示了一个例子:

const fs = require( 'fs' );
const writeable = fs.createWriteStream( './path/to/files.ext' );
writeable.write( 'some data' );
writeable.write( 'more data!' );
代码片段 6.11:使用可写流

在前面的代码片段中,我们加载了fs模块并调用了createWriteStream函数。 这将返回一个将数据写入文件系统的可写流。 然后我们多次调用stream.write()函数。 每次调用write函数时,传入函数的数据都被推入可写流并写入磁盘。

Node.js 中最强大的特性之一是管道流的能力。 通过管道将流传递到源并将其“管道”到目的地。 您从一个流获取数据输出,并将其通过管道输送到另一个流的输入。 这是非常强大的,因为它允许我们简化连接流的过程。

考虑这样一个问题:我们必须从磁盘加载文件,并将其以 HTTP 响应的形式发送给客户机。 我们可以用两种方法来做到这一点。 我们可以构建的第一个实现是将整个文件加载到内存中,然后一次性将其全部推到客户机。 这对我们的服务器来说是非常低效的。 第二是利用流。 我们将文件从磁盘流化,并将数据块推入请求流。 要在没有管道的情况下做到这一点,我们必须将侦听器附加到读流并捕获每个数据块,然后将数据块推入 HTTP 响应。 伪代码如下所示:

const fileSystemStream = load( 'filePath' );
fileSystemStream.on( 'data', data => HTTP_Response.push( data ) );
fileSystemStream.on( 'end', HTTP_Response.end() );
片段 6.12:用流发送数据到 HTTP 响应

在前面的代码片段中的伪代码中,我们创建了一个从指定的文件路径加载的流。 然后我们为data事件和end事件添加一个事件处理程序。 每次数据事件为我们提供数据时,我们将该数据推入HTTP_Response流。 一旦没有更多的数据和结束事件被触发,我们关闭数据发送到客户端的HTTP_Response流。 这需要几行代码,并要求开发人员管理数据和数据流。 我们可以在一行代码上用流管道构建完全相同的功能。

管道流是通过Stream.pipe()功能完成的。 Pipe 在源流上被调用,并作为参数传递给目标流(例如,readableStream.pipe( writeableStream ))。 Pipe 返回目标流,这允许它用于链接管道命令。 使用与前面示例相同的场景,我们可以使用 pipe 命令将伪代码简化为一行。 如下代码片段所示:

load( 'filePath' ).pipe( HTTP_Response );
代码片段 6.13:管道数据伪代码

在前面的代码片段中,我们加载了文件数据并将其输送到HTTP_response。 可读流加载的每个数据块自动传递给可写流HTTP_Response。 当可读流完成加载数据时,它会自动关闭,并告诉写入流也关闭。

文件系统操作

Node 的文件系统模块,名为“fs”,提供了一个 API,我们可以用它与文件系统交互。 文件系统 api 是围绕 POSIX 标准建模的。 POSIX (Portable Operating System Interface, Portable Operating System Interface)标准是由 IEEE 计算机协会(IEEE Computer Society)指定的一种标准,用于保持不同操作系统文件系统之间的普遍兼容性。 您不需要了解该标准的细节,但要了解 fs 模块遵循该标准以保持跨平台兼容性。 导入文件系统模块,可以使用以下命令:const fs = require( 'fs' );

Node.js 中的大多数文件系统函数要求您指定要使用的文件的路径。 为 fs 模块指定文件路径时,可以在一个指定的路径三个方面:作为一个字符串,缓冲,或URL 对象使用file:协议。 当路径是字符串时,文件系统模块将尝试解析字符串以获取有效的文件路径。 如果文件路径是一个缓冲区,文件系统模块将尝试将缓冲区的内容解析为一个有效的文件路径。 如果路径是 URL 对象,文件系统将把对象转换为有效的 URL 字符串,然后尝试解析字符串以获取有效的文件路径。 下面的代码片段显示了这三种显示文件路径的方法:

fs.existsSync( '/some/path/to/file.txt' );
fs.existsSync( Buffer.from( '/some/path/to/file.txt' ) );
fs.existsSync( new URL( 'file://some/path/to/file.txt' ) );
片段 6.14:文件系统路径格式

正如您在前面的示例中看到的,我们使用了fs模块中的existsSync函数。 在第一行中,我们将文件路径作为字符串传入。 在第二行中,我们从文件路径字符串创建一个缓冲区,并将缓冲区传递给existsSync函数。 在最后一个示例中,我们从文件路径的file:协议 URL 创建一个 URL 对象,并将 URL 对象传递给existsSync函数。

文件路径分为相对绝对两种。 绝对路径是从操作系统的根文件夹解析的。 从当前工作目录解析相对路径。 当前工作目录可通过process.cwd()函数获取。 通过字符串或缓冲区指定的路径可以是相对路径或绝对路径。 使用 URL 对象指定的任何路径都必须是该对象的绝对路径。

文件系统模块引入了许多功能,使我们能够与硬盘交互。 对于这些函数的大部分,有同步和异步两种实现。 同步 fs 函数阻塞! 在编写使用 fs 模块的任何代码时,记住这一点非常重要。

请注意

还记得在第 2 章,异步 JavaScript中对阻塞操作的定义吗? 阻塞操作将阻止事件循环处理任何事件。

如果您使用同步fs函数来加载一个大文件,它将阻塞事件循环。 在同步fs函数完成其工作之前,不会处理任何事件。 Node.js 线程将不做其他事情,包括响应 HTTP 请求、处理事件或任何其他异步工作。 您应该始终使用fs函数的异步版本。 唯一需要使用同步版本的情况是,必须在任何其他操作之前执行一个文件系统操作。 一个例子是加载整个系统或服务器所依赖的文件。

Express Server

在本主题的前面一节中,我们讨论了基本的 Node.js HTTP 服务器。 我们创建的服务器非常基础,缺少很多我们期望从真正的 web 服务器获得的功能。 在 Node.js 中,用于创建最小且灵活的 web 服务器的最常见模块之一是Express。 Express 接受 Node.js 服务器对象,并将其包装在简化功能的 API 中。 Express 可通过 NPM(npm install express --save)安装。

请注意

Express 的完整文档可在https://expressjs.com上找到。

基本的 Express 服务器非常容易创建。 让我们回顾一下我们在本章前面创建的基本 Node.js HTTP 服务器。 在基本的 HTTP 服务器示例中,我们首先使用HTTP.createServer()函数创建一个服务器,并传递一个基本的请求处理程序。 然后我们用server.listen()功能启动服务器。 Express 服务器以类似的方式创建。 与 HTTP 服务器一样,我们必须首先要求我们的模块。 为Express模块添加require语句,并创建变量来保存主机名和端口号。 接下来,我们必须创建 Express 服务器。 这只需调用默认从require('express')语句导入的函数即可。 调用导入的函数并将结果保存在变量中。 如下代码片段所示:

请注意

简单 HTTP 服务器的代码可以在练习 33 的代码中找到。

const express = require( 'express' );
const hostname = '127.0.0.1';
const port = 8000;
const app = express();
片段 6.15:设置 Express 服务器

在前面的代码片段中,我们导入了Express模块并将其保存到变量Express中。 然后我们创建了两个常量变量—一个用于保存主机名,另一个用于保存端口号。 在最后一行代码中,我们调用了由 require 语句导入的函数。 这将创建一个具有所有默认参数的基本Express服务器。

复制基本 HTTP 服务器必须做的下一步是添加基本 HTTP 请求处理程序。 这可以通过app.get()功能来完成。 App.get为提供给它的路径设置 HTTP GET 请求处理程序。 它接受两个参数——一个路径和一个回调。 路径指定处理程序将捕获请求的 URL 路径。 callback是处理 HTTP 请求时调用的函数。 我们应该为服务器的根路径('/')添加一个路由处理程序。 如下代码片段所示:

app.get( '/', ( req, res ) => res.end( 'Working express server!' ) )
代码片段 6.16:设置路由处理程序

在前面的代码片段中,我们添加了一个带有app.get()的路由处理程序。 我们传入根路径('/'),以便当基路径('localhost/')被 HTTP 请求击中时,指定的回调函数将被调用。 在回调中,我们传入一个有两个参数的函数:reqres。 与简单的 HTTP 服务器一样,req表示传入的 HTTP 请求,res表示传出的 HTTP 响应。 在函数体中,我们用字符串Working express server!关闭 HTTP 响应。 这告诉Express使用 base 200 HTTP 响应代码,并将文本作为响应体发送。

在最后一步中,为了让基本的Express服务器正常工作,我们必须让它侦听 HTTP 请求。 为此,我们可以使用app.listen()函数。 这个函数告诉服务器在指定的端口开始侦听 HTTP 请求。 我们将向app.listen()传递三个参数。 第一个参数为端口号。 第二个参数是主机名。 第三个参数是一个回调函数,它在服务器开始监听时被调用。 使用正确的端口、主机名和一个回调函数调用listen函数,该回调函数打印我们可以访问服务器的 URL。 下面的代码片段显示了一个例子:

app.listen( port, hostname, () => console.log( 'Server running at http://${hostname}:${port}/' ) );
片段 6.17:让 Express 服务器侦听传入的请求

在前面的代码片段中,我们调用了listen函数。 我们传入端口号,它被解析为8000; 主机名,解析为'127.0.0.1'; 和一个记录服务器 URL 的callback函数。 当服务器在局域网的8000端口开始监听 HTTP 请求时,调用callback函数。 转到登录到控制台的 URL,查看基本服务器的运行情况!

Exercise 34: Creating a Basic Express Server

要构建基本的 Express 服务器,请执行以下步骤:

  1. 导入express模块。
  2. 设置主机名和端口的变量,分别赋值为127.0.0.18000,
  3. 通过调用express()创建服务器应用,并将其保存到app变量中。
  4. 向基本路由/添加一个 get 请求处理程序。
  5. 提供callback功能,接收reqres,并以Working express server!.文本结束响应。
  6. 使用app.listen()让服务器监听指定的端口和主机。
  7. 提供回调到app.listen() ,记录Server running at ${server uri}
  8. 启动服务器并在浏览器中加载指定的 URL。

编码

index.js
const express = require( 'express' );
const hostname = '127.0.0.1';
const port = 8000;
const app = express();
app.get( '/', ( req, res ) => res.end( 'Working express server!' ) );
app.listen( port, hostname, () => console.log( 'Server running at http://${hostname}:${port}/' ) );
片段 6.18:Simple Express 服务器

https://bit.ly/2Qz4Z93

结果

Figure 6.6: Returning the new cart array

图 6.6:返回新的购物车数组

Figure 6.7: Returning the new cart array

图 6.7:返回新的购物车数组

您已经成功构建了一个基本的 Express 服务器。

路由

Express 最强大的功能之一是它灵活的路由。 路由是指 web 服务器的端点 uri 如何响应客户端请求。 当客户端向 web 服务器发出请求时,它使用指定的 HTTP 方法(GETPOST等)请求指定的端点(URI 或路径)。 web 服务器必须显式地处理它将接受的路径和方法,以及说明如何处理请求的回调函数。 在 Express 中,这可以通过以下代码行完成:app.METHOD( PATH, HANDLER );。 变量是 Express 服务器的实例。 方法是为其设置处理程序的 HTTP 方法。 该方法应该是小写的。 该路径是处理程序将响应的服务器上的 URI 路径。 处理程序是回调函数,如果路径和方法与请求匹配,则执行该回调函数。 下面的代码片段显示了该功能的一个示例:

app.get( '/', ( req, res ) => res.end('GET request at /') );
app.post( '/user', ( req, res ) => res.end( 'POST request at /user') );
app.delete( '/cart/item', ( req, res ) => res.end('DELETE request at /cart/item') );
片段 6.19:快速路由示例

在前面的代码片段中,我们为 Express 服务器设置了三个路由处理程序。 第一个是建立了.get()功能。 这意味着服务器将寻找到指定路由的GET请求。 我们传入服务器的基本路由(/)。 当基本路由被一个GET请求击中时,提供的回调函数将被调用。 在回调函数中,我们使用字符串GET request at /作为响应。 在第二行代码中,我们将服务器设置为在/user路径上响应POST请求。 当一个POST请求击中我们的 Express 服务器时,我们调用提供的回调函数,用字符串POST request at /user.关闭响应。在最后一行代码中,我们为DELETE请求设置一个处理程序。 当一个DELETE请求进入 URI/cart/item,时,我们使用所提供的回调进行响应。

Express 还支持特殊功能app.all()。 如果您经常处理 HTTP 请求,那么您将认识到 ALL 不是一个有效的 HTTP 方法。 app.all()是一个特殊的处理函数,它告诉 Express 用指定的回调将所有有效的 HTTP 请求方法响应到指定的 URI。 如果一个路由打算接受任何请求方法,它被添加到 Express 中以帮助减少重复代码。

Express 支持对请求 URI 和 HTTP 方法拥有多个回调函数。 为此,我们必须向回调函数添加第三个参数:nextnext是一个函数,当next被调用时,Express 将移动到与方法和 URI 匹配的下一个回调处理程序。 下面的代码片段显示了一个例子:

app.get( '/', ( req, res, next ) => next() );
app.get( '/', ( req, res ) => res.end( 'Second handler!' ) );
代码片段 6.20:同一路由的多个请求处理程序

在前面的代码片段中,我们设置了两个不同的路由处理程序和对基 URI 的GET请求。 当一个到基本路由的GET请求被捕获时,第一个处理程序将被调用。 这个处理程序只调用next()函数,该函数告诉 Express 寻找下一个匹配的处理程序。 Express 看到有第二个匹配的处理程序,并调用第二个处理程序函数,该函数关闭 HTTP 响应。 重要的是要注意,HTTP 响应只能被关闭并设置回客户机一次。 如果为 URI 服务器和 HTTP 方法设置多个处理程序,则必须确保只有一个处理程序关闭 HTTP 请求,否则将发生错误。 多个处理程序提供的功能对于 Express 中的中间件和错误处理至关重要。 这些应用将在本节的后面进行更详细的讨论。

高级路由

如前所述,在 Express 中,当检查要调用哪个处理程序回调时,路由路径是它匹配的路径 uri 以及 HTTP 方法。 路由路径作为第一个参数传递给函数,如app.get()。 Express 的强大功能来自于创建匹配多个 uri 的动态路由路径的能力。 在 Express 中,路由路径可以是字符串、字符串模式或正则表达式。 Express 将解析基于字符串的路由中的特殊字符?+*()$[]。 当用于字符串路径时,特殊字符?+*()是正则表达式对应项的子集。 []字符用于转义部分 URL。 它们不是按字面意思在字符串中解释的。 $字符是 Express 路径解析模块中的保留字符。 如果必须在路径字符串中使用$字符,则必须使用[ and ]进行转义。 例如,/user/$22515应该在 Express 路由处理程序中写成/data/[\$]22515

如果我们希望在我们的路线中加入特殊字符以增加灵活性,我们可以使用字符?+*()。 这些字符的操作方式与正则表达式中的对应字符相同。 这意味着?字符用于表示可选字符。 任何跟随?符号的字符或字符组都将被视为可选的,Express 将匹配按字面意思匹配全字符串的可选字符或按字面意思匹配不含可选字符的全字符串的 uri。 如下代码片段所示:

app.get( '/abc?de', ( req, res ) => {
  console.log( 'Matched /abde or /abcde' );
} );
代码段 6.21:路由路径中的可选字符

在前面的代码片段中,我们为 URL 路径'/abc?de'设置了一个GET处理程序。当这个 URL 被命中时,回调函数被调用,它记录两个可能的 URI 匹配选项。 由于?字符在c字符之后,因此 c字符被认为是可选的。 Express 将把两个GET请求匹配到包含或不包含可选字符的 URI。 /abde/abcde 的请求将匹配。

符号 T0 用来表示它所跟随的字符或字符组的零次或多次重复。 Express 将匹配的路由字面上匹配没有重复字符的字符串,以及包含该标记字符一次或多次连续重复的任何字符串。 下面的代码片段显示了一个例子:

app.get( '/fo+d', ( req, res ) => {
  console.log( 'Matched /fd, /fod, /food, /fooooooooood' );
} );
片段 6.22:使用零个或多个重复字符进行路由

在前面的代码片段中,我们为 URL 路径fo+d设置了一个GET处理程序。 当命中这个 URI 时,将调用回调,它将记录一些匹配的选项。 由于 o 字符后面跟着+字符,Express 将解析包含 0 个或多个o字符的任何路由。 Express 将匹配fdfodfoodfoooooooooooood和任何其他连续o的字符串 URI。

*字符的功能类似于+字符,但匹配任意字符的零次或多次重复。 Express 将匹配不带额外字符的按字面意思匹配字符串的路由。 任何类型的一个或多个连续字符被用来代替星号。 下面的代码片段显示了一个例子:

app.get( '/fo*d', ( req, res ) => {
  console.log( 'Matched /fd, /fod, /fad, /faeioud' );
} );
代码片段 6.23:使用零或多个字符进行路由

在前面的代码片段中,我们为 URL 路径'fo*d设置了一个GET处理程序。 由于o字符后面跟着*字符,Express 将解析任何包含零或多个额外字符的路由。 Express 将匹配fdfodfadfooodfaeioud,以及任何替换*的连续字符的字符串 URI。 在比较+*字符时,请注意匹配字符串之间的区别。 *字符将匹配+字符匹配的所有字符串,并添加任何有效字符代替星号的字符串。

最后一组字符是()。 括号将一组字符组合在一起。 当与其他特殊字符(?, +, or *)结合使用时,分组字符被视为单个单位。 例如,URI/ab(cd)?ef将匹配 URI/abef/abcdef。 字符cd被分组在一起,整个组都服从于?字符。 下面的代码片段显示了这种与每个特殊字符交互的示例:

app.get( '/b(es)?t', ( req, res ) => {
  console.log( 'Matched /bt and /best' );
} );
app.get( '/b(es)+t', ( req, res ) => {
  console.log( 'Matched /bt, /best, /besest, /besesest' );
} );
app.get( '/b(es)*t', ( req, res ) => {
  console.log( 'Matched /bt, /best, /besest, /besesest' );
} );
片段 6.24:使用字符组进行路由

在前面的代码片段中,我们为路径b(es)?tb(es)+tb(es)*t设置了GET请求处理程序。 每个处理程序调用一个回调函数,该回调函数记录一些匹配选项。 在所有的处理程序中,字符es被分组在一起。 在第一个处理程序中,分组字符服从于?字符,并被认为是可选的。 处理程序将匹配包含完整字符串且只包含非可选字符的 uri。 两个选项是btbest。 在第二个处理程序中,字符组受制于+字符。 字符组有零次或多次连续重复的 uri 将匹配。 匹配选项有:btbest,besestbesesest,以及任何其他具有更连续重复的字符串。

Express 还允许我们在路由字符串中设置路由参数。 路由参数被命名为路由部分,它允许我们指定路由 URL 的部分来捕获并保存到变量中。 URL 的捕获部分被保存到req.params对象中,并使用与捕获的名称相匹配的键名。 URL 参数使用:字符指定,后跟捕获名称。 无论哪个字符串落在路径的这一部分,都会被捕获并保存。 下面的代码片段显示了一个例子:

app.get( '/amazon/audible/:userId/books/:bookId', ( req, res ) => {
  console.log( req.params );
} );
代码片段 6.25:使用 URL 参数进行路由

在前面的代码片段中,我们为路由/amazon/audible/:userId/books/:bookId设置了一个 get 参数。 该路由有两个命名参数捕获:一个用于userId,一个用于bookId。 这两个命名捕获可以包含任意一组有效的 URL 字符。 audible//books之间的字符将被保存到req.params.userId变量中,books/之后的字符将被保存到req.params.bookId变量中。 重要的是要注意,/字符是用来分割 URL 部分的。 保存的捕获组将不包含/字符,因为 Express 将其解析为 URL 分隔符。

快速路由也可以使用正则表达式来代替路径字符串。 如果将正则表达式传递给请求处理程序的第一个参数而不是字符串,则 Express 将解析该正则表达式,任何匹配该正则表达式的字符串将触发提供的回调处理程序。 如果您不熟悉正则表达式,您可以在网上找到许多教授基础的教程。 下面的代码片段显示了正则表达式路径的一个示例:

app.get( /^web.*/, ( req, res ) => {
  console.log( 'Matched strings like web, website, and webmail' );
} );
代码片段 6.26:使用正则表达式进行路由

在前面的代码片段中,我们为正则表达式路由/^web.*/设置了一个GET处理程序。 如果匹配了这个处理程序,服务器将记录两个匹配的字符串示例。 我们提供给GET处理程序的正则表达式指定 URI 必须以字符串web开始,后面可以跟任意数量的字符。 这将匹配诸如/web/website/webmail等 uri。

中间件

Express 还通过一个称为中间件的特性扩展了服务器的灵活性。 Express 是一个路由和中间件框架,它本身几乎没有什么功能。 中间件是访问 HTTP 请求请求和响应对象的函数,并运行在处理序列的中间位置。 中间件可以执行以下四项任务之一:执行代码、更改请求和响应对象、结束 HTTP 请求-响应序列,以及调用应用于请求的下一个中间件。

请注意

中间件功能可以手工编写,也可以通过第三方 NPM 模块下载。 在编写中间件之前,检查该中间件是否已经存在。 官方中间件模块和一些精选的流行模块可以在https://expressjs.com/en/resources/middleware.html找到。

中间件函数有三个输入变量:reqresnextReq表示请求对象,res表示响应对象,next是一个函数,告诉 Express 继续到下一个中间件处理程序。 在本节的前面,我们看到了在向一个 URI 注册多个路由处理程序时使用的next函数。 next函数告诉中间件处理器将控制传递给处理器堆栈中的下一个中间件。 简单地说,它告诉next 中间件运行。 如果中间件没有结束请求-响应序列,它必须调用next函数。 如果没有,请求将挂起并最终超时。 中间件可以附加app.use()app.METHOD()功能,其中 method 是中间件所附加的小写 HTTP 方法。 使用app.use()设置的中间件将触发与指定可选路径匹配的所有 HTTP 方法。 带有 HTTP 方法函数的中间件将触发与方法和指定路径匹配的所有请求。 下面的代码片段显示了一个中间件示例:

app.use( ( req, res, next ) => {
  req.currentTime = new Date();
  next();
} );
app.get( '/', ( req, res ) => {
  console.log( req.currentTime );
} );
代码片段 6.27:设置中间件

在前面的代码片段中,我们使用app.use()设置了一个中间件函数。 我们没有提供到app.use(),的路径,所以所有的请求都会触发中间件。 中间件通过将请求中的currentTime字段设置为新的日期对象来更新请求对象。 然后,中间件调用下一个函数,该函数将控制权传递给下一个中间件或路由处理程序。 假设一个对基 URI 的请求,下一个命中的处理程序是已注册的处理程序,它打印req.currentTime中保存的值。

错误处理

Express 的最后一个重要方面是错误处理。 错误处理是 Express 处理和管理错误的过程。 Express 可以处理同步错误和异步错误。 Express 有内置的错误处理,所以您不需要自己编写。 Express 的内置错误处理程序将在响应体中将错误返回给客户端。 这可能包括错误细节,如堆栈跟踪。 如果希望用户看到自定义错误消息或页面,则必须附加自己的错误处理程序。

内置的 Express 错误处理程序将捕获在路由处理程序或中间件函数中的同步代码中抛出的任何错误。 这包括运行时错误和使用 throw 关键字抛出的错误。 但是,Express 不会捕获在异步函数中抛出的错误。 要在异步函数中调用错误,必须将next函数添加到回调处理程序中。 如果发生错误,则必须使用希望处理的错误调用 next。 下面的代码片段显示了使用默认错误处理程序的同步和异步错误处理示例:

app.get( '/synchronousError', ( req, res ) => {
  throw new Error( 'Synchronous error' );
} );
app.get( '/asynchronousError', ( req, res, next ) => {
  setTimeout( () => next( new Error( 'Asynchronous error' ) ), 0 );
} );
代码片段 6.28:同步和异步错误处理

在前面的代码片段中,我们首先为路径/synchronousError创建了一个GET请求处理程序。 如果命中此处理程序,则调用回调函数,这将在同步代码块中抛出一个错误。 由于错误是在同步代码块中抛出的,所以 Express 会自动捕获错误并将其传递给客户端。 在第二个示例中,我们为路径/asynchronousError创建了一个GET请求处理程序。 当命中这个处理程序时,我们调用一个回调函数,该函数开始超时并调用next函数并返回错误。 错误发生在一个异步代码块中,因此必须通过下一个函数将其传递给 Express。 当 Express 捕捉到一个错误时(无论是通过抛出事件同步捕获还是通过下一个函数异步捕获),它会立即跳过堆栈中所有适用的中间件和路由处理程序,跳转到第一个适用的错误处理程序。

为了定义我们自己的错误处理程序中间件函数,我们以与其他中间件函数相同的方式添加它,但有一个关键区别。 错误处理程序中间件回调函数在回调中有四个参数,而不是三个。 参数依次为:errreqresnext。 其解释如下:

  • err表示正在处理的错误。
  • req表示请求对象。
  • res表示响应对象。
  • next是一个告诉 Express 转到下一个错误处理程序的函数。

自定义错误处理程序应该是最后定义的中间件。 下面的代码片段显示了一个自定义错误处理示例:

app.get( '/', ( req, res ) => {
  throw new Error( 'OH NO AN ERROR!' );
} );
app.use( ( err, req, res, next ) => {
  req.json( 'Got an error!' + err.message );
} );
代码片段 6.29:添加自定义错误处理程序

在前面的代码片段中,我们为基本路由添加了一个GET请求处理程序。 当处理程序被击中时,它调用一个回调函数,并抛出一个错误。 这个错误被 Express 自动捕获并传递给下一个错误处理程序。 下一个错误处理程序是我们用app.use()函数定义的。 这个错误处理程序捕获错误,然后用错误消息响应客户机。

Exercise 35: Building a Backend with Node.js

您的任务是为一个笔记应用构建一个 Node.js Express 服务器。 服务器应该向基本路由(/)提供一个基本的 HTML 页面(在index.html下的活动文件夹中提供)。 服务器需要有一个 API 路由来从服务器本地文件系统中的文本文件加载已保存的通知,还需要有一个 API 路由来将通知的更改保存到服务器本地文件系统中的文本文件。 服务器应该在加载通知时接受对 URI/loadGET请求,在保存通知时接受对 URI/savePOST请求。 所提供的 HTML 文件将假定这些是服务器上使用的 API 路径。 在构建服务器时,您可能希望使用将 strict 选项设置为 false 的身体解析器中间件,以简化请求的处理。

要构建一个工作的 Node.js 服务器来服务 HTML 文件并接受 API 调用,执行以下步骤:

  1. npm init建立项目。
  2. 使用 npm 安装expressbody-parser
  3. expresshttpbody-parser保存为bodyParserfs模块,并保存为变量。
  4. 创建一个名为notePath的变量,该变量包含文本文件(./note.txt)的路径。
  5. 我们正在创建服务器的日志。
  6. 使用express()创建服务器应用,并将其保存在app变量中。
  7. 使用http.createServer(app)从 Express 应用创建一个 HTTP 服务器,并将其保存在服务器变量中。
  8. 我们正在配置服务器的日志。
  9. Use the body-parser middleware to parse JSON request bodies.

    告诉 Express 应用使用中间件app.use()

    bodyParser.json()传递到使用函数。

    通过key/value pair. strict:false将一个选项对象传递给bodyParser.json()

  10. 创建一个路由器,用express.Router()处理路由,并将它保存到 router 变量中。

  11. Add a get route handler for the base route with router.route('/').get.

    添加一个回调函数,接受reqres

    在回调中,,使用res.sendFile()发送index.html文件。

    为第一个参数传入index.html,为第二个参数传入选项对象{root: __dirname}。

  12. Add a route for the /save route that accepts a POST request with router.route( '/save' ).post.

    路由处理程序回调应该接受参数reqres

    回调时,调用fs函数writeFile(),带有notePathreq.body参数和callback函数。

    在回调函数中,接受参数errdata

    如果提供了err,则用状态码500和 JSON 格式的错误关闭响应。

    如果没有提供错误,用200状态码和数据对象的 JSON 关闭响应。

  13. Add a route for the /load route that accepts a get request with router.route( '/load ).get.

    路由处理程序回调应该接受参数reqres

    回调时,调用带有notePathutf8参数的fs函数readFile和一个回调函数。

    在回调函数中,接受参数errdata

    如果提供了err,则用状态码500和 JSON 格式的错误关闭响应。

    如果没有提供错误,用200状态码和数据对象的 JSON 关闭响应。

  14. 使express应用使用路由器处理基本路由上的请求app.use('/', router)

  15. Set the server up to listen on the correct port and hostname and pass in a callback using server.listen( port, hostname, callback ).

    回调应该接受一个错误参数。 如果发现错误,则抛出该错误。

    否则,记录服务器正在侦听的端口。

  16. 启动服务器并加载在(localhost:PORT)运行的 URL。

  17. 通过保存笔记,刷新网页,加载保存的笔记(应该与之前保存的内容匹配),然后更新笔记,测试服务器的路由和功能。

编码

index.js
router.route( '/' ).get( ( req, res ) => res.sendFile( 'index.html', { root: __dirname } ) );
router.route( '/save' ).post( ( req, res ) => {
 fs.writeFile( notePath, req.body, 'utf8', err => {
   if ( err ) {
     res.status( 500 );
   }
   res.end();
 } );
} );
router.route( '/load' ).get( ( req, res ) => {
 fs.readFile( notePath, 'utf8', ( err, data ) => {
   if ( err ) {
     res.status( 500 ).end();
   }
   res.json( data );
 } );
} );
片段 6.30:用于复杂应用的快速服务器路由

https://bit.ly/2C4FR64

结果

Figure 6.8: Listening on port 8000

图 6.8:侦听端口 8000

Figure 6.9: Loading the test note

Figure 6.9:加载测试记录

您已经成功构建了一个工作的 Node.js 服务器,它提供 HTML 文件并接受 API 调用。

React

React是一个用于构建用户界面的 JavaScript 库。 React 主要由 FaceBook 维护。 React 最初由 Facebook 软件工程师 Jordal Walke 创建,并于 2013 年开放源代码。 React 旨在简化网页开发,允许开发者轻松构建单页面网站和移动应用。

请注意

React 的完整文档,以及扩展教程,可以在他们的主页上找到:https://reactjs.org/

React 使用声明性方法来设计视图,以提高页面的可预测性和调试性。 开发人员可以为应用中的每个状态声明和设计简单的视图。 React 将在状态改变时处理视图的更新和呈现。 React 依赖于一个基于组件的模型。 开发人员构建封装组件来跟踪和处理他们自己的内部状态。 我们可以将组件组合成复杂的用户界面,类似于我们如何使用函数组合从简单函数构建复杂函数。 通过组件,我们可以通过应用在组件之间传递丰富的数据类型。 这是允许的,因为组件逻辑完全是用 JavaScript 编写的。 最后,React 允许我们在转换到框架时非常灵活。 没有对应用背后的技术堆栈做任何假设。 React 可以在浏览器、Node.js 服务器上加载时编译,或者使用 React Native 编译到移动应用中。 这使得 React 可以慢慢地融入到新特性中,而不需要重构现有的代码。 您可以在技术栈中的任何一点开始合并 React。

插入反应

React 可以通过 NPM 安装,并在服务器上编译,或者通过脚本标记集成到应用中。 有几种方法可以安装 React 并将其添加到项目中。

将 React 添加到应用的最快方法是通过<script>标签包含内置库。 如果你有一个已经存在的项目,并且你想慢慢地开始将 React 整合到其中,这个方法通常是最简单的。 以这种方式添加 React 只需要不到一分钟的时间,并且可以为立即开始添加组件做好准备。 首先,我们需要向 HTML 页面添加一个 DOM 容器,以便 React 组件在其中进行附加。 这通常是一个具有惟一 ID 的 div。 然后添加带有脚本标签的ReactReactDOM模块。 一旦添加了脚本标记,我们就可以用脚本标记加载 React 组件了。 下面的代码片段显示了一个例子:

<div id="react-attach-point"></div>
<script src="https://unpkg.com/react@16/umd/react.development.js" crossorigin></script><script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js" crossorigin></script>
<script src="react_components.js"></script>
片段 6.31:向网页添加 React

建立一个 React 应用并将其安装到一个新项目的下一个最简单的方法是与 React 应用创建者一起。 该模块是一个 Node.js 命令行界面,它会自动设置一个 React 项目,该项目带有一个简单的预定义文件夹结构,并安装了基本的依赖项。 CLI 工具可以通过命令行npm install create-react-app -g安装。 这个命令告诉 NPM 在全局作用域中安装 CLI 模块,以便可以从命令行运行它。 命令行安装完成后,可以运行create-react-app my-app-name命令创建一个新的 React 项目。 CLI 工具将使用提供的名称(示例命令中的my-app-name)在工作目录中创建一个文件夹,安装 React 依赖项,并为应用的资源创建两个文件夹。 CLI 工具将使用示例应用填充源代码文件夹src。您可以使用npm start命令启动应用。 从这一点上,你可以开始破解和修改文件,看看 React 是如何工作的,或者你可以删除src中的所有文件,并开始编写自己的应用。

安装 React 最困难的方法是一次只安装一个独立的依赖项。 此方法提供了最大的灵活性,并允许您将 React 集成到现有的工具链中。 要安装 React,必须使用 NPM 安装reactreact-dom模块。 这两个模块都可以安装在本地项目范围内,并且应该用--save-s标记保存到package.json依赖项列表中。 一旦安装了模块,就可以用现有的工具链创建和构建 React 组件。

在本主题中,我们将使用 React 和 JSX。 JSX是一种 JavaScript 语法糖,浏览器默认不支持。 JSX 必须通过 Babel 转换成有效的 JavaScript 代码。 为了最终完成 React 的设置,你需要设置 Babel 将 React 和 JSX 代码转换成 JavaScript。 如果您的项目没有安装 Babel,您可以使用npm install babel -s命令安装它。

这将把 Babel 保存为您的项目的依赖项。 要将 React JSX 插件添加到 Babel 中,运行npm install babel-preset-react-app -s命令。 该命令为 Babel 添加 JSX 编译库。 在建立了 babel 之后,我们必须创建一个构建脚本,我们可以运行它来编译我们所有的代码。 在包中。 . json,添加以下行: 请注意,npx不是打印错误。 它是 NPM 附带的一个包运行器工具。 这一行告诉 Babel 用预设的react-app/prod将代码从src目录编译到lib目录。 每次我们修改 React 代码时都应该运行这个命令,并希望在前端反映出来。 现在可以开始构建 React 应用了。

请注意

您可以提供上一段所述的 Babel 设置命令来演示如何为转译设置项目。

基础知识

React 是围绕称为组件的小型封装代码片段构建的。 React 中的组件是通过子类化React.ComponentReact.PureComponent定义的。 最常用的方法是使用React.Component。 在最简单的形式中,React 组件接受属性(通常称为props),并通过调用render()返回要显示的视图。 属性在组件初始化时定义。 创建的每个组件都必须在子类中定义一个名为render()的方法。 render 函数返回将在屏幕上以 JSX 形式呈现的内容的描述。 下面的代码片段显示了一个示例组件声明:

class HelloWorld extends React.Component {
  render() {
    return (
      <div>
        Hello World!!! Made by {this.props.by}!!!
      </div>
    );
  }
}
ReactDOM.render(
  <HelloWorld by="Zach"/>,
  document.getElementById('root')
);
片段 6.32:基本的 React 元素

在前面的代码片段中,我们定义了一个名为HelloWorld的新 React 组件class。 这个新类扩展了基本的React.Component。 在声明中,我们定义了render()函数。 函数返回一个 JSX 块,该块定义了将在屏幕上呈现的内容。 在 JSX 的这个块中,我们用文本Hello World!!! Made by *!!!创建了一个div,其中*字符被替换为通过 by 属性传入的值。 在最后几行中,我们调用ReactDom.render()函数。 这告诉ReactDom模块渲染我们已经传递到render()函数中的所有组件和视图。 在前面的代码片段中,我们通过属性by设置为Zach传递了HelloWorld组件,并告诉 render 函数将呈现的 DOM 附加到根元素。 传递到属性中的数据被传递到组件中的this.props并填充到Hello World!!! div中。

请注意

如果你的代码库不使用 ES6 或 ES6 类,你可以使用 create-react-class 模块,但是,这个模块的细节超出了本书的范围。

恭喜你! 你已经了解了 React 最基本的形式。 通过扩展这个例子,您现在可以构建基本的静态网页。 这看起来可能不是很有用,但它是所有 web 开发中最基本的构建块。

React

从前面的代码片段中非常基本的示例中,我们可以看到 React 使用了一个奇怪的语法糖 JSX。 JSX 既不是 HTML 也不是 JavaScript。 它是 JavaScript 的语法扩展,合并了 HTML 和 XML 的一些概念,以帮助描述用户界面应该是什么样的。 JSX 对于 React 应用不是必需的,但是建议您在构建 React UI 时使用它。 它看起来像一种模板语言,但拥有 JavaScript 的全部功能。 它可以被编译成标准的 JavaScript 与 Babel React 插件。 下面的代码片段显示了 JSX 和等效 JavaScript 的一个示例:

const elementJSX = <div>Hello, world!</div>;
const elementJS = React.createElement( "div", null, "Hello, world!" );
Snippet 6.33: JSX vs JS

在前面的代码片段中,我们定义了一个名为elementJSX的变量,并将一个 JSX 元素保存到其中。 在第二行中,我们创建了一个名为elementJS的变量,并使用纯 JavaScript 将等效元素保存到其中。 在这个例子中,您可以清楚地看到 JSX 的 markdown 样式如何简化了在 JavaScript 中定义元素的方法。

JSX

JSX可以嵌入表达式,就像标准 JavaScript 中的模板字面量一样。 然而,主要的区别是 JSX 只使用花括号({})来定义表达式。 很像模板字面量,JSX 中使用的表达式可以是变量、对象引用或函数调用。 这允许我们使用 JSX 在 React 中创建动态元素。 下面的代码片段显示了 JSX 表达式的一个示例:

const name = "David";
function multiplyBy2( num ) { return num * 2; }
const element1 = <div>Hello {name}!</div>;
const element2 = <div>6 * 2 = {multiplyBy2(6)}</div>;
代码片段 6.34:JSX 表达式

在前面的代码片段中,我们首先创建了一个名为 name 的变量,该变量包含字符串David和一个名为multiplyBy2的函数,该函数接受一个数字,并返回这个数字乘以2。 然后我们创建一个名为element1的变量,并将一个 JSX 元素保存到其中。 这个 JSX 元素包含一个div,它包含一个引用name变量的表达式。 当构建这个 JSX 元素时,表达式计算name变量为字符串David,并将其插入到最终标记中。 在最后一行代码中,我们创建了一个名为element2的变量,并将另一个 JSX 元素保存到其中。 这个 JSX 元素包含一个 div,其中包含引用multiplyBy2函数的表达式。 当创建 JSX 元素时,表达式计算其中的代码并调用函数。 函数返回值放入最终标记中。 可以看到,JSX 中的表达式与模板字面量中的表达式非常相似。

【T0

当我们创建 React 元素时,我们必须有某种方式将它们呈现到 DOM 中。 这一点在 React 的介绍示例中已经有了简要的介绍。 在那个例子中,我们使用ReactDOM库来渲染我们创建的组件。 ReactDOM对象是从react-dom模块导入的,它提供了特定于 DOM 的方法,可以在应用中使用; 然而,大多数组件确实需要这些方法。 最常用的函数是render()函数。 这个函数接受三个参数。

第一个参数是我们将呈现或附加到 DOM 的 React 元素。 第二个参数是将 React 组件渲染到的容器或 DOM 节点。 最后一个参数是一个可选的回调方法。 回调函数将在组件呈现后执行。 对于完整的 React 应用,ReactDOM.render()通常只需要在应用的顶层,并用于在视图中渲染整个应用。 在应用中,当 React 组件被缓慢地合并到现有的代码库中,ReactDOM.render()可以在每个新的点上使用,当 React 组件被合并到非 React 代码中。 下面的代码片段显示了ReactDOM.render()的示例:

import ReactDOM from 'react-dom';
const element = <div>HELLO WORLD!!!</div>;
ReactDOM.render( element, document.getElementById('root'), () => {
  console.log( 'Done rendering' );
});
代码片段 6.35:将元素呈现到 DOM 中

在前面的示例中,我们首先导入了ReactDOM模块。 然后我们用 JSX 创建了一个新的 React 元素。 这个简单的元素只包含一个带有文本HELLO WORLD!!!div。 然后我们用这三个参数调用ReactDOM.render()函数。 这个函数调用告诉浏览器选择根 DOM 节点并附加 React 元素呈现的标记。 当呈现完成时,将调用提供的回调,并将Done rendering字符串记录到控制台。

React。 组件

React 围绕组件展开。 如前所述,创建新组件最简单的方法是创建继承React.Component类的新子类。 React.Component类可以通过 React NPM 模块导入的 React 对象来访问。 当我们定义 React 组件时,我们必须至少定义一个render()函数。 函数返回关于组件将包含什么的 JSX 描述。 如果我们希望创建更复杂的组件,比如带有状态的组件,我们可以向组件添加一个构造函数。 构造函数必须接受props变量,并且必须用props变量调用super()函数。 当创建 React 组件时,props变量将包含一个具有指定属性的对象。 下面的代码片段显示了一个带有构造函数的 React 组件示例:

class ConstructorExample extends React.Component{
  constructor( props ){
    super( props );
    this.variable = 'test';
  }
  render() { return <div>Constructor Example</div>; }
}
代码片段 6.36:React 类构造函数

在前面的代码片段中,我们创建了一个名为ConstructorExample的新组件。 在同一段代码中,我们调用constructor函数。 constructor函数接受一个参数,即包含属性的对象。 在构造函数中,我们调用super()函数并传入props变量。 然后我们创建一个名为variableclass变量,并将其赋值为test。 在类的末尾,像所有 React 组件所要求的那样,我们添加了一个返回组件 JSX 标记的render()函数。

国家

要向 React 组件添加一个局部状态,只需在这个作用域中初始化变量状态(this.state = {};)。 状态变量是 React 中一个特殊的关键字名称。 对this.state的任何更改都将导致render()函数被调用。 这允许我们根据组件的当前状态动态更改视图。

重要的是要知道状态函数的三个关键问题。 首先,永远不要用this.state.value = 'value'这样的语句直接修改状态。 以这种方式修改状态不会导致调用render()和更新视图。 相反,您必须使用setState()函数。 这将用传入函数的数据更新状态。 例如,我们必须这样设置状态:this.setState( { name: 'Zach' } )。 第二个关键细节是状态更新可能是异步的。 React 可以将多个setState调用批处理为单个更新以提高性能。 因为这个事实,而且this.propsthis.state可以异步更改,所以我们不能依赖它们的值来计算状态。 如果我们必须使用属性的当前状态或当前值来计算下一个状态,我们可以使用第二种形式的setState,它接受函数而不是对象。 该函数将接收前一个状态作为第一个参数,并接收状态更新应用时的属性对象作为第二个参数。 这可靠地允许我们使用前一个状态和属性信息来计算下一个状态。 最后,状态更新被合并而不是覆盖。 很像Object.assign函数,setState做了一个状态对象和新状态的浅合并。 当设置状态时,新对象将被合并到旧的状态对象中。 只有在新状态对象中指定的属性会更改。 旧状态对象中不在新状态对象中的所有属性将保持不变。

在 React 组件中,属性对象在组件内部是只读的。 这意味着从组件内部对属性对象的更改不会反映到父组件或 DOM 结构内部的任何变量。 数据只能向下流动。 因此,父组件对子组件属性的 JSX 标记的任何更改都将导致子组件重新呈现新的属性值。 为了让数据向上流动,我们必须以属性的形式将函数从父组件传递到子组件。 下面的代码片段显示了一个例子:

class ChildElement extends React.Component {
 render() {
   return (
     <button onClick={this.props.onClick}>
       Click me!
     </button>
   );
 }
}
class ParentElement extends React.Component {
 clicked() { console.log( 'clicked' ); }
 render() {
   return <ChildElement onClick={this.clicked.bind(this)}/>;
 }
}
代码片段 6.37:呈现子组件

在这个片段中,我们创建了两个组件。 第一个叫ChildElement,第二个叫ParentElementChildElement简单地包含一个按钮的 JSX,当单击时,调用通过onClick属性传入的函数。 ParentElement包含一个名为 clicked 的函数,该函数记录到控制台,当呈现时,返回带有ChildElement实例的 JSX。 ParentElement的 JSX 中创建的ChildElement属性设置为ParentElementclicked()功能。 当点击ChildElement中的按钮时,将调用clicked()函数。 在本例中,将父作用域绑定到此。 当我们将它传递给子元素(this.clicked.bind(this))时单击。 如果this.clicked需要访问父组件中的任何内容,我们必须将它的作用域绑定到父组件的作用域。 在 React 应用中,可以使用此功能创建向上的数据流。

在 React 中处理 DOM 事件与 HTML DOM 元素事件处理非常相似,但有一些主要的区别。 首先,在 React 中,事件名称使用了camelcase而不是小写字母。 这意味着在名称中的每个“新单词”,该单词的第一个字母是大写的。 例如,DOM 事件onclick在 React 中变成onClick。 其次,在 JSX 中,函数事件处理程序作为函数直接传递到处理程序定义中,而不是作为包含处理程序函数名的字符串。 下面的代码展示了标准 HTML 和 React 之间的区别:

<button onclick="doSomething()">HTML</button>
<button onClick={doSomething}>JSX and React</button>
片段 6.38:JSX 与 HTML 事件

在前面的代码片段中,我们创建了两个按钮。 第一个是在 HTML 中,有一个附加到它的onclick监听器调用doSomething函数。 第二个按钮是 JSX 格式的,也有一个调用doSomething函数的onclick监听器。 请注意侦听器定义方式的不同。 JSX 事件名称为camelcase,HTML 事件名称为小写。 在 JSX 中,我们通过一个表达式设置处理程序函数,该表达式的计算结果为该函数。 在 HTML 中,我们将事件处理程序设置为调用函数的字符串。

请注意

我们在第 3 章,DOM 操作和事件处理中了解到,直接在 DOM 中附加事件是一个不好的实践。 JSX 不是 HTML,这种做法是可以接受的,因为 JSX 通过转义嵌入在 JSX 中的任何值来防止注入攻击。

React 事件处理和标准 DOM 事件处理的另一个显著区别是,在 React 中,事件处理函数不能返回 false 来阻止默认行为。 必须对事件对象显式调用preventDefault()函数。

当在 React 中附加事件监听器时,我们必须小心使用this作用域。 在 JavaScript 中,默认情况下类方法没有绑定到this作用域。 如果函数被传递到其他地方,并从其他地方调用,则可能没有正确设置this作用域。 当将它们附加到侦听器或将方法作为属性传递时,您应该确保正确地绑定this作用域。

条件渲染

在 React 中,我们创建不同的组件来封装我们需要的视图或行为。 我们需要一种方法,根据应用的状态,只呈现我们创建的一些组件。 在 React 中,这被称为条件渲染。 在 React 中,条件呈现的工作方式与 JavaScript 条件语句相同。 我们可以使用 JavaScript 的 if 或条件操作符来决定要呈现哪些元素。 这可以通过几种方式实现。

有两种简单的方法,一种是使用一个函数根据当前状态返回一个 React 元素(JSX),而第二种方法是在 JSX 中使用一个条件语句根据当前状态返回一个 React 元素。 以下代码片段显示了这些形式的条件呈现的示例:

class AccountControl extends React.Component {
  constructor( props ) {
    super( props );
    this.state = { account: this.props.account };
  }
  isLoggedIn() {
    if ( this.state.account ) { return <LogoutButton/>; }
    else { return <LoginButton/>; }
  }
  render() {
    return (
      <div>
        {this.isLoggedIn()}
        {this.state.account ? <LogoutButton/> : <LoginButton/>}
      </div>
    );
  }
}
代码片段 6.39:条件呈现

在前面的代码片段中,我们创建了一个名为AccountControl的元素。 在构造函数中,我们将本地状态设置为一个对象,该对象包含从属性变量传入的帐户信息。 render 函数简单地返回一个带有两个表达式的div。 这两个表达式都利用条件呈现来显示基于当前状态的信息。 第一个表达式调用isLoggedIn函数,该函数检查this.state.account并根据当前状态返回LogoutButtonLoginButton,。 第二个表达式使用条件运算符检查this.state.account内联并根据本地状态返回LogoutButtonLoginButton

项目清单

在 React 中呈现项目列表非常简单。 它基于 JSX 和表达式的概念。 如前所述,JSX 使用表达式来创建动态代码。 如果一个表达式的计算结果是一个组件数组,那么所有组件都将呈现为在 JSX 中内联添加的。 我们可以构建组件的集合或数组,将集合保存在变量中,并将该变量包含在 JSX 表达式中。 下面的代码片段显示了一个例子:

class ListElement extends React.Component {
  render() {
    return (
      <ul> {this.props.items.map( i => <li>{i}</li> )} </ul>
    );
  }
}
ReactDOM.render(
  <ListElement items={[ 1, 4, 5, 5, 7, 9 ]}/>,
  document.getElementById( 'root' )
);
代码片段 6.40:呈现列表

在前面的代码片段中,我们创建了一个名为ListElement的元素。 这个元素只是接受一个项目数组,并将该数组映射到一个 JSX 元素数组,该数组包含<li>标记中的数组项值。 然后将得到的列表项数组返回到<ul>标记中。 当 JSX 将其编译为 HTML 时,数组中的每一项都按顺序插入到<ul>元素中。

HTML 表单

我们必须讨论的 React 的最后一个关键概念是 HTML 表单。 HTML 表单在 React 中的工作方式与其他 DOM 元素不同,因为 HTML 表单跟踪自己的内部状态。 如果我们只需要处理表单的默认行为,那么我们可以在 React 中直接使用它们,不会有任何问题。 然而,当我们想让 JavaScript 处理表单提交并访问表单中的所有数据时,我们遇到了一个复杂的问题。 出现这个问题是因为元素和 React 组件都试图同时跟踪表单的状态。

实现这一目标的方法是使用受控组件。 受控组件的目标是从表单元素中移除状态控制,并使 React 成为控制组件。 这是通过为字段的值更改事件(onChange)添加一个 React 事件监听器来完成的,并让 React 将其内部的state变量值设置为表单的值。 然后,React 将字段的值设置为保存在state变量中的值。 React 从input字段读取任何更改,并强制input字段接受存储在 React 组件中的数据所发生的任何更改。 下面的代码片段显示了一个例子:

class ControlledInput extends React.Component {
  constructor(props) {
    super(props);
    this.state = {value: ''};
  }
  handleChange(event) { this.setState({value: event.target.value}); }
  render() {
    return (
      <div>
        <input type="text" value={this.state.value} onChange={this.handleChange.bind(this)} />
        <div>Value: {this.state.value} </div>
      </div>
    );
  }
}
代码片段 6.41:反应组件状态

在前面的代码片段中,我们创建了一个名为ControlledInput的组件。 该组件有一个名为 value 的状态变量,用于存储文本输入的值。 我们创建了一个名为handleChange的函数,它通过设置一个等于从事件中读取的值来更新组件的状态。 在渲染函数中,我们创建一个包含input字段和div字段的 div。 这个输入字段的值映射到this.state.value和一个调用handleChange函数的事件监听器。 第二个div简单地反映了this.state.value的价值。 当我们在文本输入中进行更改时,将调用onChange侦听器,并将组件state.value设置为输入字段的当前值。 每当this.state.value发生变化时,这些变化就会反射回input字段。 组件的this.state.value值是绝对的,input字段被强制镜像。

Activity 6:使用 React 创建前端

Exercise 32为您做笔记的前端团队意外退出。 您必须使用 React 为这个应用构建前端。 你的前端应该有两个视图,Home 视图和 Edit 视图。 为每个视图创建一个R``eact组件。 home视图应该有一个按钮,可以将视图更改为edit视图。 edit视图应该有一个按钮,切换回home看来,文本输入包含Note Text、【病人】负载按钮,调用 API 加载路径,和保存按钮,调用 API 保存路径。 Node.js 服务器已经提供给你了。 用activities/activity6/activity/src/index.js编写 React 代码。 当您准备好测试您的代码时,在启动服务器之前运行构建脚本(在package.json中定义)。 你可以参考练习 35中的index.html文件来获得关于如何调用 API 路由的提示。

要构建一个有效的 React 前端并将其与 Node.js Express 服务器集成,执行以下步骤:

  1. activity/activity6/activity处开启启动器活动。 运行npm install安装所需的依赖项。
  2. src/index.js文件中创建HomeEditor组件。
  3. home视图应该显示应用名称,并有一个按钮,将应用状态更改为edit视图。
  4. edit视图应该有一个返回按钮,改变程序状态edit看来,文本输入,由edit view状态,控制负载按钮,使得一个请求到服务器保存的Note Text,保存按钮,使一个请求到服务器保存注意文本。
  5. App组件中,使用app状态来决定显示哪个视图(homeeditor)。

编码

结果

Figure 6.10: Home view

图 6.10:主视图

Figure 6.11: Edit View

图 6.11:编辑视图

Figure 6.12: Server View

图 6.12:服务器视图

你已经成功构建了一个工作的 React 前端,并将其与 Node.js Express 服务器集成。

请注意

这个活动的解在 293 页可以找到。

JavaScript 生态系统在过去 10 多年里得到了巨大的发展。 在本章中,我们首先讨论了 JavaScript 生态系统。 JavaScript 可用于构建完整的后端 web 服务器和服务、命令行界面、移动应用和前端网站。 在第二节中,我们介绍了 Node.js。 我们讨论了如何设置用于浏览器外 JavaScript 开发的 Node.js、Node Package Manager、加载和创建模块、基本 HTTP 服务器、流和管道、文件系统操作以及 Express 服务器。 在最后一个主题中,我们介绍了用于前端 web 开发的 React 框架。 我们讨论了 React 的安装以及 React 的基础和细节。

这是这本书的全部内容。 在本书中,您研究了 ES6 中的主要特性,并实现了这些特性来构建应用。 然后,处理 JavaScript 浏览器事件并创建遵循 TDD 模式的程序。 最后,你构建了后端框架 Node.js 和前端框架 React。 现在,你应该准备好工具,把你学到的东西应用到现实世界的实践中去。 感谢您选择这本高级 JavaScript 书籍。