NodeJS 移动应用开发学习手册(一)
原文:
zh.annas-archive.org/md5/4B062FCE9E3A0F235CC690D228FCDE03译者:飞龙
前言
MERN 堆栈可以被视为共享一个共同点的一组工具,即语言 JavaScript。本书以食谱的形式探讨了如何使用 MERN 堆栈构建遵循 MVC 架构模式的 Web 客户端和服务器应用程序。
MVC 架构模式的模型和控制器由关于使用 ExpressJS 和 Mongoose 构建 RESTful API 的章节涵盖。这些章节涵盖了关于 HTTP 协议、方法类型、状态码、URL、REST 和 CRUD 操作的核心概念。之后,它转向了特定于 ExpressJS 的主题,如请求处理程序、中间件和安全性,以及关于 Mongoose 的特定主题,如模式、模型和自定义验证。
MVC 架构模式的视图由关于 ReactJS 的章节涵盖。ReactJS 是一个基于组件的 UI 库,具有声明性 API。本书旨在提供构建 ReactJS Web 应用程序和组件的基本知识。除了 ReactJS,本书还包含了一个关于 Redux 的整个章节,从核心概念和原则到存储增强器、时间旅行和异步数据流等高级功能的解释。
此外,本书还涵盖了使用 ExpressJS 和 SocketIO 进行实时通信,以实现实时交换数据。
通过本书,您将了解使用 MVC 架构模式构建全栈 Web 应用程序的核心概念和要点。
为了充分利用本书
本书适用于有兴趣开始使用 MERN 堆栈开发 Web 应用程序的开发人员。为了能够理解章节,您应该已经对 JavaScript 语言有一般的知识和理解。
本书所需的内容
为了能够使用这些食谱,您需要以下内容:
-
您喜欢的 IDE 或代码编辑器。在编写食谱代码时使用了 Visual Studio Code(vscode),所以建议您试一试
-
能够运行 NodeJS 和 MongoDB 的操作系统(O.S),最好是以下之一:
-
macOS X Yosemite/El Capitan/Sierra
-
Linux
-
Windows 7/8/10(如果在 Windows 7 中安装 VSCode,则需要.NET 框架 4.5)
-
最好至少 1GB RAM 和 1.6GHz 处理器或更快
下载示例代码文件
您可以从www.packtpub.com的帐户中下载本书的示例代码文件。如果您在其他地方购买了本书,可以访问www.packtpub.com/support并注册,文件将直接发送到您的邮箱。
您可以按照以下步骤下载代码文件:
-
登录或注册www.packtpub.com。
-
选择“支持”选项卡。
-
点击“代码下载和勘误”。
-
在搜索框中输入书名,按照屏幕上的说明操作。
下载文件后,请确保使用最新版本的解压缩或提取文件夹:
-
Windows 的 WinRAR/7-Zip
-
Mac 的 Zipeg/iZip/UnRarX
-
Linux 的 7-Zip/PeaZip
该书的代码包也托管在 GitHub 上github.com/PacktPublishing/MERN-Quick-Start-Guide。如果代码有更新,将在现有的 GitHub 存储库上更新。
我们还有来自我们丰富书籍和视频目录的其他代码包,可在github.com/PacktPublishing/上找到。快去看看吧!
下载彩色图片
我们还提供了一个 PDF 文件,其中包含本书中使用的屏幕截图/图表的彩色图片。您可以在这里下载:www.packtpub.com/sites/default/files/downloads/MERNQuickStartGuide_ColorImages.pdf。
实战代码
访问以下链接查看代码运行的视频:
使用的约定
本书中使用了许多文本约定。
CodeInText:表示文本中的代码词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 句柄。 例如:"将下载的WebStorm-10*.dmg磁盘映像文件挂载为系统中的另一个磁盘。"
代码块设置如下:
{
"dependencies": {
"express": "4.16.3",
"node-fetch": "2.1.1",
"uuid": "3.2.1"
}
}
任何命令行输入或输出都以以下方式编写:
npm install
粗体:表示新术语、重要单词或屏幕上看到的单词。 例如,菜单或对话框中的单词会以这种形式出现在文本中。 例如:"从管理面板中选择系统信息。"
警告或重要说明会以这种形式出现。
提示和技巧会出现在这样的形式中。
章节
在本书中,您会发现一些经常出现的标题(准备工作、如何做...、让我们测试一下...、它是如何工作的...、还有更多...和参见)。
为了清晰地说明如何完成一个食谱,使用以下章节:
准备工作
本节告诉您在食谱中可以期待什么,并描述了如何设置食谱所需的任何软件或任何初步设置。
如何做...
本节包含了遵循该食谱所需的步骤。
让我们测试一下...
本节包括有关如何测试*如何做...*部分中给出的代码的详细步骤。
它是如何工作的...
本节通常包括对上一节中发生的事情的详细解释。
还有更多...
本节包括有关食谱的其他信息,以使您对食谱更加了解。
参见
本节为食谱提供了其他有用信息的链接。
第一章:MERN 堆栈简介
在本章中,我们将涵盖以下主题:
-
MVC 架构模式
-
安装和配置 MongoDB
-
安装 Node.js
-
安装 NPM 包
技术要求
您需要一个 IDE、Visual Studio Code、Node.js 和 MongoDB。您还需要安装 Git,以便使用本书的 Git 存储库。
本章的代码文件可以在 GitHub 上找到:
github.com/PacktPublishing/MERN-Quick-Start-Guide/tree/master/Chapter01
查看以下视频以查看代码的运行情况:
介绍
MERN 堆栈是由四个主要组件组成的解决方案:
-
MongoDB:使用面向文档的数据模型的数据库。
-
ExpressJS:用于构建 Web 应用程序和 API 的 Web 应用程序框架。
-
ReactJS:用于构建用户界面的声明性、基于组件的、同构的 JavaScript 库。
-
Node.js:基于 Chrome 的 V8 JavaScript 引擎构建的跨平台 JavaScript 运行时环境,允许开发人员构建各种工具、服务器和应用程序。
这些构成 MERN 堆栈的基本组件都是开源的,因此由一群伟大的开发者维护和开发。将这些组件联系在一起的是一种共同的语言,JavaScript。
本章的食谱主要关注设置开发环境以使用 MERN 堆栈。
您可以自由选择代码编辑器或 IDE。但是,如果您在选择 IDE 方面有困难,我建议您尝试一下 Visual Studio Code。
MVC 架构模式
大多数现代 Web 应用程序都实现了 MVC 架构模式。它由三个相互连接的部分组成,用于分离 Web 应用程序中信息的内部表示:
-
模型:管理应用程序的业务逻辑,确定数据应该如何存储、创建和修改
-
视图:数据或信息的任何可视表示
-
控制器:解释用户生成的事件并将其转换为命令,以便模型和视图相应地更新:
关注点分离(SoC)设计模式将前端与后端代码分开。遵循 MVC 架构模式,开发人员能够遵循关注点分离设计模式,从而实现一致和可管理的应用程序结构。
以下章节中的食谱实现了这种架构模式,以分离前端和后端。
安装和配置 MongoDB
官方的 MongoDB 网站提供了包含二进制文件的最新软件包,可用于在 Linux、OS X 和 Windows 上安装 MongoDB。
准备就绪
访问 MongoDB 的官方网站www.mongodb.com/download-center,选择 Community Server,然后选择您首选的软件操作系统版本并下载。
安装 MongoDB 并进行配置可能需要额外的步骤。
如何做...
访问 MongoDB 的文档网站docs.mongodb.com/master/installation/获取说明,并在教程部分检查您特定平台的内容。
安装后,可以以独立方式启动MongoDB-的守护进程mongod-的实例:
-
打开一个新的终端
-
创建一个名为
data的新目录,其中包含 Mongo 数据库 -
输入
mongod --port 27017 --dbpath /data/启动一个新实例并创建一个数据库 -
打开另一个终端
-
输入
mongo --port 27017连接到 Mongo shell 实例
还有更多...
作为替代方案,您可以选择使用数据库即服务(DBaaS)如 MongoDB Atlas,它在撰写本文时允许您创建一个带有 512MB 存储空间的免费集群。另一个简单的选择是 mLab,尽管还有许多其他选择。
安装 Node.js
官方 Node.js 网站提供了包含 LTS 和 Current(包含最新功能)二进制文件的两个包,可用于在 Linux、OS X 和 Windows 上安装 Node.js。
准备工作
为了本书的目的,我们将安装 Node.js v10.1.x。
如何做...
要下载最新版本的 Node.js:
-
访问官方网站
nodejs.org/en/download/ -
选择当前|最新功能
-
选择您喜欢的平台或操作系统(OS)的二进制文件
-
下载并安装
如果您喜欢通过包管理器安装 Node.js,请访问nodejs.org/en/download/package-manager/并选择您喜欢的平台或操作系统。
安装 npm 包
Node.js 的安装包括一个名为npm的包管理器,它是默认和最广泛使用的用于安装 JavaScript/Node.js 库的包管理器。
NPM 包列在 NPM 注册表registry.npmjs.org/中,您可以在其中搜索包,甚至发布您自己的包。
还有其他 NPM 的替代方案,如 Yarn,它与公共 NPM 注册表兼容。您可以自由选择您喜欢的包管理器;但是,为了本书的目的,配方中使用的包管理器将是 NPM。
准备工作
NPM 期望在您的project文件夹的根目录中找到一个package.json文件。这是一个描述项目细节的配置文件,例如其依赖关系、项目名称和项目作者。
在您能够在项目中安装任何包之前,您必须创建一个package.json文件。以下是您通常会采取的创建项目的步骤:
-
在您喜欢的位置创建一个新的
project文件夹,然后将其命名为mern-cookbook或者您自己选择的其他名称。 -
打开一个新的终端。
-
更改当前目录到您刚刚创建的新文件夹。通常使用终端中的
cd命令来完成。 -
运行
npm init来创建一个新的package.json文件,按照终端显示的步骤进行操作。
之后,您应该有一个类似以下的package.json文件:
{
"name": "mern-cookbook",
"version": "1.0.0",
"description": "mern cookbook recipes",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "Eddy Wilson",
"license": "MIT"
}
之后,您将能够使用 NPM 为您的项目安装新的包。
如何做...
-
打开一个新的终端
-
将当前目录更改为您新创建的
project文件夹所在的位置 -
运行以下命令来安装
chalk包:
npm --save-exact install chalk
现在,您将能够在 Node.js 中使用require来使用包。按照以下步骤来查看如何使用它:
- 创建一个名为
index.js的新文件,并添加以下代码:
const chalk = require('chalk')
const { red, blue } = chalk
console.log(red('hello'), blue('world!'))
- 然后,打开一个新的终端并运行以下命令:
node index.js
它是如何工作的...
NPM 将连接并在 NPM 注册表中查找名为 react 的包,如果存在,将下载并安装它。
以下是一些您可以使用 NPM 的有用标志:
-
--save:这将在您的package.json文件的dependencies部分中安装并添加包名称和版本。这些依赖是您的项目在生产中将使用的模块。 -
--save-dev:这与--save标志的工作方式相同。它将在package.json文件的devDependencies部分中安装并添加包名称。这些依赖是您的项目在开发过程中将使用的模块。 -
--save-exact:这将保留已安装包的原始版本。这意味着,如果您与其他人分享您的项目,他们将能够安装与您使用的确切相同版本的包。
虽然本书将为您提供逐步指南,以在每个示例中安装必要的软件包,但建议您访问 NPM 文档网站docs.npmjs.com/getting-started/using-a-package.json以获取更多信息。
第二章:使用 ExpressJS 构建 Web 服务器
在本章中,我们将涵盖以下配方:
-
在 ExpressJS 中进行路由
-
模块化路由处理程序
-
编写中间件函数
-
编写可配置的中间件函数
-
编写路由级中间件函数
-
编写错误处理程序中间件函数
-
使用 ExpressJS 内置的中间件函数来提供静态资产
-
解析 HTTP 请求体
-
压缩 HTTP 响应
-
使用 HTTP 请求记录器
-
管理和创建虚拟域
-
使用 helmet 保护 ExpressJS web 应用程序
-
使用模板引擎
-
调试您的 ExpressJS Web 应用程序
技术要求
您需要一个 IDE,Visual Studio Code,Node.js 和 MongoDB。您还需要安装 Git,以便使用本书的 Git 存储库。
本章的代码文件可以在 GitHub 上找到:
github.com/PacktPublishing/MERN-Quick-Start-Guide/tree/master/Chapter02
查看以下视频以查看代码的运行情况:
介绍
ExpressJS 是首选的 Node.js web 应用程序框架,用于构建强大的 Web 应用程序和 API。
在本章中,配方将专注于构建一个完全功能的 Web 服务器并理解核心基础知识。
在 ExpressJS 中进行路由
路由是指应用程序在通过 HTTP 动词或 HTTP 方法请求资源时如何响应或操作的。
HTTP代表超文本传输协议,它是万维网(WWW)数据通信的基础。WWW 中的所有文档和数据都由统一资源定位符(URL)标识。
HTTP 动词或 HTTP 方法是客户端-服务器模型。通常,Web 浏览器充当客户端,在我们的情况下,ExpressJS 是允许我们创建一个能够理解这些请求的服务器的框架。每个请求都期望发送一个响应给客户端,以识别所请求资源的状态。
请求方法可以是:
-
安全:在服务器上执行只读操作的 HTTP 动词。换句话说,它不会改变服务器状态。例如:
GET。 -
幂等:当发送相同的请求时,具有相同效果的 HTTP 动词。例如,发送
PUT请求以修改用户的名字,如果正确实现,应在发送多个相同请求时对服务器产生相同的效果。所有安全方法也是幂等的。例如,GET,PUT和DELETE方法都是幂等的。 -
可缓存:可以缓存的 HTTP 响应。并非所有方法或 HTTP 动词都可以缓存。只有响应的状态码和用于发出请求的方法都是可缓存的,响应才是可缓存的。例如,GET 方法是可缓存的,以及以下状态码:
200(请求成功),204(无内容),206(部分内容),301(永久移动),404(未找到),405(方法不允许),410(已删除或内容永久从服务器删除),和414(URI 太长)。
准备就绪
理解路由是构建健壮的 RESTful API 中最重要的核心方面之一。
在这个配方中,我们将看到 ExpressJS 如何处理或解释 HTTP 请求。在开始之前,创建一个新的package.json文件,内容如下:
{
"dependencies": {
"express": "4.16.3"
}
}
然后,通过打开终端并运行以下命令来安装依赖项:
npm install
ExpressJS 完成了理解客户端请求的整个工作。请求可能来自浏览器,例如。一旦请求被解释,ExpressJS 将所有信息保存在两个对象中:
-
请求:这包含有关客户端请求的所有数据和信息。例如,ExpressJS 解析 URI 并在 request.query 上提供其参数。
-
Response:这包含将发送给客户端的数据和信息。在发送信息给客户端之前,可以修改响应的标头。
response对象有多种可用于向客户端发送状态代码和数据的方法。例如:response.status(200).send('Some Data!')。
如何做...
Request和Response对象作为参数传递给route方法内定义的路由处理程序。
路由方法
这些派生自 HTTP 动词或 HTTP 方法。路由方法用于定义应用程序对特定 HTTP 动词的响应。
ExpressJS 路由方法的名称与 HTTP 动词相对应。例如:app.get()对应于GET HTTP 动词,或者app.delete()对应于DELETE HTTP 动词。
一个非常基本的路由可以写成以下形式:
-
创建一个名为
1-basic-route.js的新文件 -
首先包括 ExpressJS 库,然后初始化一个新的 ExpressJS 应用程序:
const express = require('express')
const app = express()
- 添加一个新的路由方法来处理路径
"/"的请求。第一个参数指定路径或 URL,下一个参数是路由处理程序。在路由处理程序内部,让我们使用response对象发送状态码200(OK)和文本给客户端:
app.get('/', (request, response, nextHandler) => {
response.status(200).send('Hello from ExpressJS')
})
- 最后,使用
listen方法在端口1337上接受新连接:
app.listen(
1337,
() => console.log('Web Server running on port 1337'),
)
-
保存文件
-
打开终端并运行以下命令:
node 1-basic-route.js
- 在浏览器中打开一个新标签,并访问端口
1337上的localhost以查看结果:
http://localhost:1337/
有关 ExpressJS 支持的 HTTP 方法的更多信息,请访问官方 ExpressJS 网站expressjs.com/en/guide/routing.html#route-methods。
路由处理程序
路由处理程序是回调函数,接受三个参数。第一个是request对象,第二个是response对象,最后一个是callback,它将处理程序传递给链中的下一个请求处理程序。也可以在路由方法内使用多个callback函数。
让我们看一个如何在路由方法内编写路由处理程序的工作示例:
-
创建一个名为
2-route-handlers.js的新文件 -
包括 ExpressJS 库,然后初始化一个新的 ExpressJS 应用程序:
const express = require('express')
const app = express()
- 添加两个路由方法来处理相同路径
"/one"的请求。使用response对象的type方法将发送到客户端的响应的内容类型设置为text/plain。使用write方法向客户端发送部分数据。要完成发送数据,使用响应对象的end方法。调用nextHandler将处理程序传递给链中的第二个处理程序:
app.get('/one', (request, response, nextHandler) => {
response.type('text/plain')
response.write('Hello ')
nextHandler()
})
app.get('/one', (request, response, nextHandler) => {
response.status(200).end('World!')
})
- 添加一个
route方法来处理路径"/two"上的请求。在route方法内定义了两个路由处理程序来处理相同的请求:
app.get('/two',
(request, response, nextHandler) => {
response.type('text/plain')
response.write('Hello ')
nextHandler()
},
(request, response, nextHandler) => {
response.status(200).end('Moon!')
}
)
- 使用
listen方法在端口1337上接受新连接:
app.listen(
1337,
() => console.log('Web Server running on port 1337'),
)
-
保存文件
-
打开终端并运行:
node 2-route-handlers.js
- 要查看结果,请在浏览器中打开一个新标签并访问:
http://localhost:1337/one http://localhost:1337/two
可链接的路由方法
使用app.route(path)可以使路由方法可链接,因为path是为单个位置指定的。这可能是处理多个路由方法时最好的方法,因为除了使代码更易读且不太容易出现拼写错误和冗余之外,还允许同时处理多个路由方法。
-
创建一个名为
3-chainable-routes.js的新文件 -
初始化一个新的 ExpressJS 应用程序:
const express = require('express')
const app = express()
- 使用
route方法链接三个路由方法:
app
.route('/home')
.get((request, response, nextHandler) => {
response.type('text/html')
response.write('<!DOCTYPE html>')
nextHandler()
})
.get((request, response, nextHandler) => {
response.end(`
<html lang="en">
<head>
<meta charset="utf-8">
<title>WebApp powered by ExpressJS</title>
</head>
<body role="application">
<form method="post" action="/home">
<input type="text" />
<button type="submit">Send</button>
</form>
</body>
</html>
`)
})
.post((request, response, nextHandler) => {
response.send('Got it!')
})
- 使用
listen方法在端口1337上接受新连接:
app.listen(
1337,
() => console.log('Web Server running on port 1337'),
)
-
保存文件
-
打开终端并运行:
node 3-chainable-routes.js
- 要查看结果,请在浏览器中打开一个新标签并访问:
http://localhost:1337/home
还有更多...
路由路径可以是字符串或正则表达式。路由路径会使用path-to-regexp NPM 包www.npmjs.com/package/path-to-regexp内部转换为正则表达式。
path-to-regexp在某种程度上帮助你以更易读的方式编写路径正则表达式。例如,考虑以下代码:
app.get(/([a-z]+)-([0-9]+)$/, (request, response, nextHandler) => {
response.send(request.params)
})
// Output: {"0":"abc","1":"12345"} for path /abc-12345
可以这样写:
app.get('/:0-:1', (request, response, nextHandler) => {
response.send(request.params)
})
// Outputs: {"0":"abc","1":"12345"} for /abc-12345
或者更好地说:
app.get('/:id-:tag', (request, response, nextHandler) => {
response.send(request.params)
})
// Outputs: {"id":"abc","tag":"12345"} for /abc-12345
看一下这个表达式:/([a-z]+)-([0-9]+)$/。正则表达式中的括号称为捕获括号;当它们找到匹配时,它们会记住它。在前面的例子中,对于abc-12345,记住了两个字符串,{"0":"abc","1":"12345"}。这是 ExpressJS 找到匹配项、记住其值并将其关联到键的方式:
app.get('/:userId/:action-:where', (request, response, nextHandler) => {
response.send(request.params)
})
// Route path: /123/edit-profile
// Outputs: {"userId":"123","action":"edit","where":"profile"}
模块化路由处理程序
ExpressJS 有一个内置的名为router的类。路由器只是一个允许开发人员编写可挂载和模块化路由处理程序的类。
路由器是 ExpressJS 核心路由系统的一个实例。这意味着 ExpressJS 应用程序的所有路由方法都可用:
const router = express.Router()
router.get('/', (request, response, next) => {
response.send('Hello there!')
})
router.post('/', (request, response, next) => {
response.send('I got your data!')
})
准备工作
在这个示例中,我们将看到如何使用路由器来创建一个模块化应用程序。在开始之前,创建一个新的package.json文件,内容如下:
{
"dependencies": {
"express": "4.16.3"
}
}
然后,通过打开终端并运行以下命令来安装依赖项:
npm install
如何做...
假设你想在 ExpressJS 主应用程序中编写一个模块化的迷你应用程序,可以挂载到任何 URI。你想要能够选择挂载的路径,或者只是想要将相同的路由方法和处理程序挂载到几个其他路径或 URI。
-
创建一个名为
modular-router.js的新文件 -
初始化一个新的 ExpressJS 应用程序:
const express = require('express')
const app = express()
- 为你的迷你应用程序定义一个路由器,并添加一个请求方法来处理
"/home"路径的请求:
const miniapp = express.Router()
miniapp.get('/home', (request, response, next) => {
const url = request.originalUrl
response
.status(200)
.send(`You are visiting /home from ${url}`)
})
- 将你的模块化迷你应用程序挂载到
"/first"路径和"/second"路径:
app.use('/first', miniapp)
app.use('/second', miniapp)
- 监听端口
1337以进行新连接:
app.listen(
1337,
() => console.log('Web Server running on port 1337'),
)
-
保存文件
-
打开终端并运行以下命令:
node modular-router.js
- 要查看结果,请在 Web 浏览器中导航到:
http://localhost:1337/first/home
http://localhost:1337/second/home
你将看到两个不同的输出:
You are visting /home from /first/home
You are visting /home from /second/home
如图所示,路由器被挂载到两个不同的挂载点。路由器通常被称为迷你应用程序,因为它们可以挂载到 ExpressJS 应用程序的特定路由,不仅一次,而且还可以多次挂载到不同的挂载点、路径或 URI。
编写中间件函数
中间件函数主要用于对request和response对象进行更改。它们按顺序执行,但如果一个中间件函数不将控制传递给下一个中间件函数,请求将被搁置。
准备工作
中间件函数具有以下签名:
app.use((request, response, next) => {
next()
})
签名非常类似于编写路由处理程序。实际上,可以为特定的 HTTP 方法和特定的路径路由编写中间件函数,例如:
app.get('/', (request, response, next) => {
next()
})
因此,如果你想知道路由处理程序和中间件函数之间的区别是什么,答案很简单:它们的目的。
如果你正在编写路由处理程序,并且修改了request对象和/或response对象,那么你正在编写中间件函数。
在这个示例中,你将看到如何使用中间件函数来限制访问某些路径或路由,这取决于某个条件。在开始之前,创建一个新的package.json文件,内容如下:
{
"dependencies": {
"express": "4.16.3"
}
}
然后,通过打开终端并运行以下命令来安装依赖项:
npm install
如何做...
我们将编写一个中间件函数,只允许访问根路径"/"当查询参数allowme存在时:
-
创建一个名为
middleware-functions.js的新文件 -
初始化一个新的 ExpressJS 应用程序:
const express = require('express')
const app = express()
- 编写一个中间件函数,将属性
allowed添加到request对象:
app.use((request, response, next) => {
request.allowed = Reflect.has(request.query, 'allowme')
next()
})
- 添加一个请求方法来处理
"/"路径的请求:
app.get('/', (request, response, next) => {
if (request.allowed) {
response.send('Hello secret world!')
} else {
response.send('You are not allowed to enter')
}
})
- 监听端口
1337以进行新连接:
app.listen(
1337,
() => console.log('Web Server running on port 1337'),
)
-
保存文件
-
打开终端并运行:
node middleware-functions.js
- 要查看结果,请在 Web 浏览器中导航到:
http://localhost:1337/
http://localhost:1337/?allowme
工作原理...
就像路由处理程序一样,中间件函数需要将控制权传递给下一个处理程序;否则,我们的应用程序将一直挂起,因为没有向客户端发送数据,连接也没有关闭。
如果在中间件函数内向request或response对象添加了新属性,则下一个处理程序将可以访问这些新属性。就像我们之前编写的代码中,request对象中的allowed property对下一个处理程序是可用的。
编写可配置的中间件函数
编写中间件函数的常见模式是将中间件函数包装在另一个函数中。这样做的结果是可配置的中间件函数。它们也是高阶函数,即返回另一个函数的函数。
const fn = (options) => (response, request, next) => {
next()
}
通常会将对象用作options参数。但是,没有什么能阻止您按照自己的方式进行操作。
准备工作
在这个示例中,您将编写一个可配置的日志记录器中间件函数。在开始之前,请创建一个包含以下内容的新package.json文件:
{
"dependencies": {
"express": "4.16.3"
}
}
然后,通过打开终端并运行来安装依赖项:
npm install
操作步骤...
您的可配置中间件函数将执行的操作很简单:当发出请求时,它将打印状态代码和 URL。
-
创建一个名为
middleware-logger.js的新文件 -
导出一个接受对象作为第一个参数的函数。该函数期望对象具有一个名为
enable的属性,该属性可以是true或false:
const logger = (options) => (request, response, next) => {
if (typeof options === 'object'
&& options !== null
&& options.enable) {
console.log(
'Status Code:', response.statusCode,
'URL:', request.originalUrl,
)
}
next()
}
module.exports = logger
- 保存文件
让我们来测试一下...
我们的可配置中间件函数本身并不实用。创建一个简单的 ExpressJS 应用程序来查看我们的中间件实际工作:
-
创建一个名为
configurable-middleware-test.js的新文件 -
包含我们的
middleware-logger.js模块并初始化一个新的 ExpressJS 应用程序:
const express = require('express')
const loggerMiddleware = require('./middleware-logger')
const app = express()
- 使用
use方法包含我们的可配置中间件函数。当enable属性设置为true时,您的日志记录器将工作,并将每个请求的状态代码和 URL 记录到终端:
app.use(loggerMiddleware({
enable: true,
}))
- 监听端口
1337以获取新的连接:
app.listen(
1337,
() => console.log('Web Server running on port 1337'),
)
-
保存文件
-
打开终端并运行:
node middleware-logger-test.js
- 在浏览器中导航到:
http://localhost:1337/hello?world
- 终端应显示:
Status Code: 200 URL: /hello?world
还有更多...
如果您想尝试,可以将可配置中间件测试应用程序的enable属性设置为false。不应显示任何日志。
通常,您会希望在生产环境中禁用日志记录,因为此操作可能会影响性能。
禁用所有日志记录的替代方法是使用其他库来执行此任务,而不是使用console。有一些库允许您设置不同级别的日志记录,例如:
-
Debug 模块:
www.npmjs.com/package/debug -
Winston:
www.npmjs.com/package/winston
日志记录有几个原因很有用。主要原因是:
-
它检查您的服务是否正常运行,例如,检查您的应用程序是否连接到 MongoDB。
-
它可以发现错误和漏洞。
-
它有助于更好地了解应用程序的工作原理。例如,如果您有一个模块化应用程序,您可以看到它在包含在其他应用程序中时是如何集成的。
编写路由器级中间件函数
路由级中间件函数只在路由器内执行。它们通常在仅将中间件应用于挂载点或特定路径时使用。
准备工作
在这个示例中,您将创建一个小型的日志记录器路由器级中间件函数,它将仅记录挂载或位于路由器挂载路径中的路径的请求。在开始之前,请创建一个包含以下内容的新package.json文件:
{
"dependencies": {
"express": "4.16.3"
}
}
然后,通过打开终端并运行来安装依赖项:
npm install
操作步骤...
-
创建一个名为
router-level.js的新文件 -
初始化一个新的 ExpressJS 应用程序并定义一个路由器:
const express = require('express')
const app = express()
const router = express.Router()
- 定义我们的日志记录中间件函数:
router.use((request, response, next) => {
console.log('URL:', request.originalUrl)
next()
})
- 将路由器挂载到路径
"/router"
app.use('/router', router)
- 监听端口
1337以获取新的连接:
app.listen(
1337,
() => console.log('Web Server running on port 1337'),
)
-
保存文件
-
打开终端并运行:
node router-level.js
- 在您的网络浏览器中导航到:
http://localhost:1337/router/example
- 终端应显示:
URL: /router/example
- 然后,在您的网络浏览器中导航到:
http://localhost:1337/example
- 终端中不应显示任何日志
还有更多...
通过调用next('router')可以将控制权传递回到路由器之外的下一个中间件函数或路由方法。
router.use((request, response, next) => {
next('route')
})
例如,通过创建一个期望接收用户 ID 作为查询参数的路由器。当未提供用户 ID 时,可以使用next('router')函数来退出路由器或将控制权传递给路由器之外的下一个中间件函数。路由器之外的下一个中间件函数可以用来在路由器将控制权传递给它时显示其他信息。例如:
-
创建一个名为
router-level-control.js的新文件 -
初始化一个新的 ExpressJS 应用程序:
const express = require('express')
const app = express()
- 定义一个新的路由器:
const router = express.Router()
- 在路由器内部定义我们的日志中间件函数:
router.use((request, response, next) => {
if (!request.query.id) {
next('router') // Next, out of Router
} else {
next() // Next, in Router
}
})
- 添加一个路由方法来处理
"/"路径的GET请求,只有在中间件函数将控制权传递给它时才会执行:
router.get('/', (request, response, next) => {
const id = request.query.id
response.send(`You specified a user ID => ${id}`)
})
- 在路由器之外添加一个路由方法来处理
"/"路径的GET请求。但是,将路由器作为第二个参数包含在路由处理程序中,并添加另一个路由处理程序来处理相同的请求,只有当路由器将控制权传递给它时才会执行:
app.get('/', router, (request, response, next) => {
response
.status(400)
.send('A user ID needs to be specified')
})
- 监听端口
1337以进行新连接:
app.listen(
1337,
() => console.log('Web Server running on port 1337'),
)
-
保存文件
-
打开终端并运行:
node router-level-control.js
- 要查看结果,请在浏览器中导航到:
http://localhost:1337/
http://localhost:1337/?id=7331
它是如何工作的...
当导航到第一个 URL(http://localhost:1337/)时,将显示以下消息:
A user ID needs to be specified
这是因为路由器中的中间件函数检查查询中是否提供了id,因为没有提供,它将控制权传递给路由器之外的下一个处理程序,使用next('router')。
另一方面,当导航到第二个 URL(localhost:1337/?id=7331)时,将显示以下消息:
You specified a user ID => 7331
这是因为在查询中提供了id,路由器中的中间件函数将控制权传递给路由器内部的下一个处理程序,使用next()。
编写错误处理程序中间件函数
ExpressJS 已经默认包含了一个内置的错误处理程序,在所有中间件和路由处理程序结束时执行。
内置错误处理程序可以被触发的方式有几种。一种是在路由处理程序内部发生错误时隐式触发。例如:
app.get('/', (request, response, next) => {
throw new Error('Oh no!, something went wrong!')
})
另一种触发内置错误处理程序的方式是显式地将error作为参数传递给next(error)。例如:
app.get('/', (request, response, next) => {
try {
throw new Error('Oh no!, something went wrong!')
} catch (error) {
next(error)
}
})
堆栈跟踪将显示在客户端上。如果NODE_ENV设置为生产模式,则不包括堆栈跟踪。
也可以编写一个自定义错误处理程序中间件函数,它看起来与路由处理程序几乎相同,唯一的区别是错误处理程序函数中间件期望接收四个参数:
app.use((error, request, response, next) => {
next(error)
})
请注意,next(error)是可选的。这意味着,如果指定了next(error),它将把控制权转移到下一个错误处理程序。如果没有定义其他错误处理程序,那么控制权将传递给内置的错误处理程序。
准备工作
在这个示例中,我们将看到如何创建一个自定义的错误处理程序。在开始之前,请创建一个新的package.json文件,内容如下:
{
"dependencies": {
"express": "4.16.3"
}
}
然后,通过打开终端并运行来安装依赖:
npm install
如何做...
您将构建一个自定义的错误处理程序,将错误消息发送给客户端。
-
创建一个名为
custom-error-handler.js的新文件 -
包含 ExpressJS 库,然后初始化一个新的 ExpressJS 应用程序:
const express = require('express')
const app = express()
- 定义一个新的路由方法来处理
"/"路径的GET请求,并且每次都抛出一个错误:
app.get('/', (request, response, next) => {
try {
throw new Error('Oh no!, something went wrong!')
} catch (err) {
next(err)
}
})
- 定义一个自定义错误处理程序中间件函数,将错误消息发送回客户端的浏览器:
app.use((error, request, response, next) => {
response.end(error.message)
})
- 监听端口
1337以进行新连接:
app.listen(
1337,
() => console.log('Web Server running on port 1337'),
)
-
保存文件
-
打开终端并运行:
node custom-error-handler.js
- 要查看结果,请在您的网络浏览器中导航到:
http://localhost:1337/
使用 ExpressJS 内置的中间件函数来提供静态资产
在 ExpressJS 的 4.x 版本之前,它依赖于 ConnectJS,ConnectJS 是一个 HTTP 服务器框架github.com/senchalabs/connect。实际上,大多数为 ConnectJS 编写的中间件也受到 ExpressJS 的支持。
从 ExpressJS 的 4.x 版本开始,它不再依赖于 ConnectJS,并且所有先前内置的中间件函数都已移动到单独的模块中expressjs.com/en/resources/middleware.html。
ExpressJS 4.x 和更新版本只包括两个内置的中间件函数。第一个已经看到了:内置的错误处理程序中间件函数。第二个是express.static中间件函数,负责提供静态资产。
express.static中间件函数基于serve-static模块expressjs.com/en/resources/middleware/serve-static.html。
express.static和serve-static之间的主要区别是第二个可以在 ExpressJS 之外使用。
准备就绪
在这个示例中,您将看到如何构建一个 Web 应用程序,该应用程序将在特定路径中提供静态资产。在开始之前,创建一个新的package.json文件,内容如下:
{
"dependencies": {
"express": "4.16.3"
}
}
然后,通过打开终端并运行以下命令来安装依赖项:
npm install
如何做...
-
创建一个名为
public的新目录 -
进入新的
public目录 -
创建一个名为
index.html的新文件 -
添加以下代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Simple Web Application</title>
</head>
<body>
<section role="application">
<h1>Welcome Home!</h1>
</section>
</body>
</html>
-
保存文件
-
从
public目录中导航回去 -
创建一个名为
serve-static-assets.js的新文件 -
添加以下代码。初始化一个新的 ExpressJS 应用程序:
const express = require('express')
const path = require('path')
const app = express()
- 包括
express.static可配置的中间件函数,并传递/public目录的路径,其中index.html文件位于其中:
const publicDir = path.join(__dirname, './public')
app.use('/', express.static(publicDir))
- 监听端口
1337以进行新连接:
app.listen(
1337,
() => console.log('Web Server running on port 1337'),
)
-
保存文件
-
打开终端并运行:
node serve-static-assets.js
- 要查看结果,在浏览器中导航到:
http://localhost:1337/index.html
它是如何工作的...
我们的index.html文件将被显示,因为我们指定了"/"作为查找资产的根目录。
尝试将路径从"/"更改为"/public"。然后,您将能够看到index.html文件和其他要包含在/public目录中的文件,可以在http://localhost:1337/public/[fileName]下访问。
还有更多...
假设您有一个大型项目,其中包含数十个静态文件,包括图像、字体文件和 PDF 文档(涉及隐私和法律事务等)。您决定要将它们保存在单独的文件中,但又不想更改挂载路径或 URI。它们可以在/public下提供,但它们将存在于项目目录中的单独目录中:
首先,让我们创建第一个包含一个名为index.html的单个文件的public目录:
-
如果您在上一个示例中没有创建
public目录,请创建一个名为public的新目录 -
进入新的
public目录 -
创建一个名为
index.html的新文件 -
添加以下代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Simple Web Application</title>
</head>
<body>
<section role="application">
<h1>Welcome Home!</h1>
</section>
</body>
</html>
- 保存文件
现在,让我们创建一个包含另一个名为second.html的文件的第二个公共目录:
-
从
public目录中移出 -
创建一个名为
another-public的新目录 -
进入新的
another-public目录 -
创建一个名为
second.html的新空文件 -
添加以下代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Simple Web Application</title>
</head>
<body>
<section role="application">
Welcome to Second Page!
</section>
</body>
</html>
- 保存文件
正如你所看到的,这两个文件存在于不同的目录中。为了在一个挂载点下提供这些文件:
-
从
another-public目录中移出 -
创建一个名为
router-serve-static.js的新文件 -
包括 ExpressJS 和 path 库。然后,初始化一个新的 ExpressJS 应用程序:
const express = require('express')
const path = require('path')
const app = express()
- 定义一个路由器:
const staticRouter = express.Router()
- 使用
express.static可配置的中间件函数来包含public和another-public两个目录:
const assets = {
first: path.join(__dirname, './public'),
second: path.join(__dirname, './another-public')
}
staticRouter
.use(express.static(assets.first))
.use(express.static(assets.second))
- 将路由器挂载到
"/"路径:
app.use('/', staticRouter)
- 监听端口
1337以进行新连接:
app.listen(
1337,
() => console.log('Web Server running on port 1337'),
)
-
保存文件
-
打开终端并运行:
node router-serve-static.js
- 要查看结果,在浏览器中导航到:
http://localhost:1337/index.html
http://localhost:1337/second.html
- 在不同位置的两个不同文件在一个路径下提供服务
如果在不同目录下存在两个或更多同名文件,只会显示找到的第一个文件在客户端上。
解析 HTTP 请求主体
body-parser 是一个中间件函数,用于解析传入的请求主体,并将其作为 request.body 在 request 对象中可用 expressjs.com/en/resources/middleware/body-parser.html。
该模块允许应用程序解析传入请求为:
-
JSON
-
文本
-
原始(缓冲区原始传入数据)
-
URL 编码表单
当传入请求被压缩时,该模块支持对 gzip 和 deflate 编码的自动解压缩。
做好准备
在这个配方中,您将看到如何使用 body-parser NPM 模块来解析以两种不同方式编码的两个不同表单发送的内容主体。在开始之前,创建一个新的 package.json 文件,内容如下:
{
"dependencies": {
"body-parser": "1.18.2",
"express": "4.16.3"
}
}
然后,通过打开终端并运行来安装依赖项:
npm install
如何做到这一点...
两个表单将显示给用户,它们都将以两种不同的方式编码发送数据到我们的 Web 服务器应用程序。第一个是 URL 编码表单,而另一个将以纯文本形式编码其主体。
-
创建一个名为
parse-form.js的文件 -
包括
body-parserNPM 模块。然后,初始化一个新的 ExpressJS 应用程序:
const express = require('express')
const bodyParser = require('body-parser')
const app = express()
- 包括
body-parser中间件函数来处理 URL 编码请求和纯文本请求:
app.use(bodyParser.urlencoded({ extended: true }))
app.use(bodyParser.text())
- 添加一个新的路由方法来处理
"/"路径的GET请求。提供使用不同编码提交数据的两个表单的 HTML 内容:
app.get('/', (request, response, next) => {
response.send(`
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>WebApp powered by ExpressJS</title>
</head>
<body>
<div role="application">
<form method="post" action="/setdata">
<input name="urlencoded" type="text" />
<button type="submit">Send</button>
</form>
<form method="post" action="/setdata"
enctype="text/plain">
<input name="txtencoded" type="text" />
<button type="submit">Send</button>
</form>
</div>
</body>
</html>
`)
})
- 添加一个新的路由方法来处理
"/setdata"路径的POST请求。在终端上显示request.body的内容:
app.post('/setdata', (request, response, next) => {
console.log(request.body)
response.end()
})
- 监听端口
1337,等待新连接:
app.listen(
1337,
() => console.log('Web Server running on port 1337'),
)
-
保存文件
-
打开一个终端并运行:
node parse-form.js
- 在您的 Web 浏览器中,导航到:
http://localhost:1337/
-
在第一个输入框中填写任何数据并提交表单:
-
在您的 Web 浏览器中,导航回:
http://localhost:1337/
-
在第二个输入框中填写任何数据并提交表单:
-
检查终端中的输出
它是如何工作的...
终端输出类似于:
{ 'urlencoded': 'Example' }
txtencoded=Example
上面使用了两个解析器:
-
第一个
bodyParser.urlencoded()解析传入的multipart/form-data编码类型的请求。结果以 Object 形式在request.body中可用 -
第二个
bodyParser.text()解析传入的text/plain编码类型的请求。结果以 String 形式在request.body中可用
压缩 HTTP 响应
compression 是一个中间件函数,用于压缩将发送到客户端的响应主体。该模块使用支持以下内容编码机制的 zlib 模块 nodejs.org/api/zlib.html。
-
gzip
-
deflate
Accept-Encoding HTTP 头用于确定客户端(例如 Web 浏览器)支持哪种内容编码机制,而 Content-Encoding HTTP 头用于告诉客户端响应主体应用了哪种内容编码机制。
compression 是一个可配置的中间件函数。它接受一个 options 对象作为第一个参数,以定义中间件的特定行为,并且还可以传递 zlib 选项。
做好准备
在这个配方中,我们将看到如何配置和使用 compression NPM 模块来压缩发送到客户端的请求主体。在开始之前,创建一个新的 package.json 文件,内容如下:
{
"dependencies": {
"compression": "1.7.2",
"express": "4.16.3"
}
}
然后,通过打开终端并运行来安装依赖项:
npm install
如何做到这一点...
-
创建一个名为
compress-site.js的新文件 -
包括
compressionNPM 模块。然后,初始化一个新的 ExpressJS 应用程序:
const express = require('express')
const compression = require('compression')
const app = express()
- 包括
compression中间件函数。指定压缩的level为9(最佳压缩),threshold或者响应主体应该考虑压缩的最小大小为0字节:
app.use(compression({ level: 9, threshold: 0 }))
- 定义一个路由方法来处理
GET请求的路径"/",它将提供我们希望被压缩的示例 HTML 内容,并将打印客户端接受的编码:
app.get('/', (request, response, next) => {
response.send(`
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>WebApp powered by ExpressJS</title>
</head>
<body>
<section role="application">
<h1>Hello! this page is compressed!</h1>
</section>
</body>
</html>
`)
console.log(request.acceptsEncodings())
})
- 监听端口
1337以进行新连接:
app.listen(
1337,
() => console.log('Web Server running on port 1337'),
)
-
保存文件
-
打开终端并运行:
node compress-site.js
- 在您的浏览器中,导航到:
http://localhost:1337/
它是如何工作的...
终端的输出将显示客户端(例如 Web 浏览器)支持的内容编码机制。它可能看起来像这样:
[ 'gzip', 'deflate', 'sdch', 'br', 'identity' ]
客户端发送的内容编码机制由compression内部使用,以了解是否支持压缩。如果不支持压缩,则响应主体不会被压缩。
如果打开 Chrome Dev Tools 或类似工具并分析所做的请求,则服务器发送的Content-Encoding标头指示compression使用的内容编码机制。
Chrome Dev Tools | Network Tab 显示响应标头
compression库将Content-Encoding标头设置为用于压缩响应主体的编码机制。
threshold选项默认设置为 1 KB,这意味着如果响应大小低于指定的字节数,则不会被压缩。将其设置为 0 或false,即使大小低于 1 KB,也会压缩响应。
使用 HTTP 请求记录器
如前所述,编写请求记录器很简单。但是,编写我们自己的可能需要花费宝贵的时间。幸运的是,还有其他几种选择。例如,一个非常流行的 HTTP 请求记录器是 morgan expressjs.com/en/resources/middleware/morgan.html。
morgan是一个可配置的中间件函数,接受两个参数format和options,用于指定日志显示的格式以及需要显示的信息类型。
有几种预定义的格式:
-
tiny:最小输出 -
short:与 tiny 相同,包括远程 IP 地址 -
common:标准 Apache 日志输出 -
combined:标准 Apache 组合日志输出 -
dev:显示与微格式相同的信息。但是,响应状态是有颜色的。
准备工作
创建一个新的package.json文件,内容如下:
{
"dependencies": {
"express": "4.16.3",
"morgan": "1.9.0"
}
}
然后,通过打开终端并运行以下命令来安装依赖项:
npm install
如何做...
让我们构建一个可工作的示例。我们将包括morgan可配置的中间件函数,使用dev格式显示每个请求的信息。
-
创建一个名为
morgan-logger.js的新文件 -
初始化一个新的 ExpressJS 应用程序:
const express = require('express')
const morgan = require('morgan')
const app = express()
- 包括
morgan可配置的中间件。将'dev'作为我们将使用的格式作为中间件函数的第一个参数传递:
app.use(morgan('dev'))
- 定义一个路由方法来处理所有
GET请求:
app.get('*', (request, response, next) => {
response.send('Hello Morgan!')
})
- 监听端口
1337以进行新连接:
app.listen(
1337,
() => console.log('Web Server running on port 1337'),
)
-
保存文件
-
打开终端并运行:
node morgan-logger.js
- 要在终端中查看结果,请在 Web 浏览器中导航到:
http://localhost:1337/
http://localhost:1337/example
管理和创建虚拟域
使用 ExpressJS 管理虚拟域名非常容易。假设您有两个或更多子域,并且希望提供两个不同的 Web 应用程序。但是,您不希望为每个子域创建不同的 Web 服务器应用程序。在这种情况下,ExpressJS 允许开发人员在单个 Web 服务器应用程序中使用vhostexpressjs.com/en/resources/middleware/vhost.html管理虚拟域。
vhost是一个可配置的中间件函数,接受两个参数。第一个是hostname。第二个参数是当hostname匹配时将被调用的请求处理程序。
hostname遵循与路由路径相同的规则。它们可以是字符串,也可以是正则表达式。
准备工作
创建一个新的package.json文件,内容如下:
{
"dependencies": {
"express": "4.16.3",
"vhost": "3.0.2"
}
}
然后,通过打开终端并运行以下命令来安装依赖项:
npm install
如何做...
使用路由器构建两个迷你应用程序,这两个应用程序将在两个不同的子域中提供服务:
-
创建一个名为
virtual-domains.js的新文件 -
包括
vhostNPM 模块。然后,初始化一个新的 ExpressJS 应用程序:
const express = require('express')
const vhost = require('vhost')
const app = express()
- 定义我们将用来构建两个迷你应用程序的两个路由器:
const app1 = express.Router()
const app2 = express.Router()
- 在第一个路由器中添加一个路由方法来处理
"/"路径的GET请求:
app1.get('/', (request, response, next) => {
response.send('This is the main application.')
})
- 在第二个路由器中添加一个路由方法来处理
"/"路径的GET请求:
app2.get('/', (request, response, next) => {
response.send('This is a second application.')
})
- 将我们的路由器挂载到我们的 ExpressJS 应用程序上。在
localhost下提供第一个应用程序,在second.localhost下提供第二个应用程序:
app.use(vhost('localhost', app1))
app.use(vhost('second.localhost', app2))
- 监听端口
1337以获取新连接:
app.listen(
1337,
() => console.log('Web Server running on port 1337'),
)
-
保存文件
-
打开终端并运行:
node virtual-domains.js
- 要查看结果,请在您的 Web 浏览器中导航到:
http://localhost:1337/
http://second.localhost:1337/
还有更多...
vhost向request对象添加了一个vhost 对象,其中包含完整的主机名(显示主机名和端口)、主机名(不包括端口)和匹配字符串。这样可以更好地控制如何处理虚拟域。
例如,我们可以编写一个允许用户使用他们的名字拥有自己子域的应用程序:
-
创建一个名为
user-subdomains.js的新文件 -
包括
vhostNPM 模块。然后,初始化一个新的 ExpressJS 应用程序:
const express = require('express')
const vhost = require('vhost')
const app = express()
- 定义一个新的路由器。然后,在
"/"路径上添加一个路由方法来处理GET请求。使用vhost对象来访问子域的数组:
const users = express.Router()
users.get('/', (request, response, next) => {
const username = request
.vhost[0]
.split('-')
.map(name => (
name[0].toUpperCase() +
name.slice(1)
))
.join(' ')
response.send(`Hello, ${username}`)
})
- 挂载路由器:
app.use(vhost('*.localhost', users))
- 监听端口
1337以获取新连接:
app.listen(
1337,
() => console.log('Web Server running on port 1337'),
)
-
保存文件
-
打开终端并运行:
node user-subdomains.js
- 要查看结果,请在您的 Web 浏览器中导航到:
http://john-smith.localhost:1337/
http://jx-huang.localhost:1337/
http://batman.localhost:1337/
使用 Helmet 保护 ExpressJS Web 应用程序
Helmet允许保护 Web 服务器应用程序免受常见攻击,例如跨站脚本(XSS)、不安全的请求和点击劫持。
Helmet 是一组 12 个中间件函数,允许您设置特定的 HTTP 头:
-
内容安全策略(CSP):这是一种有效的方法,可以列出允许在您的 Web 应用程序中使用什么样的外部资源,例如 JavaScript、CSS 和图像。 -
证书透明度:这是一种提供特定域或特定域发行的证书更透明的方式sites.google.com/a/chromium.org/dev/Home/chromium-security/certificate-transparency。 -
DNS 预取控制:这告诉浏览器是否应该对尚未加载的资源(例如链接)执行域名解析(DNS)。 -
Frameguard:这有助于防止点击劫持,告诉浏览器不要允许将您的 Web 应用程序放在iframe中。 -
隐藏 Powered-By:这只是隐藏X-Powered-By头,表示不显示服务器的技术。ExpressJS 默认将此头设置为"Express"。 -
HTTP 公钥固定:这有助于防止中间人攻击,将您的 Web 应用程序的公钥固定到Public-Key-Pins头。 -
HTTP 严格传输安全性:这告诉浏览器严格坚持您的 Web 应用程序的 HTTPs 版本。 -
IE 不打开:这可以防止 Internet Explorer 在您的站点上执行不受信任的下载或 HTML 文件,从而防止恶意脚本的注入。 -
无缓存:这告诉浏览器禁用浏览器缓存。 -
不嗅探 MIME 类型:这会强制浏览器禁用 MIME 嗅探或猜测所提供文件的内容类型。 -
引荐政策:引荐头提供了关于请求来源的数据。它允许开发人员禁用它,或者设置更严格的策略来设置引荐头。 -
XSS 过滤器:这通过设置X-XSS-Protection头来防止反射型跨站脚本(XSS)攻击。
准备就绪
在本教程中,我们将使用 Helmet 提供的大多数中间件函数来保护我们的 ExpressJS Web 应用程序免受常见攻击。在开始之前,请创建一个新的package.json文件,内容如下:
{
"dependencies": {
"body-parser": "1.18.2",
"express": "4.16.3",
"helmet": "3.12.0",
"uuid": "3.2.1"
}
}
然后,通过打开终端并运行来安装依赖项:
npm install
如何做...
-
创建一个名为
secure-helmet.js的新文件 -
包括 ExpressJS、helmet 和 body NPM 模块:
const express = require('express')
const helmet = require('helmet')
const bodyParser = require('body-parser')
const uuid = require('uuid/v1')
const app = express()
- 生成一个随机 ID,该 ID 将用于
nonce,nonce是一个 HTML 属性,用于白名单内联执行 HTML 代码中允许执行的脚本或样式:
const suid = uuid()
- 使用 body parser 来解析
json和application/csp-report内容类型的 JSON 请求主体。application/csp-report是一个包含json类型的 JSON 请求主体的内容类型,当违反一个或多个 CSP 规则时,浏览器会发送该内容类型:
app.use(bodyParser.json({
type: ['json', 'application/csp-report'],
}))
- 使用
Content Security Policy中间件函数来定义指令。defaultSrc指定可以从哪里加载资源。self选项指定仅从您自己的域加载资源。我们将使用none,这意味着不会加载任何资源。但是,因为我们正在列入白名单scriptSrc,我们将能够加载 Javascript 脚本,但只有那些具有我们将指定的nonce的脚本。reportUri用于告诉浏览器发送我们的Content Security Policy的违规报告的位置:
app.use(helmet.contentSecurityPolicy({
directives: {
// By default do not allow unless whitelisted
defaultSrc: [`'none'`],
// Only allow scripts with this nonce
scriptSrc: [`'nonce-${suid}'`],
reportUri: '/csp-violation',
}
}))
- 添加一个路由方法来处理路径
"/csp-violation"的POST请求,以接收来自客户端的违规报告:
app.post('/csp-violation', (request, response, next) => {
const { body } = request
if (body) {
console.log('CSP Report Violation:')
console.dir(body, { colors: true, depth: 5 })
}
response.status(204).send()
})
- 使用
DNS Prefetch Control中间件禁用资源预取:
app.use(helmet.dnsPrefetchControl({ allow: false }))
- 使用
Frameguard中间件函数禁用您的应用程序在iframe中加载:
app.use(helmet.frameguard({ action: 'deny' }))
- 使用
hidePoweredBy中间件函数替换X-Powered-By标头并设置一个虚假的标头:
app.use(helmet.hidePoweredBy({
setTo: 'Django/1.2.1 SVN-13336',
}))
- 使用
ieNoOpen中间件函数禁用 IE 不受信任的执行:
app.use(helmet.ieNoOpen())
- 使用
noSniff中间件函数禁用 MIME 类型猜测:
app.use(helmet.noSniff())
- 使用
referrerPolicy中间件函数使标头仅对我们的域名可用:
app.use(helmet.referrerPolicy({ policy: 'same-origin' }))
- 使用
xssFilter中间件函数防止反射型 XSS 攻击:
app.use(helmet.xssFilter())
- 添加一个路由方法来处理路径
"/"上的GET请求,并提供一个样本 HTML 内容,该内容将尝试从外部来源加载图像,尝试执行内联脚本,并尝试加载未指定nonce的外部脚本。我们还将添加一个有效的脚本,因为将指定nonce属性允许执行:
app.get('/', (request, response, next) => {
response.send(`
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Web App</title>
</head>
<body>
<span id="txtlog"></span>
<img alt="Evil Picture" src="img/pic.jpg">
<script>
alert('This does not get executed!')
</script>
<script src="img/evilstuff.js"></script>
<script nonce="${suid}">
document.getElementById('txtlog')
.innerText = 'Hello World!'
</script>
</body>
</html>
`)
})
- 在端口
1337上监听新连接:
app.listen(
1337,
() => console.log('Web Server running on port 1337'),
)
-
保存文件
-
打开终端并运行:
node secure-helmet.js
- 要查看结果,在您的网络浏览器中导航到:
http://localhost:1337/
工作原理...
一切都很简单直接地使用Helmet。您通过选择和应用特定的Helmet中间件函数来指定要实施的安全措施,Helmet将设置正确的标头,然后发送给客户端。
在客户端(网络浏览器)中,一切都是自动的。网络浏览器负责解释服务器发送的标头并应用安全策略。这也意味着旧的浏览器可能无法支持或理解所有这些标头。也就是说,如果您考虑应用程序的安全性,那么没有太多好的理由要支持旧的网络浏览器。
例如,如果您使用 Chrome,您应该能够在控制台中看到类似于以下内容:
Chrome Dev Tools | 控制台显示 CSP 违规
- 在终端中,您应该能够看到类似于浏览器发送的以下输出:
CSP Report Violation: {
"csp-report": {
"document-uri": "http://localhost:1337/",
"referrer": "",
"violated-directive": "img-src",
"effective-directive": "img-src",
"original-policy": "default-src 'none'; script-src
'[nonce]'; report-uri /csp-violation",
"disposition": "enforce",
"blocked-uri": "http://evil.com/pic.jpg",
"line-number": 9,
"source-file": "http://localhost:1337/",
"status-code": 200
}
}
CSP Report Violation: {
"csp-report": {
"document-uri": "http://localhost:1337/",
"referrer": "",
"violated-directive": "script-src",
"effective-directive": "script-src",
"original-policy": "default-src 'none'; script-src
'[nonce]'; report-uri /csp-violation",
"disposition": "enforce",
"blocked-uri": "inline",
"line-number": 9,
"status-code": 200
}
}
CSP Report Violation: {
"csp-report": {
"document-uri": "http://localhost:1337/",
"referrer": "",
"violated-directive": "script-src",
"effective-directive": "script-src",
"original-policy": "default-src 'none'; script-src
'[nonce]'; report-uri /csp-violation",
"disposition": "enforce",
"blocked-uri": "http://evil.com/evilstuff.js",
"status-code": 200
}
}
使用模板引擎
模板引擎允许您以更方便的方式生成 HTML 代码。模板或视图可以以任何格式编写,由模板引擎解释,将变量替换为其他值,最终转换为 HTML。
ExpressJS 的官方网站上提供了一个可以与 ExpressJS 直接使用的大量模板引擎列表,网址为github.com/expressjs/express/wiki#template-engines。
准备工作
在这个教程中,您将构建自己的模板引擎。要开发和使用自己的模板引擎,您首先需要注册它,然后定义视图所在的路径,最后告诉 ExpressJS 使用哪个模板引擎。
app.engine('...', (path, options, callback) => { ... });
app.set('views', './');
app.set('view engine', '...');
在开始之前,创建一个新的package.json文件,内容如下:
{
"dependencies": {
"express": "4.16.3"
}
}
然后,通过打开终端并运行来安装依赖项:
npm install
操作步骤...
首先创建一个包含简单模板的views目录:
-
创建一个名为
views的新目录 -
在我们的
views目录中创建一个名为home.tpl的新文件 -
添加以下代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Using Template Engines</title>
</head>
<body>
<section role="application">
<h1>%title%</h1>
<p>%description%</p>
</section>
</body>
</html>
- 保存文件
现在,创建一个新的模板引擎,将之前的模板转换为 HTML,并用提供的选项替换%[var]%:
-
移出
views目录 -
创建一个名为
my-template-engine.js的新文件 -
包括 ExpressJS 和 fs(文件系统)库。然后,初始化一个新的 ExpressJS 应用程序:
const express = require('express')
const fs = require('fs')
const app = express()
- 使用
engine方法注册一个名为tpl的新模板引擎。我们将读取文件内容,并用options对象中指定的内容替换%[var]%:
app.engine('tpl', (filepath, options, callback) => {
fs.readFile(filepath, (err, data) => {
if (err) {
return callback(err)
}
const content = data
.toString()
.replace(/%[a-z]+%/gi, (match) => {
const variable = match.replace(/%/g, '')
if (Reflect.has(options, variable)) {
return options[variable]
}
return match
})
return callback(null, content)
})
})
- 定义视图所在的路径。我们的模板位于
views目录中:
app.set('views', './views')
- 告诉 ExpressJS 使用我们的模板引擎:
app.set('view engine', 'tpl')
- 添加一个路由方法来处理
"/"路径的GET请求,并渲染我们的主页模板。提供title和description选项,它们将替换模板中的%title%和%description%:
app.get('/', (request, response, next) => {
response.render('home', {
title: 'Hello',
description: 'World!',
})
})
- 监听端口
1337以获取新连接:
app.listen(
1337,
() => console.log('Web Server running on port 1337'),
)
-
保存文件
-
打开终端并运行:
node my-template-engine.js
- 在您的浏览器中,导航到:
http://localhost:1337/
我们刚刚编写的模板引擎不会转义 HTML 字符。这意味着,如果用来自客户端的数据替换这些属性,就应该小心,因为它可能容易受到 XSS 攻击。您可能希望使用官方 ExpressJS 网站上更安全的模板引擎。
调试您的 ExpressJS Web 应用程序
关于 Web 应用程序整个周期的 ExpressJS 上的调试信息非常简单。ExpressJS 在内部使用debug NPM 模块记录信息。与console.log不同,debug日志在生产模式下可以轻松禁用。
准备工作
在这个教程中,您将学习如何调试您的 ExpressJS Web 应用程序。在开始之前,创建一个新的package.json文件,内容如下:
{
"dependencies": {
"debug": "3.1.0",
"express": "4.16.3"
}
}
然后,通过打开终端并运行来安装依赖项:
npm install
操作步骤...
-
创建一个名为
debugging.js的新文件 -
初始化一个新的 ExpressJS 应用程序:
const express = require('express')
const app = express()
- 添加一个路由方法来处理任何路径的
GET请求:
app.get('*', (request, response, next) => {
response.send('Hello there!')
})
- 监听端口
1337以获取新连接:
app.listen(
1337,
() => console.log('Web Server running on port 1337'),
)
-
保存文件
-
打开终端并运行:
-
在 Windows 上:
set DEBUG=express:* node debugging.js
- 在 Linux 或 MacOS 上:
DEBUG=express:* node debugging.js
- 在您的 Web 浏览器中,导航到:
http://localhost:1337/
- 观察您终端的输出以查看日志
工作原理...
DEBUG环境变量用于告诉debug模块调试 ExpressJS 应用程序的哪些部分。在我们之前编写的代码中,express:*告诉调试模块记录与 express 应用程序相关的所有内容。
我们可以使用DEBUG=express:router来显示与 ExpressJS 的路由相关的日志。
还有更多...
您可以在自己的项目中使用 debug NPM 模块。例如:
-
创建一个名为
myapp.js的新文件 -
添加以下代码:
const express = require('express')
const app = express()
const debug = require('debug')('myapp')
app.get('*', (request, response, next) => {
debug('Request:', request.originalUrl)
response.send('Hello there!')
})
app.listen(
1337,
() => console.log('Web Server running on port 1337'),
)
-
保存文件
-
打开终端并运行:
-
在 Windows 上:
set DEBUG=myapp node myapp.js
- 在 Linux 和 MacOS 上:
DEBUG=myapp node myapp.js
-
在您的 Web 浏览器中,导航到:
-
观察您的终端输出。它会显示类似以下内容:
Web Server running on port 1337
myapp Request: / +0ms
您可以使用DEBUG环境变量来告诉debug模块不仅显示myapp的日志,还显示 ExpressJS 的日志,如下所示:
在 Windows 上:
set DEBUG=myapp,express:* node myapp.js
在 Linux 和 MacOS 上:
DEBUG=myapp,express:* node myapp.js