跳到内容

a

Node.js 与 Express

在这一部分中,我们的重点转向后端:也就是说,在堆栈的服务器端实现功能。

我们将在 NodeJS 的基础上建立我们的后端,这是一个基于谷歌 Chrome V8 JavaScript 引擎的 JavaScript 运行时间。

本课程材料是用 Node.js 的 16.13.2 版本编写的。请确保你的 Node 版本至少和教材中使用的版本一样新(你可以通过在命令行中运行 node -v 来检查版本)。

正如在 第一章节 中提到的,浏览器还不支持 JavaScript 的最新功能,这就是为什么在浏览器中运行的代码必须用例如 babel 进行 转写 。在后端运行的 JavaScript 的情况则不同。最新版本的 Node 支持 JavaScript 的绝大部分最新特性,所以我们可以使用最新的特性,而不必转译我们的代码。

我们的目标是实现一个能与 第二章节 中的笔记应用一起工作的后端。然而,让我们从最基本的开始,实现一个经典的 "hello world " 应用。

注意 本章节的应用和练习并不都是 React 应用,而且我们不会使用 create-react-app 工具来初始化这个应用的项目。

我们在第二章节已经提到了 npm,它是一个用于管理 JavaScript 包的工具。事实上,npm 起源于 Node 生态系统。

让我们导航到一个合适的目录,用 npm init 命令为我们的应用创建一个新模板。我们将回答该工具提出的问题,结果是在项目的根部自动生成一个包含项目信息的 package.json 文件。

{
  "name": "backend",
  "version": "0.0.1",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "Matti Luukkainen",
  "license": "MIT"
}

该文件定义了,比如说,应用的入口点是 index.js 文件。

让我们对 scripts 对象做一个小小的改动。

{
  // ...
  "scripts": {
    "start": "node index.js",    "test": "echo \"Error: no test specified\" && exit 1"
  },
  // ...
}

接下来,让我们创建我们应用的第一个版本,在项目的根部添加一个 index.js 文件,代码如下。

console.log('hello world')

我们可以直接用 Node 从命令行中运行该程序。

node index.js

或者我们可以作为一个npm脚本运行它。

npm start

start npm 脚本可以工作,因为我们在 package.json 文件中定义了它。

{
  // ...
  "scripts": {
    "start": "node index.js",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  // ...
}

尽管项目的执行在通过从命令行调用 node index.js 来启动时是有效的,但 npm 项目习惯于以 npm 脚本来执行这样的任务。

默认情况下,package.json 文件也定义了另一个常用的 npm 脚本,叫做< i>npm test。由于我们的项目还没有一个测试库,npm test 命令只是简单地执行以下命令。

echo "Error: no test specified" && exit 1

Simple web server

让我们通过编辑 index.js 文件,将应用变成一个网络服务器,如下所示。

const http = require('http')

const app = http.createServer((request, response) => {
  response.writeHead(200, { 'Content-Type': 'text/plain' })
  response.end('Hello World')
})

const PORT = 3001
app.listen(PORT)
console.log(`Server running on port ${PORT}`)

一旦应用运行,下面的信息将被打印在控制台。

Server running on port 3001

我们可以通过访问 http://localhost:3001 地址,在浏览器中打开我们卑微的应用。

fullstack content

事实上,无论 URL 的后半部分是什么,服务器的工作方式都是一样的。同样,地址 http://localhost:3001/foo/bar 将显示相同的内容。

*NB如果 3001 端口已经被其他应用使用,那么启动服务器将导致以下错误信息。

➜  hello npm start

> hello@1.0.0 start /Users/mluukkai/opetus/_2019fullstack-code/part3/hello
> node index.js

Server running on port 3001
events.js:167
      throw er; // Unhandled 'error' event
      ^

Error: listen EADDRINUSE :::3001
    at Server.setupListenHandle [as _listen2] (net.js:1330:14)
    at listenInCluster (net.js:1378:12)

你有两个选择。要么关闭使用 3001 端口的应用(材料最后部分的 json-server 使用的是 3001 端口),要么为这个应用使用一个不同的端口。

让我们仔细看看这段代码的第一行。

const http = require('http')

在第一行中,应用导入了 Node 的内置 网络服务器 模块。这实际上就是我们在浏览器端代码中已经在做的事情,但语法略有不同。

import http from 'http'

如今,在浏览器中运行的代码都使用 ES6 模块。模块用 export 来定义,用 import 来使用。

然而,Node.js 使用所谓的 CommonJS 模块。其原因是,早在 JavaScript 在语言规范中支持模块之前,Node 生态系统就有了对模块的需求。Node 现在也支持使用 ES6 模块,但由于支持还 不是很完善,我们将坚持使用 CommonJS 模块。

CommonJS 模块的功能几乎与 ES6 模块完全一样,至少就我们在本课程中的需求而言是如此。

我们的代码中的下一块如下所示:

const app = http.createServer((request, response) => {
  response.writeHead(200, { 'Content-Type': 'text/plain' })
  response.end('Hello World')
})

该代码使用 http 模块的 createServer 方法来创建一个新的网络服务器。一个 事件处理程序 被注册到服务器上,每当 HTTP 请求被发送到服务器的地址 http://localhost:3001,该程序就会被调用。

该请求被响应,状态代码为 200,Content-Type 头设置为 text/plain,要返回的网站内容设置为 Hello World

最后几行绑定了分配给 app 变量的 http 服务器,以监听发送到 3001 端口的 HTTP 请求。

const PORT = 3001
app.listen(PORT)
console.log(`Server running on port ${PORT}`)

本课程中后端服务器的主要目的是向前端提供 JSON 格式的原始数据。出于这个原因,让我们立即改变我们的服务器以 JSON 格式返回一个硬编码的笔记列表。

const http = require('http')

let notes = [  {    id: 1,    content: "HTML is easy",    date: "2022-05-30T17:30:31.098Z",    important: true  },  {    id: 2,    content: "Browser can execute only Javascript",    date: "2022-05-30T18:39:34.091Z",    important: false  },  {    id: 3,    content: "GET and POST are the most important methods of HTTP protocol",    date: "2022-05-30T19:20:14.298Z",    important: true  }]const app = http.createServer((request, response) => {  response.writeHead(200, { 'Content-Type': 'application/json' })  response.end(JSON.stringify(notes))})
const PORT = 3001
app.listen(PORT)
console.log(`Server running on port ${PORT}`)

让我们重新启动服务器(你可以在控制台中按 Ctrl+C 来关闭服务器),让我们刷新浏览器。

Content-Type 头中的 application/json 值通知接收者,数据是 JSON 格式的。notes 数组通过 JSON.stringify(notes) 方法被转换为 JSON。

当我们打开浏览器时,显示的格式与 第二章节 中完全一样,在那里我们使用 json-server 来提供笔记的列表。

fullstack content

Express

用 Node 内置的 http 网络服务器直接实现我们的服务器代码是可行的。然而,这很麻烦,特别是一旦应用的规模扩大。

许多库已经被开发出来,通过提供一个更讨人喜欢的接口来与内置的 http 模块一起工作,从而缓解 Node 的服务器端开发。这些库的目的是为我们通常需要建立后端服务器的一般使用情况提供一个更好的抽象。到目前为止,用于这一目的的最流行的库是 express

让我们用命令将 express 定义为项目的依赖关系来使用它。

npm install express

这个依赖关系也被添加到我们的 package.json 文件中。

{
  // ...
  "dependencies": {
    "express": "^4.17.2"
  }
}

该依赖的源代码被安装到位于项目根部的 node/modules 目录中。除了 express 之外,你还可以在该目录中找到大量的其他依赖关系。

fullstack content

这些实际上是 express 库的依赖关系,以及它所有的依赖关系,等等。这些被称为我们项目的 transitive dependencies

我们的项目中安装了 4.17.2. 版本的 express。在 package.json 中版本号前面的圆点是什么意思?

"express": "^4.17.2"

npm 中使用的版本管理模式被称为 语义版本管理

^4.17.2 前面的圆点意味着如果项目的依赖关系被更新,所安装的 express 版本将至少是 4.17.2。然而,安装的 express 版本也可以是具有更大的 patch 号(最后一个数字),或者更大的 minor 号(中间的数字)。第一个 major 数字所表示的库的主要版本必须是相同的。

我们可以用命令更新项目的依赖关系。

npm update

同样地,如果我们在另一台电脑上开始做这个项目,我们可以用命令安装 package.json 中定义的项目的所有最新的依赖项。

npm install

如果一个依赖项的 major 号没有改变,那么较新的版本应该是 向后兼容。这意味着,如果我们的应用在未来碰巧使用了 4.99.175 版本的 Express,那么这部分实现的所有代码仍然要工作,而无需对代码进行修改。相反,未来的 5.0.0. 版本的 express可能包含 变化,会导致我们的应用不再工作。

Web and express

让我们回到我们的应用,并做如下修改。

const express = require('express')
const app = express()

let notes = [
  ...
]

app.get('/', (request, response) => {
  response.send('<h1>Hello World!</h1>')
})

app.get('/api/notes', (request, response) => {
  response.json(notes)
})

const PORT = 3001
app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`)
})

为了让我们的应用的新版本投入使用,我们必须重新启动应用。

应用并没有发生很大的变化。就在我们代码的开头,我们导入了 express,这次是一个 函数 ,用来创建一个存储在 app 变量中的 Express 应用。

const express = require('express')
const app = express()

接下来,我们定义两个通往应用的 路径 。第一个定义了一个事件处理程序,用来处理对应用 / 根的 HTTP GET 请求。

app.get('/', (request, response) => {
  response.send('<h1>Hello World!</h1>')
})

该事件处理函数接受两个参数。第一个 request 参数包含 HTTP 请求的所有信息,第二个 response 参数用于定义如何对请求进行响应。

在我们的代码中,请求是通过使用 response 对象的 send 方法回答的。调用该方法使服务器响应 HTTP 请求,发送一个响应,其中包含传递给 send 方法的字符串

Hello World!

。由于参数是一个字符串,Express 自动将 Content-Type 头的值设置为 text/html。响应的状态代码默认为 200。

我们可以从开发者工具中的 网络 标签来验证这一点。

fullstack content

第二个路由定义了一个事件处理程序,处理向应用的 notes 路径发出的 HTTP GET 请求。

app.get('/api/notes', (request, response) => {
  response.json(notes)
})

该请求用 response 对象的 json 方法来响应。调用该方法将发送传给它的 notes 数组,作为 JSON 格式的字符串。Express 自动将 Content-Type 头设置为 application/json 的适当值。

fullstack content

接下来,让我们快速浏览一下以 JSON 格式发送的数据。

在早期版本中,我们只使用 Node,我们必须用 JSON.stringify 方法将数据转换成 JSON 格式。

response.end(JSON.stringify(notes))

有了 Express,这就不再需要了,因为这种转换会自动发生。

值得注意的是,JSON 是一个字符串,而不是像分配给 notes 的值那样的一个 JavaScript 对象。

下面的实验说明了这一点。

fullstack content

上面的实验是在交互式 node-repl 中完成的。你可以通过在命令行中输入 node 来启动交互式 node-repl。在你写应用代码的时候,这个副本对于测试命令如何工作特别有用。我强烈建议这样做 !

nodemon

如果我们对应用的代码做了修改,我们必须重新启动应用,以便看到这些修改。我们重启应用的方法是:首先通过输入 Ctrl+C 来关闭它,然后再重启应用。与 React 中方便的工作流程相比,即浏览器在发生变化后自动重新加载,这感觉有点麻烦。

解决这个问题的方法是 nodemon

nodemon 将观察 nodemon 启动时所在目录中的文件,如果有任何文件发生变化,nodemon 将自动重启你的 node 应用

让我们用命令将 nodemon 定义为一个 开发依赖项 来安装它。

npm install --save-dev nodemon

package.json 的内容也有变化。

{
  //...
  "dependencies": {
    "express": "^4.17.2",
  },
  "devDependencies": {
    "nodemon": "^2.0.15"
  }
}

如果你不小心用错了命令,nodemon 依赖被添加到了 "dependencies " 下,而不是 "devDependencies " 下,那么请手动修改 package.json 的内容,以符合上面的内容。

我们所说的开发依赖,指的是只在应用的开发过程中需要的工具,例如用于测试或自动重启应用,如 nodemon

当应用在生产服务器(如 Heroku)上以生产模式运行时,不需要这些开发依赖性。

我们可以像这样用 nodemon 启动我们的应用。

node_modules/.bin/nodemon index.js

现在对应用代码的修改会导致服务器自动重新启动。值得注意的是,即使后端服务器自动重启,浏览器仍然需要手动刷新。这是因为与在 React 中工作时不同,我们没有自动重新加载浏览器所需的 hot reload 功能。

这个命令很长,而且很不讨人喜欢,所以让我们在 package.json 文件中为它定义一个专门的 npm 脚本

{
  // ..
  "scripts": {
    "start": "node index.js",
    "dev": "nodemon index.js",    "test": "echo \"Error: no test specified\" && exit 1"
  },
  // ..
}

在脚本中不需要指定 nodemon 的 node/modules/.bin/nodemon 路径,因为 _npm 自动知道从该目录中搜索该文件。

我们现在可以用命令在开发模式下启动服务器。

npm run dev

starttest 脚本不同,我们还必须在命令中加入 run

REST

让我们扩展我们的应用,使其提供与 json-server 一样的 RESTful HTTP API。

Representational State Transfer,又称 REST,于 2000 年在 Roy Fielding 的 论文 中提出。REST 是一种架构风格,旨在建立可扩展的网络应用。

我们不打算深入研究 Fielding 对 REST 的定义,也不打算花时间去思考什么是 RESTful 和什么不是。相反,我们将采取一个更 狭窄的观点,只关注 RESTful APIs 在网络应用中的典型理解。事实上,REST 的原始定义甚至不限于网络应用。

我们在 前一部分 中提到,在 RESTful 思想中,单一的东西,如我们应用中的笔记,被称为 资源 。每个资源都有一个相关的 URL,这是资源的唯一地址。

一个惯例是通过结合资源类型的名称和资源的唯一标识符来创建资源的唯一地址。

让我们假设我们的服务的根 URL 是 www.example.com/api

如果我们把笔记的资源类型定义为 笔记 ,那么标识符为 10 的笔记资源的地址就有唯一的地址 www.example.com/api/notes/10

所有笔记资源的整个集合的 URL 是 www.example.com/api/notes

我们可以对资源执行不同的操作。要执行的操作是由 HTTP verb 定义的。

URL verb functionality
notes/10 GET fetches a single resource
notes GET fetches all resources in the collection
notes POST creates a new resource based on the request data
notes/10 DELETE removes the identified resource
notes/10 PUT replaces the entire identified resource with the request data
notes/10 PATCH replaces a part of the identified resource with the request data

这就是我们如何设法粗略地定义 REST 所指的 统一接口,这意味着一种定义接口的一致方式,使系统有可能合作。

这种解释 REST 的方式属于 Richardson 成熟度模型中的 RESTful 成熟度第二层次。根据 Roy Fielding 提供的定义,我们实际上并没有定义一个 REST API。事实上,世界上绝大部分所谓的 "REST "API 都不符合 Fielding 在其论文中列出的原始标准。

在某些地方(例如见 Richardson, Ruby: RESTful Web Services),你会看到我们的直接 CRUDAPI 模型被称为 面向资源架构 的例子,而不是 REST。我们将避免陷入语义学的争论,而是回到我们的应用上工作。

Fetching a single resource

让我们扩展我们的应用,使其提供一个 REST 接口来操作单个笔记。首先,让我们创建一个 路由 来获取一个单一的资源。

我们将为单个笔记使用的唯一地址的形式是 notes/10,其中末尾的数字是指笔记的唯一 ID 号。

我们可以通过使用冒号语法为 Express 中的路由定义 参数

app.get('/api/notes/:id', (request, response) => {
  const id = request.params.id
  const note = notes.find(note => note.id === id)
  response.json(note)
})

现在 app.get("/api/notes/:id", ...) 将处理所有形式为 /api/notes/SOMETHING 的 HTTP GET 请求,其中 SOMETHING 是一个任意的字符串。

请求路径中的 id 参数,可以通过 request 对象访问。

const id = request.params.id

现在熟悉的数组的 find 方法被用来寻找与参数相匹配的 id 的笔记。然后,该笔记被返回给请求的发送者。

当我们在浏览器中访问 http://localhost:3001/api/notes/1 来测试我们的应用时,我们注意到它似乎没有工作,因为浏览器显示的是一个空页面。这对我们这些软件开发者来说并不奇怪,是时候进行调试了。

在我们的代码中添加 console.log 命令是一个经过时间验证的技巧。

app.get('/api/notes/:id', (request, response) => {
  const id = request.params.id
  console.log(id)
  const note = notes.find(note => note.id === id)
  console.log(note)
  response.json(note)
})

当我们在浏览器中再次访问 http://localhost:3001/api/notes/1 时,控制台,也就是本例中的终端,将显示如下内容。

fullstack content

路由中的 id 参数被传递给我们的应用,但 find 方法没有找到一个匹配的笔记。

为了进一步调查,我们还在传递给 find 方法的比较函数里面添加了一个控制台日志。为了做到这一点,我们必须摆脱紧凑的箭头函数语法 note => note.id === id,而使用带有明确返回语句的语法。

app.get('/api/notes/:id', (request, response) => {
  const id = request.params.id
  const note = notes.find(note => {
    console.log(note.id, typeof note.id, id, typeof id, note.id === id)
    return note.id === id
  })
  console.log(note)
  response.json(note)
})

当我们在浏览器中再次访问这个 URL 时,每次调用比较函数都会向控制台打印一些不同的东西。控制台的输出如下。


1 'number' '1' 'string' false
2 'number' '1' 'string' false
3 'number' '1' 'string' false

错误的原因变得清晰了。id 变量包含一个字符串 "1",而笔记的 id 是整数。在 JavaScript 中,"triple equals " 比较 === 认为所有不同类型的值默认是不相等的,也就是说,1 不是 "1"。

让我们通过把 id 参数从一个字符串变成一个 数字 来解决这个问题。

app.get('/api/notes/:id', (request, response) => {
  const id = Number(request.params.id)  const note = notes.find(note => note.id === id)
  response.json(note)
})

现在获取一个单独的资源可以了。

fullstack content

然而,我们的应用还有一个问题。

如果我们搜索一个 id 不存在的笔记,服务器的反应是。

fullstack content

返回的 HTTP 状态代码是 200,这意味着响应成功了。由于 content-length 头的值为 0,所以没有数据随响应一起被送回来,同样可以从浏览器中验证。

出现这种行为的原因是,如果没有找到匹配的笔记,note 变量被设置为 undefined。这种情况需要在服务器上以更好的方式来处理。如果没有找到笔记,服务器应该用状态代码 404 not found 来响应,而不是 200。

让我们对我们的代码做如下修改。

app.get('/api/notes/:id', (request, response) => {
  const id = Number(request.params.id)
  const note = notes.find(note => note.id === id)

  if (note) {    response.json(note)  } else {    response.status(404).end()  }})

由于响应中没有附加数据,我们使用 status 方法来设置状态,并使用 end 方法来响应请求,而不发送任何数据。

if 条件利用了这样一个事实,即所有的 JavaScript 对象都是 truthy,意味着它们在比较操作中计算为真。然而,undefinedfalsy,这意味着它将被计算为假。

我们的应用工作了,如果没有找到笔记,就会发送错误状态代码。然而,应用并没有返回任何东西给用户看,就像 Web 应用在我们访问一个不存在的页面时通常做的那样。我们实际上不需要在浏览器中显示任何东西,因为 REST APIs 是用于编程的接口,而错误状态代码是需要的全部内容。

无论如何,我们可以通过 覆盖默认的 NOT FOUND 信息 来提供一个关于发送 404 错误的原因的线索。

Deleting resources

接下来让我们实现一个删除资源的路径。删除是通过向资源的 URL 发出 HTTP DELETE 请求来实现的。

app.delete('/api/notes/:id', (request, response) => {
  const id = Number(request.params.id)
  notes = notes.filter(note => note.id !== id)

  response.status(204).end()
})

如果删除资源是成功的,也就是说,笔记存在并且被删除了,我们用状态代码 204 无内容 来响应请求,并且在响应中不返回数据。

对于在资源不存在的情况下应该向 DELETE 请求返回什么状态码,目前还没有达成共识。实际上,唯一的两个选择是 204 和 404。为了简单起见,我们的应用在这两种情况下都会用 204 来响应。

Postman

那么我们如何测试删除操作呢?HTTP GET 请求很容易从浏览器中发出。我们可以写一些 JavaScript 来测试删除操作,但写测试代码并不总是在每种情况下的最佳解决方案。

有许多工具可以使后端测试更容易。其中之一是一个命令行程序 curl。然而,我们将看一下使用 Postman 来测试应用,而不是 curl。

让我们安装 Postman 桌面客户端 从这里 并尝试一下。

fullstack content

在这种情况下,使用 Postman 是非常容易的。只需定义网址,然后选择正确的请求类型(DELETE)。

后端服务器似乎反应正确。通过对 http://localhost:3001/api/notes 的 HTTP GET 请求,我们看到 id 为 2 的笔记已经不在列表中了,这表明删除成功了。

因为应用中的笔记只保存在内存中,所以当我们重新启动应用时,笔记的列表将恢复到原来的状态。

The Visual Studio Code REST client

如果你使用 Visual Studio Code,你可以使用 VS Code REST 客户端 插件,而不是 Postman。

一旦插件安装完毕,使用它就非常简单。我们在应用的根部建立一个名为 requests 的目录。我们将所有的 REST 客户端请求保存在该目录中,作为以 .rest 扩展名结尾的文件。

让我们创建一个新的 get_all_notes.rest 文件并定义获取所有笔记的请求。

fullstack content

通过点击 发送请求 文本,REST 客户端将执行 HTTP 请求,来自服务器的响应在编辑器中打开。

fullstack content

The WebStorm HTTP Client

如果你使用 IntelliJ WebStorm,你可以使用其内置的 HTTP 客户端的类似程序。创建一个扩展名为 ".rest " 的新文件,编辑器将显示你创建和运行请求的选项。你可以按照 本指南 来了解更多信息。

Receiving data

接下来,让我们实现向服务器添加新笔记的功能。通过向 http://localhost:3001/api/notes,并在请求 body 中以 JSON 格式发送新笔记的所有信息,就可以添加一个笔记。

为了方便地访问数据,我们需要 express json-parser 的帮助,它可以通过命令 app.use(express.json()) 来使用。

让我们激活 json-parser 并实现一个初始处理程序来处理 HTTP POST 请求。

const express = require('express')
const app = express()

app.use(express.json())
//...

app.post('/api/notes', (request, response) => {  const note = request.body  console.log(note)  response.json(note)})

事件处理函数可以访问 request 对象的 body 属性中的数据。

如果没有 json-parser,body 属性将是未定义的。json-parser 的功能是将请求的 JSON 数据转化为 JavaScript 对象,然后在调用路由处理程序之前将其附加到 request 对象的 body 属性。

就目前而言,应用除了将收到的数据打印到控制台并在响应中送回外,并没有对其做任何处理。

在我们实现其余的应用逻辑之前,让我们用 Postman 验证数据是否真的被服务器收到。除了在 Postman 中定义 URL 和请求类型外,我们还必须定义在 body 中发送的数据。

fullstack content

应用将我们在请求中发送的数据打印到控制台。

fullstack content

NB 当你在后端工作时,保持运行应用的终端始终可见 。由于 Nodemon 的存在,我们对代码的任何改动都会重新启动应用。如果你关注控制台,你将立即能够发现应用中出现的错误。

fullstack content

同样,检查控制台也很有用,可以确保后端在不同的情况下表现得像我们期望的那样,比如当我们用 HTTP POST 请求发送数据时。当然,当应用还在开发时,在代码中添加大量的 console.log 命令是个好主意。

问题的一个潜在原因是请求中 Content-Type 头的设置不正确。如果正文的类型没有被正确定义,这种情况就会发生在 Postman 上。

fullstack content

Content-Type 头被设置为 text/plain

fullstack content

服务器似乎只收到一个空对象。

fullstack content

如果头中没有正确的值,服务器将不能正确地解析数据。它甚至不会尝试猜测数据的格式,因为有 大量 潜在的 Content-Types

如果你使用的是 VS Code,那么你应该安装上一章中的 REST 客户端, 如果你还没有安装的话 。POST请求可以像这样使用REST客户端发送:

fullstack content

我们为这个请求创建了一个新的 create_note.rest 文件。请求的格式是按照 文档中的说明

与 Postman 相比,REST 客户端的一个好处是,请求可以方便地在项目库的根部获得,而且可以分发给开发团队的每个人。你也可以使用###分隔符在同一个文件中添加多个请求。

GET http://localhost:3001/api/notes/

###
POST http://localhost:3001/api/notes/ HTTP/1.1
content-type: application/json

{
    "name": "sample",
    "time": "Wed, 21 Oct 2015 18:27:50 GMT"
}

Postman 也允许用户保存请求,但情况可能变得相当混乱,特别是当你在多个不相关的项目上工作时。

重要的附注

有时当你在调试时,你可能想找出在 HTTP 请求中设置了哪些头文件。实现这一目的的方法之一是通过 request 对象的 get 方法,该方法可用于获取单个头的值。request 对象也有 headers 属性,它包含一个特定请求的所有头信息。

如果你不小心在顶行和指定 HTTP 头信息的行之间添加了一个空行,VS REST 客户端就会出现问题。在这种情况下,REST 客户端解释为所有的头信息都是空的,这导致后端服务器不知道它所收到的数据是 JSON 格式的。

如果在你的代码中的某个时刻,你用 console.log(request.headers) 命令打印所有的请求头,你就能发现这个丢失的 Content-Type 头。

让我们回到应用。一旦我们知道应用正确地接收了数据,就是最后处理请求的时候了。

app.post('/api/notes', (request, response) => {
  const maxId = notes.length > 0
    ? Math.max(...notes.map(n => n.id))
    : 0

  const note = request.body
  note.id = maxId + 1

  notes = notes.concat(note)

  response.json(note)
})

我们需要一个唯一的 id 给这个笔记。首先,我们找出当前列表中最大的 ID 号码,并将其分配给 maxId 变量。然后,新笔记的 id 被定义为 maxId+1。这种方法实际上是不推荐的,但我们现在将继续使用它,因为我们很快就会取代它。

目前的版本仍然有一个问题,即 HTTP POST 请求可以被用来添加具有任意属性的对象。让我们通过定义 content 属性不得为空来改进这个应用。importantdate 属性将被赋予默认值。所有其他属性都被丢弃。

const generateId = () => {
  const maxId = notes.length > 0
    ? Math.max(...notes.map(n => n.id))
    : 0
  return maxId + 1
}

app.post('/api/notes', (request, response) => {
  const body = request.body

  if (!body.content) {
    return response.status(400).json({
      error: 'content missing'
    })
  }

  const note = {
    content: body.content,
    important: body.important || false,
    date: new Date(),
    id: generateId(),
  }

  notes = notes.concat(note)

  response.json(note)
})

为笔记生成新的 ID 号码的逻辑已经被提取到一个单独的 generateId 函数中。

如果收到的数据缺少 content 属性的值,服务器将以状态代码 400 bad request 响应请求。

if (!body.content) {
  return response.status(400).json({
    error: 'content missing'
  })
}

注意,调用 return 是至关重要的,因为否则代码会执行到最后,错误的笔记会被保存到应用中。

如果内容属性有一个值,笔记将基于收到的数据。如前所述,在服务器上生成时间戳比在浏览器中生成时间戳更好,因为我们不能相信运行浏览器的主机有正确的时钟设置。现在,date 属性的生成是由服务器完成的。

如果 重要的 属性丢失,我们将默认其值为 。默认值目前是以一种看起来相当奇怪的方式生成的。

important: body.important || false,

如果保存在 body 变量中的数据有 important 属性,表达式将计算为其值。如果该属性不存在,那么表达式将计算为 false,这在垂直线的右侧被定义。

确切地说,当 重要 属性是 false 时,那么 body.important || false 表达式实际上将从右侧返回 false...

你可以在 这个 github 仓库part3-1 分支中找到我们当前应用的全部代码。

目前应用状态的代码具体在分支 part3-1

fullstack content

如果你克隆了这个项目,在用 npm startnpm run dev 启动应用之前,运行 npm install 命令。

在我们进入练习之前还有一件事。生成 ID 的函数目前看起来是这样的。

const generateId = () => {
  const maxId = notes.length > 0
    ? Math.max(...notes.map(n => n.id))
    : 0
  return maxId + 1
}

该函数主体包含了一行看起来有点耐人寻味的内容。

Math.max(...notes.map(n => n.id))

这一行代码到底发生了什么?notes.map(n => n.id) 创建一个新的数组,其中包含了所有笔记的 ID。Math.max 返回传递给它的数字的最大值。然而,notes.map(n => n.id) 是一个 数组 ,所以它不能直接作为一个参数给 Math.max。数组可以通过使用 " 三点 "spread 语法 ... 转换为单个数字。

About HTTP request types

HTTP 标准 谈到了与请求类型有关的两个属性:安全空闲

HTTP GET 请求应该是 安全的

特别是,约定俗成的是,GET 和 HEAD 方法不应具有除检索以外的行动意义。这些方法应该被认为是 " 安全的 "。

安全意味着执行的请求不能在服务器中引起任何 副作用 。我们所说的副作用是指数据库的状态不能因为请求而改变,而且响应必须只返回服务器上已经存在的数据。

没有什么能保证 GET 请求实际上是 安全的 ,这实际上只是 HTTP 标准中定义的一个建议。通过在我们的 API 中坚持 RESTful 原则,GET 请求实际上总是以一种 安全 的方式被使用。

HTTP 标准还定义了请求类型 HEAD,这应该是安全的。在实践中,HEAD 的工作方式应该与 GET 完全一样,但它不返回任何东西,只返回状态代码和响应头。当你发出 HEAD 请求时,响应体将不会被返回。

除了 POST,所有的 HTTP 请求都应该是 idempotent

方法也可以具有 " 同位素 " 的属性,即(除了错误或过期问题)N>0 个相同的请求的副作用与单个请求相同。GET、HEAD、PUT 和 DELETE 等方法都有这个属性

这意味着,如果一个请求有副作用,那么无论这个请求被发送多少次,结果都应该是一样的。

如果我们向 url /api/notes/10 发出 HTTP PUT 请求,并随请求发送数据 { content:"no side effects!", important: true },无论发送多少次请求,结果都是一样的。

就像 GET 请求的 安全 一样, 空闲 也只是 HTTP 标准中的一个建议,并不是简单地基于请求类型就能保证的。然而,当我们的 API 遵守 RESTful 原则时,那么 GET、HEAD、PUT 和 DELETE 请求的使用方式就是 idempotent。

POST 是唯一的 HTTP 请求类型,既不 安全 也不 空闲 。如果我们向 /api/notes 发送 5 个不同的 HTTP POST 请求,其正文为 {content:"many same", important: true},服务器上产生的 5 个笔记都会有相同的内容。

Middleware

我们之前使用的快递 json-parser 是一个所谓的 中间件

中间件是可以用来处理 requestresponse 对象的函数。

我们之前使用的 json-parser 从请求中获取原始数据,这些数据存储在 request 对象中,将其解析为一个 JavaScript 对象,并将其作为一个新的属性 body 分配给 request 对象。

在实践中,你可以同时使用几个中间件。当你有多个中间件时,它们会按照在 Express 中被使用的顺序一个一个地被执行。

让我们来实现我们自己的中间件,它可以打印出发送到服务器的每个请求的信息。

中间件是一个接收三个参数的函数。

const requestLogger = (request, response, next) => {
  console.log('Method:', request.method)
  console.log('Path:  ', request.path)
  console.log('Body:  ', request.body)
  console.log('---')
  next()
}

在函数体的最后,调用作为参数传递的 next 函数。这个 next 函数将控制权交给下一个中间件。

中间件是这样被使用的。

app.use(requestLogger)

中间件函数的调用顺序是它们被 Express 服务器对象的 use 方法所使用的顺序。请注意,json-parser 是在 requestLogger 中间件之前被使用的,因为否则在执行记录器的时候,request.body 将不会被初始化。

如果我们想让中间件函数在路由事件处理程序被调用前执行,那么就必须在路由之前使用这些中间件函数。也有一些情况,我们想在路由之后定义中间件函数。在实践中,这意味着我们要定义的中间件函数只有在没有路由处理 HTTP 请求时才会被调用。

让我们在路由之后添加以下中间件,用于捕捉向不存在的路由发出的请求。对于这些请求,中间件将返回一个 JSON 格式的错误信息。

const unknownEndpoint = (request, response) => {
  response.status(404).send({ error: 'unknown endpoint' })
}

app.use(unknownEndpoint)

你可以在 这个 github 仓库part3-2 分支中找到我们当前应用的全部代码。