diff --git a/src/content/1/zh/part1a.md b/src/content/1/zh/part1a.md index bc36adfb509..8653bb45a36 100644 --- a/src/content/1/zh/part1a.md +++ b/src/content/1/zh/part1a.md @@ -58,7 +58,7 @@ Vite[默认](https://vitejs.dev/config/server-options.html#server-port)在端口 ![](../../images/1/1-vite4.png) -应用的代码位于src文件夹中。让我们简化默认代码,使文件main.jsx的内容如下所示: +应用的代码位于src文件夹中。让我们简化默认代码,使文件main.jsx的内容如下所示: ```js import ReactDOM from 'react-dom/client' @@ -294,7 +294,7 @@ JSX是“类[XML](https://developer.mozilla.org/en-US/docs/Web/XML/XML_introduct ### 多个组件 -让我们修改App.js文件如下: +让我们修改App.jsx文件如下: ```js // highlight-start @@ -787,8 +787,8 @@ const App = () => { export default App ``` - -然后删除多余的文件App.css和index.css,以及目录assets。 + +然后删除多余的文件App.cssindex.css,以及目录assets 整个应用都在同一个组件中。重构代码,使其由三个新组件组成:HeaderContentTotal。所有数据仍驻留在App组件中,使用props将必要的数据传递给每个组件。Header负责显示课程的名称,Content显示各部分及其练习的数量,Total显示练习的总数量。 diff --git a/src/content/1/zh/part1c.md b/src/content/1/zh/part1c.md index 5fe41f02160..6eea368dff7 100644 --- a/src/content/1/zh/part1c.md +++ b/src/content/1/zh/part1c.md @@ -199,8 +199,8 @@ const Hello = ({ name, age }) => { 到目前为止,我们所有的应用都是静态的——在最初的渲染之后,其外观保持不变。如果我们想创建一个计数器,其值随着时间的推移或点击按钮而增加呢? - -让我们从将文件App.js变成下面的样子开始: + +让我们从将文件App.jsx变成下面的样子开始: ```js const App = (props) => { @@ -213,8 +213,8 @@ const App = (props) => { export default App ``` - -将文件index.js改成: + +将文件main.jsx改成: ```js import ReactDOM from 'react-dom/client' @@ -285,7 +285,7 @@ setInterval(() => { 接下来,让我们借助React的[状态hook](https://react.dev/learn/state-a-components-memory)来给我们的应用的App组件添加状态。 -我们将改变应用的内容如下。main.js回到: +我们将改变应用的内容如下。main.jsx回到: ```js import ReactDOM from 'react-dom/client' @@ -295,8 +295,8 @@ import App from './App' ReactDOM.createRoot(document.getElementById('root')).render() ``` - -而App.js则改为: + +而App.jsx则改为: ```js import { useState } from 'react' // highlight-line diff --git a/src/content/1/zh/part1d.md b/src/content/1/zh/part1d.md index cf6f8b099f3..4b93544de44 100644 --- a/src/content/1/zh/part1d.md +++ b/src/content/1/zh/part1d.md @@ -1413,7 +1413,7 @@ rm -rf node_modules/ && npm i 注意你的应用只需要在一个浏览器会话中运行。一旦你刷新页面,所收集的反馈消失也不要紧。 -建议使用和教材与之前练习中相同的结构。文件main.js如下: +建议使用和教材与之前练习中相同的结构。文件main.jsx如下: ```js import ReactDOM from 'react-dom/client' @@ -1584,10 +1584,10 @@ export default App ``` -文件main.js的内容与之前的练习相同。 +文件main.jsx的内容与之前的练习相同。 -找出如何在JavaScript中生成随机数,例如通过搜索引擎或去[Mozilla Developer Network](https://developer.mozilla.org)。记住,你可以直接在浏览器的控制台测试生成随机数等。 +查找如何在JavaScript中生成随机数,例如通过搜索引擎或去[Mozilla Developer Network](https://developer.mozilla.org)。记住,你可以直接在浏览器的控制台测试生成随机数等。 你完成的应用可能如下所示: diff --git a/src/content/2/zh/part2a.md b/src/content/2/zh/part2a.md index 8a6b50401d1..fd3401c0e38 100644 --- a/src/content/2/zh/part2a.md +++ b/src/content/2/zh/part2a.md @@ -107,8 +107,8 @@ Visual Studio Code里可以很方便地创建“Snippets”(代码片段), 现在我们将用React做一个类似[第0章节](/zh/part0)中示例程序的前端,或者叫用户界面(用户在浏览器中所看到的部分)。 - -让我们从下面开始(文件App.js): + +让我们从下面开始(文件App.jsx): ```js const App = (props) => { @@ -130,7 +130,7 @@ export default App ``` -文件main.js如下所示: +文件main.jsx如下所示: ```js import ReactDOM from 'react-dom/client' diff --git a/src/content/3/en/part3a.md b/src/content/3/en/part3a.md index 9913f2d34a1..39eb1511e7b 100644 --- a/src/content/3/en/part3a.md +++ b/src/content/3/en/part3a.md @@ -41,7 +41,7 @@ The file defines, for instance, that the entry point of the application is the < Let's make a small change to the scripts object by adding a new script command. -```bash +```json { // ... "scripts": { @@ -72,7 +72,7 @@ npm start The start npm script works because we defined it in the package.json file: -```bash +```json { // ... "scripts": { @@ -370,7 +370,7 @@ Now, changes to the application's code will cause the server to restart automati Let's define a custom npm script in the package.json file to start the development server: -```bash +```json { // .. "scripts": { @@ -735,7 +735,7 @@ The function body contains a row that looks a bit intriguing: Math.max(...notes.map(n => Number(n.id))) ``` -What exactly is happening in that line of code? notes.map(n => n.id) creates a new array that contains all the ids of the notes in number form. [Math.max](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/max) returns the maximum value of the numbers that are passed to it. However, notes.map(n => Number(n.id)) is an array so it can't directly be given as a parameter to _Math.max_. The array can be transformed into individual numbers by using the "three dot" [spread](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax) syntax .... +What exactly is happening in that line of code? notes.map(n => Number(n.id)) creates a new array that contains all the ids of the notes in number form. [Math.max](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/max) returns the maximum value of the numbers that are passed to it. However, notes.map(n => Number(n.id)) is an array so it can't directly be given as a parameter to _Math.max_. The array can be transformed into individual numbers by using the "three dot" [spread](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax) syntax .... diff --git a/src/content/3/en/part3b.md b/src/content/3/en/part3b.md index 6a184a62ad8..3502b946829 100644 --- a/src/content/3/en/part3b.md +++ b/src/content/3/en/part3b.md @@ -457,7 +457,7 @@ Because in development mode the frontend is at the address localhost:5173 If the project was created with Vite, this problem is easy to solve. It is enough to add the following declaration to the vite.config.js file of the frontend directory. -```bash +```js import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' diff --git a/src/content/3/en/part3d.md b/src/content/3/en/part3d.md index 408fbb2fb01..ade496e6aa5 100644 --- a/src/content/3/en/part3d.md +++ b/src/content/3/en/part3d.md @@ -249,7 +249,7 @@ export default [ ] ``` -So far, our ESLint configuration file defines the _files_ option with _["**/*.js"]_, which tells ESLint to look at all JavaScript files in our project folder. The _languageOptions_ property specifies options related to language features that ESLint should expect, in which we defined the _sourceType_ option as "commonjs". This indicates that the JavaScript code in our project uses the CommonJS module system, allowing ESLint to parse the code accordingly. +So far, our ESLint configuration file defines the _files_ option with _["\*\*/\*.js"]_, which tells ESLint to look at all JavaScript files in our project folder. The _languageOptions_ property specifies options related to language features that ESLint should expect, in which we defined the _sourceType_ option as "commonjs". This indicates that the JavaScript code in our project uses the CommonJS module system, allowing ESLint to parse the code accordingly. The _globals_ property specifies global variables that are predefined. The spread operator applied here tells ESLint to include all global variables defined in the _globals.node_ settings such as the _process_. In the case of browser code we would define here _globals.browser_ to allow browser specific global variables like _window_, and _document_. diff --git a/src/content/3/zh/part3.md b/src/content/3/zh/part3.md index e14d7b800cc..7887491dde3 100644 --- a/src/content/3/zh/part3.md +++ b/src/content/3/zh/part3.md @@ -7,6 +7,15 @@ lang: zh
- 在这一部分,我们的重点转向后端,也就是在堆栈的服务器端实现功能。我们将通过使用Express库在Node.js中实现一个简单的REST API,应用的数据将被存储在MongoDB数据库中。在这一部分的最后,我们将把我们的应用部署到互联网上。 +在这一部分,我们将重点转向后端,也就是在全栈中的服务端实现功能。我们将用 Express 库在 Node.js 中实现一个简单的 REST API,并将应用的数据将存储在 MongoDB 数据库中。在这一章节的最后,我们将把我们的应用部署到互联网上。 + + +原文更新于 2025 年 3 月 16 日 + +- Node 版本更新至 v22.3.0 + +- nodemon 替换为 node --watch 命令 + +- 更新并格式化 MongoDB 指令
diff --git a/src/content/3/zh/part3a.md b/src/content/3/zh/part3a.md index 6ca29c1eb40..3e5fec25903 100644 --- a/src/content/3/zh/part3a.md +++ b/src/content/3/zh/part3a.md @@ -7,37 +7,29 @@ lang: zh
- - 在这一部分中,我们的重点转向后端:也就是说,在堆栈的服务器端实现功能。 - +在这一部分中,我们将重点转向后端,也就是在全栈中的服务端实现功能。 -我们将在 [NodeJS](https://nodejs.org/en/) 的基础上建立我们的后端,这是一个基于谷歌 [Chrome V8](https://developers.google.com/v8/) JavaScript 引擎的 JavaScript 运行时间。 - - - - 本课程材料是用 Node.js 的 16.13.2 版本编写的。请确保你的 Node 版本至少和教材中使用的版本一样新(你可以通过在命令行中运行 _node -v_ 来检查版本)。 +我们将在 [NodeJS](https://nodejs.org/en/) 的基础上建立后端,NodeJS 是一个基于谷歌的 [Chrome V8](https://developers.google.com/v8/) JavaScript 引擎的 JavaScript 运行时。 + +本教材是用 Node.js 的 v22.3.0 版本编写的。请确保你的 Node 版本至少和教材中使用的版本一样新(你可以通过在命令行中运行 _node -v_ 来检查版本)。 - 正如在 [第一章节](/en/part1/java_script) 中提到的,浏览器还不支持 JavaScript 的最新功能,这就是为什么在浏览器中运行的代码必须用例如 [babel](https://babeljs.io/) 进行 转写 。在后端运行的 JavaScript 的情况则不同。最新版本的 Node 支持 JavaScript 的绝大部分最新特性,所以我们可以使用最新的特性,而不必转译我们的代码。 - +正如[第 1 章节](/zh/part1/java_script)中提到的,浏览器还不支持 JavaScript 的最新功能,因此浏览器中运行的代码必须用例如 [babel](https://babeljs.io/) 进行转译。运行在后端的 JavaScript 的情况则不同。最新版本的 Node 支持绝大部分 JavaScript 的最新特性,所以我们无需转译代码即可使用最新的特性。 - 我们的目标是实现一个能与 [第二章节](/en/part2/) 中的笔记应用一起工作的后端。然而,让我们从最基本的开始,实现一个经典的 "hello world " 应用。 - - - - **注意** 本章节的应用和练习并不都是 React 应用,而且我们不会使用 create-react-app 工具来初始化这个应用的项目。 +我们的目标是实现一个能与[第 2 章节](/zh/part2/)中的笔记应用一起运行的后端。然而,让我们从最基础的实现一个经典的“hello world”应用开始。 + +**注意**本章节的应用和练习并不都是 React 应用,因此我们不会用 create vite@latest -- --template react 工具来初始化这些应用的项目。 - 我们在第二章节已经提到了 [npm](/en/part2/getting_data_from_server#npm),它是一个用于管理 JavaScript 包的工具。事实上,npm 起源于 Node 生态系统。 - +我们在第 2 章节已经提到了 [npm](/zh/part2/获取服务端的数据#npm),它是一个用于管理 JavaScript 包的工具。事实上,npm 就是从 Node 生态发源的。 - 让我们导航到一个合适的目录,用 _npm init_ 命令为我们的应用创建一个新模板。我们将回答该工具提出的问题,结果是在项目的根部自动生成一个包含项目信息的 package.json 文件。 +让我们进入一个合适的目录,用 _npm init_ 命令为应用创建一个模板。回答完 npm 提出的问题后(译注:全部使用默认答案,也就是全部按回车),就会在项目的根目录自动生成一个包含项目信息的 package.json 文件。 ```json { @@ -53,15 +45,13 @@ lang: zh } ``` - - 该文件定义了,比如说,应用的入口点是 index.js 文件。 +这个文件定义了,例如,应用的入口点是 index.js 文件。 + +让我们对 scripts 对象小小地改动一下,添加一条脚本命令。 - - 让我们对 scripts 对象做一个小小的改动。 - -```bash +```json { // ... "scripts": { @@ -72,35 +62,31 @@ lang: zh } ``` - - 接下来,让我们创建我们应用的第一个版本,在项目的根部添加一个 index.js 文件,代码如下。 +接下来,让我们创建应用的第一个版本,在项目的根目录下添加一个 index.js 文件,代码如下: ```js console.log('hello world') ``` - - 我们可以直接用 Node 从命令行中运行该程序。 +我们可以直接用 Node 从命令行中运行该程序: ```bash node index.js ``` - - 或者我们可以作为一个[npm脚本](https://docs.npmjs.com/misc/scripts)运行它。 +我们也可以用 [npm 脚本](https://docs.npmjs.com/misc/scripts)来运行它: ```bash npm start ``` - - start npm 脚本可以工作,因为我们在 package.json 文件中定义了它。 +因为我们在 package.json 文件中定义了 start npm 脚本,所以刚才可以运行: -```bash +```json { // ... "scripts": { @@ -111,24 +97,21 @@ npm start } ``` - - 尽管项目的执行在通过从命令行调用 _node index.js_ 来启动时是有效的,但 npm 项目习惯于以 npm 脚本来执行这样的任务。 - +尽管可以通过从命令行调用 _node index.js_ 来启动项目,但对于 npm 项目,更习惯用 npm 脚本来执行这些任务。 - - 默认情况下,package.json 文件也定义了另一个常用的 npm 脚本,叫做< i>npm test。由于我们的项目还没有一个测试库,_npm test_ 命令只是简单地执行以下命令。 + +默认情况下,package.json 文件还定义了另一个常用的 npm 脚本,叫做 npm test。由于我们的项目还没有测试库,_npm test_ 命令只是简单地执行以下命令: ```bash echo "Error: no test specified" && exit 1 ``` - -### Simple web server - + +### 简单的 web 服务端 - 让我们通过编辑 _index.js_ 文件,将应用变成一个网络服务器,如下所示。 +让我们将应用变成一个网络服务端,将 _index.js_ 文件改成: ```js const http = require('http') @@ -144,23 +127,22 @@ console.log(`Server running on port ${PORT}`) ``` - 一旦应用运行,下面的信息将被打印在控制台。 +一旦应用运行,控制台将打印下列信息: ```bash Server running on port 3001 ``` - 我们可以通过访问 地址,在浏览器中打开我们卑微的应用。 +我们可以通过在浏览器中访问地址 打开我们的简陋应用。 ![](../../images/3/1.png) - - 事实上,无论 URL 的后半部分是什么,服务器的工作方式都是一样的。同样,地址 将显示相同的内容。 - + +无论 URL 后面是什么,服务端的表现都是一样的。地址 也将显示相同的内容。 - - **NB*如果 3001 端口已经被其他应用使用,那么启动服务器将导致以下错误信息。 + +**注** 如果 3001 端口已经被其他应用使用,那么启动服务端会显示以下错误信息: ```bash ➜ hello npm start @@ -178,35 +160,34 @@ Error: listen EADDRINUSE :::3001 at listenInCluster (net.js:1378:12) ``` - - - 你有两个选择。要么关闭使用 3001 端口的应用(材料最后部分的 json-server 使用的是 3001 端口),要么为这个应用使用一个不同的端口。 + +你有两个选择。要么关闭使用 3001 端口的应用(教材上一章节的 JSON Server 当时就运行在 3001 端口),要么让这个应用使用不同的端口。 - 让我们仔细看看这段代码的第一行。 +让我们仔细看看这段代码的第一行: ```js const http = require('http') ``` - - 在第一行中,应用导入了 Node 的内置 [网络服务器](https://nodejs.org/docs/latest-v8.x/api/http.html) 模块。这实际上就是我们在浏览器端代码中已经在做的事情,但语法略有不同。 + +在第一行中,应用导入了 Node 内置的 [web server](https://nodejs.org/docs/latest-v18.x/api/http.html) 模块。这实际上就是我们在浏览器端代码中已经在做的事情,只是语法略有不同: ```js import http from 'http' ``` - - 如今,在浏览器中运行的代码都使用 ES6 模块。模块用 [export](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/export) 来定义,用 [import](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import) 来使用。 + +现在,浏览器中运行的代码都使用 ES6 模块。模块用 [export](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/export) 来定义,用 [import](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import) 来导入到当前文件。 - - 然而,Node.js 使用所谓的 [CommonJS](https://en.wikipedia.org/wiki/CommonJS) 模块。其原因是,早在 JavaScript 在语言规范中支持模块之前,Node 生态系统就有了对模块的需求。Node 现在也支持使用 ES6 模块,但由于支持还 [不是很完善](https://nodejs.org/api/esm.html#modules-ecmascript-modules),我们将坚持使用 CommonJS 模块。 + +Node.js 使用的是 [CommonJS](https://en.wikipedia.org/wiki/CommonJS) 模块。其原因是,Node 生态早在 JavaScript 在语言规范中支持模块之前就有对模块的需求了。现在,Node 也支持使用 ES6 模块,但支持还不是很完善,所以我们将继续使用 CommonJS 模块。 - CommonJS 模块的功能几乎与 ES6 模块完全一样,至少就我们在本课程中的需求而言是如此。 +CommonJS 模块的功能与 ES6 模块几乎完全一样,至少在本课程我们需要用到的地方都一样。 -我们的代码中的下一块如下所示: +代码的下一部分如下所示: ```js const app = http.createServer((request, response) => { @@ -215,16 +196,14 @@ const app = http.createServer((request, response) => { }) ``` - - 该代码使用 [http](https://nodejs.org/docs/latest-v8.x/api/http.html) 模块的 _createServer_ 方法来创建一个新的网络服务器。一个 事件处理程序 被注册到服务器上,每当 HTTP 请求被发送到服务器的地址 ,该程序就会被调用。 - + +代码使用 [http](https://nodejs.org/docs/latest-v18.x/api/http.html) 模块的 _createServer_ 方法来创建一个网络服务端。服务端注册了一个事件处理函数每当有 HTTP 请求发送到服务端地址 时,就会调用该函数。 - 该请求被响应,状态代码为 200,Content-Type 头设置为 text/plain,要返回的网站内容设置为 Hello World。 - +请求会以状态码 200 响应,响应的 Content-Type 标头设为 text/plain,要返回的网站内容设为 Hello World - 最后几行绑定了分配给 _app_ 变量的 http 服务器,以监听发送到 3001 端口的 HTTP 请求。 +最后几行将 http 服务端赋值给 _app_ 变量,再绑定到 3001 端口,以监听发送到 3001 端口的 HTTP 请求。 ```js const PORT = 3001 @@ -232,9 +211,8 @@ app.listen(PORT) console.log(`Server running on port ${PORT}`) ``` - - - 本课程中后端服务器的主要目的是向前端提供 JSON 格式的原始数据。出于这个原因,让我们立即改变我们的服务器以 JSON 格式返回一个硬编码的笔记列表。 + +本课程中后端服务端的主要目的是向前端提供 JSON 格式的原始数据。因此,让我们立即改变服务端,让它以 JSON 格式返回一个硬编码的笔记列表。 ```js const http = require('http') @@ -242,21 +220,18 @@ const http = require('http') // highlight-start let notes = [ { - id: 1, + 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", + id: "2", + content: "Browser can execute only JavaScript", important: false }, { - id: 3, + id: "3", content: "GET and POST are the most important methods of HTTP protocol", - date: "2022-05-30T19:20:14.298Z", important: true } ] @@ -273,92 +248,86 @@ console.log(`Server running on port ${PORT}`) ``` - 让我们重新启动服务器(你可以在控制台中按 _Ctrl+C_ 来关闭服务器),让我们刷新浏览器。 +让我们重启服务端(你可以在控制台中按 _Ctrl+C_ 来关闭服务端),然后刷新浏览器。 - - 在 Content-Type 头中的 application/json 值通知接收者,数据是 JSON 格式的。_notes_ 数组通过 JSON.stringify(notes) 方法被转换为 JSON。 + +Content-Type 标头中的 application/json 值告诉接收者数据是 JSON 格式的。_notes_ 数组通过 JSON.stringify(notes) 方法转换为 JSON 格式的字符串。这一步是必要的,因为 response.end() 方法只接受一个字符串或缓冲区来作为响应体。 - 当我们打开浏览器时,显示的格式与 [第二章节](/en/part2/getting_data_from_server/) 中完全一样,在那里我们使用 [json-server](https://github.com/typicode/json-server) 来提供笔记的列表。 +当我们打开浏览器时,显示的格式与[第 2 章节](/zh/part2/获取服务端的数据)中我们使用 [json-server](https://github.com/typicode/json-server) 来提供的笔记列表完全一样: -![](../../images/3/2e.png) +![](../../images/3/2new.png) ### Express - - 用 Node 内置的 [http](https://nodejs.org/docs/latest-v8.x/api/http.html) 网络服务器直接实现我们的服务器代码是可行的。然而,这很麻烦,特别是一旦应用的规模扩大。 + +直接用 Node 内置的 [http](https://nodejs.org/docs/latest-v8.x/api/http.html) web server 实现服务端代码是可行的。然而,这很麻烦,特别是一旦应用的规模扩大的话,就更麻烦了。 - - 许多库已经被开发出来,通过提供一个更讨人喜欢的接口来与内置的 http 模块一起工作,从而缓解 Node 的服务器端开发。这些库的目的是为我们通常需要建立后端服务器的一般使用情况提供一个更好的抽象。到目前为止,用于这一目的的最流行的库是 [express](http://expressjs.com)。 + +已经有许多简化 Node 的服务端开发的库,这些库提供一个更好用的接口来与内置的 http 模块一起工作。这些库的目的是为我们建立后端服务端通常需要的通用情况提供更好的抽象概念。目前为止,用于这一目的的最流行的库是 [Express](http://expressjs.com)。 - - 让我们用命令将 express 定义为项目的依赖关系来使用它。 + +让我们用下列命令将 Express 定义为项目的依赖项,然后来使用它: ```bash npm install express ``` - 这个依赖关系也被添加到我们的 package.json 文件中。 +这个依赖项也加进了 package.json 文件中: ```json { // ... "dependencies": { - "express": "^4.17.2" + "express": "^5.1.0" } } - ``` - - 该依赖的源代码被安装到位于项目根部的 node/modules 目录中。除了 express 之外,你还可以在该目录中找到大量的其他依赖关系。 +依赖项的源码被安装到项目根目录下的 node\_modules 目录中。除了 Express 之外,你还可以在该目录中找到大量其他的依赖项: ![](../../images/3/4.png) + +这些实际上是 Express 库的依赖项,以及它所有依赖项的依赖项,等等。这些统称为我们项目的[传递性依赖项](https://lexi-lambda.github.io/blog/2016/08/24/understanding-the-npm-dependency-model/)。 - - 这些实际上是 express 库的依赖关系,以及它所有的依赖关系,等等。这些被称为我们项目的 [transitive dependencies](https://lexi-lambda.github.io/blog/2016/08/24/understanding-the-npm-dependency-model/)。 - - - - 我们的项目中安装了 4.17.2. 版本的 express。在 package.json 中版本号前面的圆点是什么意思? + +项目中安装了 5.1.0 版本的 Express。在 package.json 中版本号前面的脱字号 *^* 是什么意思? ```json -"express": "^4.17.2" +"express": "^5.1.0" ``` - - npm 中使用的版本管理模式被称为 [语义版本管理](https://docs.npmjs.com/getting-started/semantic-versioning)。 - - - - ^4.17.2 前面的圆点意味着如果项目的依赖关系被更新,所安装的 express 版本将至少是 4.17.2。然而,安装的 express 版本也可以是具有更大的 patch 号(最后一个数字),或者更大的 minor 号(中间的数字)。第一个 major 数字所表示的库的主要版本必须是相同的。 +npm 中使用的版本模式称为[语义化版本](https://docs.npmjs.com/getting-started/semantic-versioning)。 + +^5.1.0 前面的脱字符 *^* 意味着将来如果项目的依赖项更新了,安装的 Express 版本最低是 5.1.0。然而,安装的 Express 版本也可以有更大的修订号(最后一个数字),或者更大的次版本号(中间的数字)。第一个主版本数字表示的库的主版本必须是相同的。 - 我们可以用命令更新项目的依赖关系。 +我们可以用下列命令更新项目的依赖项: ```bash npm update ``` - - 同样地,如果我们在另一台电脑上开始做这个项目,我们可以用命令安装 package.json 中定义的项目的所有最新的依赖项。 + +同样地,如果我们在另一台电脑上开始这个项目,我们可以在项目的根目录下用以下命令安装 package.json 中定义的项目所有最新的依赖项。 ```bash npm install ``` - - 如果一个依赖项的 major 号没有改变,那么较新的版本应该是 [向后兼容](https://en.wikipedia.org/wiki/Backward_compatibility)。这意味着,如果我们的应用在未来碰巧使用了 4.99.175 版本的 Express,那么这部分实现的所有代码仍然要工作,而无需对代码进行修改。相反,未来的 5.0.0. 版本的 express[可能包含](https://expressjs.com/en/guide/migrating-5.html) 变化,会导致我们的应用不再工作。 + +如果依赖项的主版本号没有改变,那么新版本应该是[向后兼容](https://en.wikipedia.org/wiki/Backward_compatibility)的。这意味着,如果我们的应用将来使用了 5.99.175 版本的 Express,那么本章节中实现的所有代码仍然能正确运行,无需修改。相反,未来 6.0.0 版本的 Express 可能包含会导致我们的应用无法正确运行的更改。 -### Web and express + +### Web 和 Express - 让我们回到我们的应用,并做如下修改。 +让我们回到我们的应用,并做如下修改: ```js const express = require('express') @@ -382,22 +351,19 @@ app.listen(PORT, () => { }) ``` - - -为了让我们的应用的新版本投入使用,我们必须重新启动应用。 - + +为了使用新版本的应用,首先必须重启应用。 - 应用并没有发生很大的变化。就在我们代码的开头,我们导入了 _express_,这次是一个 函数 ,用来创建一个存储在 _app_ 变量中的 Express 应用。 +应用并没有发生很大的变化。在代码的开头,我们导入了 _express_,这次 _express_ 是一个函数,用来创建 Express 应用并将其存储在 _app_ 变量中。 ```js const express = require('express') const app = express() ``` - - 接下来,我们定义两个通往应用的 路径 。第一个定义了一个事件处理程序,用来处理对应用 / 根的 HTTP GET 请求。 +接下来,我们为应用定义了两个路由。第一个路由定义了处理向应用的 / 根发送的 HTTP GET 请求的事件处理函数。 ```js app.get('/', (request, response) => { @@ -405,23 +371,19 @@ app.get('/', (request, response) => { }) ``` - - 该事件处理函数接受两个参数。第一个 [request](http://expressjs.com/en/4x/api.html#req) 参数包含 HTTP 请求的所有信息,第二个 [response](http://expressjs.com/en/4x/api.html#res) 参数用于定义如何对请求进行响应。 - - - - 在我们的代码中,请求是通过使用 _response_ 对象的 [send](http://expressjs.com/en/4x/api.html#res.send) 方法回答的。调用该方法使服务器响应 HTTP 请求,发送一个响应,其中包含传递给 _send_ 方法的字符串

Hello World!

。由于参数是一个字符串,Express 自动将 Content-Type 头的值设置为 text/html。响应的状态代码默认为 200。 +该事件处理函数接受两个参数。第一个 [request](http://expressjs.com/en/4x/api.html#req) 参数包含 HTTP 请求的所有信息,第二个 [response](http://expressjs.com/en/4x/api.html#res) 参数用于定义如何对请求进行响应。 + +在我们的代码中,请求是通过使用 _response_ 对象的 [send](http://expressjs.com/en/4x/api.html#res.send) 方法响应的。调用该方法会使服务端响应 HTTP 请求,发送一个响应,内容包含传递给 _send_ 方法的 \

Hello World!\

字符串。由于参数是一个字符串,Express 自动将 Content-Type 标头的值设为 text/html。响应的状态码默认为 200。 - 我们可以从开发者工具中的 网络 标签来验证这一点。 +我们可以在开发者工具中的网络标签页中验证: ![](../../images/3/5.png) - - 第二个路由定义了一个事件处理程序,处理向应用的 notes 路径发出的 HTTP GET 请求。 +第二个路由定义了处理向应用的 notes 路径发送的 HTTP GET 请求的事件处理函数。 ```js app.get('/api/notes', (request, response) => { @@ -429,205 +391,145 @@ app.get('/api/notes', (request, response) => { }) ``` - - 该请求用 _response_ 对象的 [json](http://expressjs.com/en/4x/api.html#res.json) 方法来响应。调用该方法将发送传给它的 __notes__ 数组,作为 JSON 格式的字符串。Express 自动将 Content-Type 头设置为 application/json 的适当值。 +请求是用 _response_ 对象的 [json](http://expressjs.com/en/4x/api.html#res.json) 方法响应的。调用该方法将以 JSON 格式的字符串发送传给它的 __notes__ 数组。Express 自动将 Content-Type 标头设为合适值 application/json。 -![](../../images/3/6ea.png) +![](../../images/3/6new.png) - 接下来,让我们快速浏览一下以 JSON 格式发送的数据。 +接下来,让我们快速浏览一下以 JSON 格式发送的数据。 - 在早期版本中,我们只使用 Node,我们必须用 _JSON.stringify_ 方法将数据转换成 JSON 格式。 +在先前只使用 Node 的版本中,我们必须用 _JSON.stringify_ 方法将数据转换成 JSON 格式的字符串: ```js response.end(JSON.stringify(notes)) ``` - - 有了 Express,这就不再需要了,因为这种转换会自动发生。 - - - - 值得注意的是,[JSON](https://en.wikipedia.org/wiki/JSON) 是一个字符串,而不是像分配给 _notes_ 的值那样的一个 JavaScript 对象。 +有了 Express,就不再需要这么做了,这一转换会自动进行。 + +值得注意的是,[JSON](https://en.wikipedia.org/wiki/JSON) 是一种数据格式。然而,它常以字符串形式表示,并且不等同于 JavaScript 对象,比如赋给 _notes_ 的值。 - 下面的实验说明了这一点。 +下面的实验说明了这一点: ![](../../assets/3/5.png) - - 上面的实验是在交互式 [node-repl](https://nodejs.org/docs/latest-v8.x/api/repl.html) 中完成的。你可以通过在命令行中输入 _node_ 来启动交互式 node-repl。在你写应用代码的时候,这个副本对于测试命令如何工作特别有用。我强烈建议这样做 ! - -### nodemon - - - 如果我们对应用的代码做了修改,我们必须重新启动应用,以便看到这些修改。我们重启应用的方法是:首先通过输入 _Ctrl+C_ 来关闭它,然后再重启应用。与 React 中方便的工作流程相比,即浏览器在发生变化后自动重新加载,这感觉有点麻烦。 +上面的实验是在交互式 [node-repl](https://nodejs.org/docs/latest-v8.x/api/repl.html) 中完成的。你可以通过在命令行中输入 _node_ 来启动交互式 node-repl。在你写应用代码的时候,这个交互窗口对测试命令是如何运行的特别有用。我强烈推荐这一工具! - - 解决这个问题的方法是 [nodemon](https://github.com/remy/nodemon)。 + +### 自动跟踪更改 - - > nodemon 将观察 nodemon 启动时所在目录中的文件,如果有任何文件发生变化,nodemon 将自动重启你的 node 应用 + +如果我们更改了应用的代码,我们首先需要在终端中停止应用(_ctrl_ + _c_),然后再重启应用来使更改生效。相比 React 代码更改后,浏览器自动重新加载的丝滑的工作流,重启总感觉很麻烦。 - - - 让我们用命令将 nodemon 定义为一个 开发依赖项 来安装它。 + +你可以通过用 _--watch_ 选项启动应用来让服务端跟踪更改: ```bash -npm install --save-dev nodemon +node --watch index.js ``` - - package.json 的内容也有变化。 - -```json -{ - //... - "dependencies": { - "express": "^4.17.2", - }, - "devDependencies": { - "nodemon": "^2.0.15" - } -} -``` - - - - 如果你不小心用错了命令,nodemon 依赖被添加到了 "dependencies " 下,而不是 "devDependencies " 下,那么请手动修改 package.json 的内容,以符合上面的内容。 - - - - 我们所说的开发依赖,指的是只在应用的开发过程中需要的工具,例如用于测试或自动重启应用,如 nodemon。 - - - -当应用在生产服务器(如 Heroku)上以生产模式运行时,不需要这些开发依赖性。 - - - - 我们可以像这样用 nodemon 启动我们的应用。 - -```bash -node_modules/.bin/nodemon index.js -``` + +现在,修改应用代码会自动重启服务端。注意即使服务端能自动重启,你仍然需要刷新浏览器。不同于 React,我们在这种情况(返回的是 JSON 数据)下没有,也无法有自动更新浏览器的热重载功能。 + +让我们在 package.json 文件中自定义一个 npm 脚本来启动开发服务端。 - -现在对应用代码的修改会导致服务器自动重新启动。值得注意的是,即使后端服务器自动重启,浏览器仍然需要手动刷新。这是因为与在 React 中工作时不同,我们没有自动重新加载浏览器所需的 [hot reload](https://gaearon.github.io/react-hot-loader/getstarted/) 功能。 - - - - 这个命令很长,而且很不讨人喜欢,所以让我们在 package.json 文件中为它定义一个专门的 npm 脚本 。 - -```bash +```json { // .. "scripts": { "start": "node index.js", - "dev": "nodemon index.js", // highlight-line + "dev": "node --watch index.js", // highlight-line "test": "echo \"Error: no test specified\" && exit 1" }, // .. } ``` - - - 在脚本中不需要指定 nodemon 的 node/_modules/.bin/nodemon 路径,因为 _npm_ 自动知道从该目录中搜索该文件。 - - - - 我们现在可以用命令在开发模式下启动服务器。 + +我们现在可以用以下命令在开发模式下启动服务端 ```bash npm run dev ``` - - - 与 starttest 脚本不同,我们还必须在命令中加入 run。 - + +不同于运行 starttest 脚本,在运行 dev 脚本时,必须在命令中加入 run。 ### REST - - 让我们扩展我们的应用,使其提供与 [json-server](https://github.com/typicode/json-server#routes) 一样的 RESTful HTTP API。 - +让我们扩展我们的应用,使其提供与 [json-server](https://github.com/typicode/json-server#routes) 一样的 RESTful HTTP API。 - Representational State Transfer,又称 REST,于 2000 年在 Roy Fielding 的 [论文](https://www.ics.uci.edu/~fielding/pubs/dissertation/rest_arch_style.htm) 中提出。REST 是一种架构风格,旨在建立可扩展的网络应用。 - - - - 我们不打算深入研究 Fielding 对 REST 的定义,也不打算花时间去思考什么是 RESTful 和什么不是。相反,我们将采取一个更 [狭窄的观点](https://en.wikipedia.org/wiki/Representational_state_transfer#Applied_to_web_services),只关注 RESTful APIs 在网络应用中的典型理解。事实上,REST 的原始定义甚至不限于网络应用。 +表现层状态转换(Representational State Transfer),又称 REST,是 Roy Fielding 于 2000 年在[论文](https://www.ics.uci.edu/~fielding/pubs/dissertation/rest_arch_style.htm)中提出的。REST 是一种旨在建立可扩展的 web 应用的架构风格。 + +我们不会深入研究 Fielding 对 REST 的定义,也不会花时间琢磨什么是 RESTful,什么不是。我们会把眼光放得[狭隘](https://en.wikipedia.org/wiki/Representational_state_transfer#Applied_to_web_services)点,只关注 web 应用中通常是如何理解 RESTful API 的。REST 的原始定义甚至不限于网络应用。 - 我们在 [前一部分](/en/part2/altering_data_in_server#rest) 中提到,在 RESTful 思想中,单一的东西,如我们应用中的笔记,被称为 资源 。每个资源都有一个相关的 URL,这是资源的唯一地址。 - - - - 一个惯例是通过结合资源类型的名称和资源的唯一标识符来创建资源的唯一地址。 +我们在[前一章节](/zh/part2/修改服务端的数据#rest)中提到,单个事物,比如我们应用中的笔记,在 RESTful 思想中称为资源。每个资源都有与之相关的 URL,即资源的唯一地址。 + +创建资源唯一地址的一个惯例是将资源类型的名称和资源的唯一标识符结合起来。 - 让我们假设我们的服务的根 URL 是 www.example.com/api。 - +假设我们服务的根 URL 是 www.example.com/api - 如果我们把笔记的资源类型定义为 笔记 ,那么标识符为 10 的笔记资源的地址就有唯一的地址 www.example.com/api/notes/10。 - +如果我们定义笔记的资源类型为 notes,那么标识符为 10 的笔记资源就有唯一地址 www.example.com/api/notes/10 - 所有笔记资源的整个集合的 URL 是 www.example.com/api/notes。 - +所有笔记资源的整个集合的 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 所指的 [统一接口](https://en.wikipedia.org/wiki/Representational_state_transfer#Architectural_constraints),这意味着一种定义接口的一致方式,使系统有可能合作。 - - - -这种解释 REST 的方式属于 Richardson 成熟度模型中的 [RESTful 成熟度第二层次](https://martinfowler.com/articles/richardsonMaturityModel.html)。根据 Roy Fielding 提供的定义,我们实际上并没有定义一个 [REST API](http://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertext-driven)。事实上,世界上绝大部分所谓的 "REST "API 都不符合 Fielding 在其论文中列出的原始标准。 - +我们可以对资源执行不同的操作。要执行的操作是通过 HTTP 动词定义的: + + +| URL | 动词 | 功能 | +| --------------------- | ------------------- | --------------------------------| +| notes/10 | GET | 获取单个资源 | +| notes | POST | 根据请求数据新建一个资源 | +| notes/10 | DELETE | 删除指定标识符的资源 | +| notes/10 | PUT | 将指定标识符的资源整个替换为请求的数据 | +| notes/10 | PATCH | 将指定标识符的资源部分替换为请求的数据 | +| | | | + + +这就是我们设法粗略定义的 REST 所指的 [统一接口](https://en.wikipedia.org/wiki/Representational_state_transfer#Architectural_constraints),也就是一种一致的定义接口的方式,从而使不同系统之间可以合作。 + + +这种解释 REST 的方式属于 Richardson 成熟度模型中 [RESTful 成熟度的第二级](https://martinfowler.com/articles/richardsonMaturityModel.html)。根据 Roy Fielding 提供的定义,我们定义的还不是 [REST API](http://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertext-driven)。事实上,世界上绝大部分所谓的“REST” API 都不符合 Fielding 在其论文中列出的原始标准。 - 在某些地方(例如见 [Richardson, Ruby: RESTful Web Services](http://shop.oreilly.com/product/9780596529260.do)),你会看到我们的直接 [CRUD](https://en.wikipedia.org/wiki/Create,_read,_update_and_delete)API 模型被称为 [面向资源架构](https://en.wikipedia.org/wiki/Resource-oriented_architecture) 的例子,而不是 REST。我们将避免陷入语义学的争论,而是回到我们的应用上工作。 - - -### Fetching a single resource +在某些地方(例如 [Richardson 的 Ruby: RESTful Web Services](http://shop.oreilly.com/product/9780596529260.do)),你会看到我们直接[增删改查](https://en.wikipedia.org/wiki/Create,_read,_update_and_delete)的 API 模型被称为是[面向资源架构](https://en.wikipedia.org/wiki/Resource-oriented_architecture)的示例,而非 REST 的。我们不要卡在咬文嚼字上,而是回到我们的应用。 + +### 获取单个资源 - 让我们扩展我们的应用,使其提供一个 REST 接口来操作单个笔记。首先,让我们创建一个 [路由](http://expressjs.com/en/guide/routing.html) 来获取一个单一的资源。 - +让我们扩展我们的应用,使其提供一个 REST 接口来操作单个笔记。首先,让我们创建一个用来获取单个资源的[路由](http://expressjs.com/en/guide/routing.html)。 - 我们将为单个笔记使用的唯一地址的形式是 notes/10,其中末尾的数字是指笔记的唯一 ID 号。 - +我们将为单个笔记使用的唯一地址的形式是 notes/10,其中末尾的数字指笔记的唯一 id。 - 我们可以通过使用冒号语法为 Express 中的路由定义 [参数](http://expressjs.com/en/guide/routing.html#route-parameters)。 +在 Express 中,可以用冒号语法来为路由定义[参数](http://expressjs.com/en/guide/routing.html#route-parameters): ```js app.get('/api/notes/:id', (request, response) => { @@ -637,122 +539,45 @@ app.get('/api/notes/:id', (request, response) => { }) ``` - - 现在 app.get("/api/notes/:id", ...) 将处理所有形式为 /api/notes/SOMETHING 的 HTTP GET 请求,其中 SOMETHING 是一个任意的字符串。 +现在 app.get('/api/notes/:id', ...) 将处理所有形式为 /api/notes/SOMETHING 的 HTTP GET 请求,其中 SOMETHING 是一个任意的字符串。 - - - 请求路径中的 id 参数,可以通过 [request](http://expressjs.com/en/api.html#req) 对象访问。 + +请求的路由中的 id 参数可以通过 [request](http://expressjs.com/en/api.html#req) 对象访问: ```js const id = request.params.id ``` - 现在熟悉的数组的 _find_ 方法被用来寻找与参数相匹配的 id 的笔记。然后,该笔记被返回给请求的发送者。 - - - - 当我们在浏览器中访问 来测试我们的应用时,我们注意到它似乎没有工作,因为浏览器显示的是一个空页面。这对我们这些软件开发者来说并不奇怪,是时候进行调试了。 - - - - 在我们的代码中添加 _console.log_ 命令是一个经过时间验证的技巧。 - -```js -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) -}) -``` - - - -当我们在浏览器中再次访问 时,控制台,也就是本例中的终端,将显示如下内容。 - -![](../../images/3/8.png) - - - - 路由中的 id 参数被传递给我们的应用,但 _find_ 方法没有找到一个匹配的笔记。 - - - - 为了进一步调查,我们还在传递给 _find_ 方法的比较函数里面添加了一个控制台日志。为了做到这一点,我们必须摆脱紧凑的箭头函数语法 note => note.id === id,而使用带有明确返回语句的语法。 - -```js -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 -``` - +我们用现在已经熟悉了的数组的 _find_ 方法来寻找 id 与参数匹配的笔记。然后,该笔记被返回给请求的发送方。 - - 错误的原因变得清晰了。_id_ 变量包含一个字符串 "1",而笔记的 id 是整数。在 JavaScript 中,"triple equals " 比较 === 认为所有不同类型的值默认是不相等的,也就是说,1 不是 "1"。 - - - - 让我们通过把 id 参数从一个字符串变成一个 [数字](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number) 来解决这个问题。 - -```js -app.get('/api/notes/:id', (request, response) => { - const id = Number(request.params.id) // highlight-line - const note = notes.find(note => note.id === id) - response.json(note) -}) -``` - - - - 现在获取一个单独的资源可以了。 - -![](../../images/3/9ea.png) + +现在我们可以在浏览器访问 来测试应用了: +![](../../images/3/9new.png) - 然而,我们的应用还有一个问题。 - +然而,我们的应用还有一个问题。 - 如果我们搜索一个 id 不存在的笔记,服务器的反应是。 +如果我们搜索一个 id 不存在的笔记,服务端的响应是: ![](../../images/3/10ea.png) - - 返回的 HTTP 状态代码是 200,这意味着响应成功了。由于 content-length 头的值为 0,所以没有数据随响应一起被送回来,同样可以从浏览器中验证。 +返回的 HTTP 状态码是 200,意味着响应成功。由于 content-length 标头的值为 0,所以该响应没有发回数据,这一点也可以从浏览器中验证。 - - - 出现这种行为的原因是,如果没有找到匹配的笔记,_note_ 变量被设置为 _undefined_。这种情况需要在服务器上以更好的方式来处理。如果没有找到笔记,服务器应该用状态代码 [404 not found](https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.5) 来响应,而不是 200。 + +出现这种行为的原因是,如果没有找到匹配的笔记,会将 _note_ 变量设为 _undefined_。服务端需要以更好的方式来处理这种情况。如果没有找到笔记,服务端应该响应状态码 [404 not found](https://www.rfc-editor.org/rfc/rfc9110.html#name-404-not-found),而非 200。 - 让我们对我们的代码做如下修改。 +让我们对代码做如下修改: ```js app.get('/api/notes/:id', (request, response) => { - const id = Number(request.params.id) + const id = request.params.id const note = notes.find(note => note.id === id) // highlight-start @@ -767,98 +592,101 @@ app.get('/api/notes/:id', (request, response) => { - 由于响应中没有附加数据,我们使用 [status](http://expressjs.com/en/4x/api.html#res.status) 方法来设置状态,并使用 [end](http://expressjs.com/en/4x/api.html#res.end) 方法来响应请求,而不发送任何数据。 - +由于响应中没有数据,我们使用 [status](http://expressjs.com/en/4x/api.html#res.status) 方法来设置状态,并使用 [end](http://expressjs.com/en/4x/api.html#res.end) 方法来响应请求,而不发送任何数据。 - if 条件利用了这样一个事实,即所有的 JavaScript 对象都是 [truthy](https://developer.mozilla.org/en-US/docs/Glossary/Truthy),意味着它们在比较操作中计算为真。然而,_undefined_ 是 [falsy](https://developer.mozilla.org/en-US/docs/Glossary/Falsy),这意味着它将被计算为假。 - +if 条件利用了这样一个事实——所有 JavaScript 对象都是[真值](https://developer.mozilla.org/en-US/docs/Glossary/Truthy),也就是在比较操作中会被计算为 true。然而,_undefined_ 是[假值](https://developer.mozilla.org/en-US/docs/Glossary/Falsy),也就是会被计算为 false。 - - 我们的应用工作了,如果没有找到笔记,就会发送错误状态代码。然而,应用并没有返回任何东西给用户看,就像 Web 应用在我们访问一个不存在的页面时通常做的那样。我们实际上不需要在浏览器中显示任何东西,因为 REST APIs 是用于编程的接口,而错误状态代码是需要的全部内容。 + +现在我们的应用能在没有找到笔记的情况下正确运行并发送错误状态码。然而,应用并没有返回任何显示给用户的内容,正如 web 应用在我们访问不存在的页面时通常做的那样。因为 REST API 是用于编程的接口,我们不需要在浏览器中显示任何内容,只需要错误状态码就够了。 - 无论如何,我们可以通过 [覆盖默认的 NOT FOUND 信息](https://stackoverflow.com/questions/14154337/how-to-send-a-custom-http-status-message-in-node-express/36507614#36507614) 来提供一个关于发送 404 错误的原因的线索。 - - -### Deleting resources +不过,我们可以通过[重载默认的 NOT FOUND 信息](https://stackoverflow.com/questions/14154337/how-to-send-a-custom-http-status-message-in-node-express/36507614#36507614)来提示关于发送 404 错误的原因。 + +### 删除资源 - 接下来让我们实现一个删除资源的路径。删除是通过向资源的 URL 发出 HTTP DELETE 请求来实现的。 +接下来,让我们实现一个用于删除资源的路由。删除是通过向资源的 URL 发送 HTTP DELETE 请求进行的: ```js app.delete('/api/notes/:id', (request, response) => { - const id = Number(request.params.id) + const id = request.params.id notes = notes.filter(note => note.id !== id) response.status(204).end() }) ``` - - 如果删除资源是成功的,也就是说,笔记存在并且被删除了,我们用状态代码 [204 无内容](https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.2.5) 来响应请求,并且在响应中不返回数据。 + +如果删除资源成功,也就是笔记之前存在并且已删除,我们就以状态码 [204 no content](https://www.rfc-editor.org/rfc/rfc9110.html#name-204-no-content) 响应请求,并且在响应中不返回数据。 - - - 对于在资源不存在的情况下应该向 DELETE 请求返回什么状态码,目前还没有达成共识。实际上,唯一的两个选择是 204 和 404。为了简单起见,我们的应用在这两种情况下都会用 204 来响应。 + +对于资源不存在的情况下应该向 DELETE 请求返回什么状态码,目前还没有达成共识。仅有的两种选择是 204 和 404。为简单起见,我们的应用在这两种情况下都响应 204。 ### Postman - 那么我们如何测试删除操作呢?HTTP GET 请求很容易从浏览器中发出。我们可以写一些 JavaScript 来测试删除操作,但写测试代码并不总是在每种情况下的最佳解决方案。 +那么我们如何测试删除操作呢?HTTP GET 请求可以简单从浏览器发送。我们可以写一些 JavaScript 来测试删除操作,但写测试代码并不在每种情况下都是最佳解决方案。 - 有许多工具可以使后端测试更容易。其中之一是一个命令行程序 [curl](https://curl.haxx.se)。然而,我们将看一下使用 [Postman](https://www.postman.com) 来测试应用,而不是 curl。 +有许多方便测试后端的工具。其中之一是命令行程序 [curl](https://curl.haxx.se)。但我们不用 curl,而是来看一下用 [Postman](https://www.postman.com) 来测试应用。 - 让我们安装 Postman 桌面客户端 [从这里](https://www.postman.com/downloads/) 并尝试一下。 +让我们[从这里](https://www.postman.com/downloads/)安装 Postman 桌面客户端并尝试一下: ![](../../images/3/11x.png) + +注:Postman 在 VS Code 中也可用,通过左侧的“扩展”选项卡 -> 搜索 Postman -> 第一个结果(已验证的发布者) -> 下载 + +然后你就可以在活动栏的“扩展”选项卡下面看到添加了一个额外的图标。一旦登录,你就可以照着下面的步骤做了 + - 在这种情况下,使用 Postman 是非常容易的。只需定义网址,然后选择正确的请求类型(DELETE)。 +在这种情况下使用 Postman 非常简单。只要定义网址,然后选择正确的请求类型(DELETE)就可以了。 - 后端服务器似乎反应正确。通过对 的 HTTP GET 请求,我们看到 id 为 2 的笔记已经不在列表中了,这表明删除成功了。 +后端服务端正确显示了响应。通过向 发送 HTTP GET 请求,我们看到 id 为 2 的笔记已经不在列表中了,也就表明删除成功了。 - - 因为应用中的笔记只保存在内存中,所以当我们重新启动应用时,笔记的列表将恢复到原来的状态。 + +因为现在应用中的笔记是硬编码的,还没有存进数据库中,所以当重启应用时,笔记的列表将重置回原来的状态。 -### The Visual Studio Code REST client + +### Visual Studio Code REST Client - 如果你使用 Visual Studio Code,你可以使用 VS Code [REST 客户端](https://marketplace.visualstudio.com/items?itemName=humao.rest-client) 插件,而不是 Postman。 +如果你使用 Visual Studio Code,你可以使用 VS Code [REST Client](https://marketplace.visualstudio.com/items?itemName=humao.rest-client) 插件来代替 Postman。 - 一旦插件安装完毕,使用它就非常简单。我们在应用的根部建立一个名为 requests 的目录。我们将所有的 REST 客户端请求保存在该目录中,作为以 .rest 扩展名结尾的文件。 +一旦插件安装好后,用起来就非常简单。在应用的根目录新建一个名为 requests 的目录。将所有 REST 客户端请求作为以 .rest 扩展名结尾的文件保存进该目录中。 - 让我们创建一个新的 get\_all\_notes.rest 文件并定义获取所有笔记的请求。 +让我们新建一个 get\_all\_notes.rest 文件并定义获取所有笔记的请求。 ![](../../images/3/12ea.png) - 通过点击 发送请求 文本,REST 客户端将执行 HTTP 请求,来自服务器的响应在编辑器中打开。 +通过点击 Send Request 文本,REST 客户端将执行 HTTP 请求,然后在编辑器中打开服务端的响应。 ![](../../images/3/13ea.png) -### The WebStorm HTTP Client + +### WebStorm HTTP Client - - 如果你使用 *IntelliJ WebStorm*,你可以使用其内置的 HTTP 客户端的类似程序。创建一个扩展名为 ".rest " 的新文件,编辑器将显示你创建和运行请求的选项。你可以按照 [本指南](https://www.jetbrains.com/help/webstorm/http-client-in-product-code-editor.html) 来了解更多信息。 + +如果你使用的是 *IntelliJ WebStorm*,你可以用其内置的 HTTP Client 进行类似的步骤。新建一个扩展名为 `.rest` 的文件,编辑器将显示创建和运行请求的选项。你可以按[本指南](https://www.jetbrains.com/help/webstorm/http-client-in-product-code-editor.html)了解更多信息。 -### Receiving data + +### 获取数据 - - 接下来,让我们实现向服务器添加新笔记的功能。通过向 ,并在请求 [body](https://www.w3.org/Protocols/rfc2616/rfc2616-sec7.html#sec7) 中以 JSON 格式发送新笔记的所有信息,就可以添加一个笔记。 - + +接下来,让我们实现向服务端添加新笔记的功能。添加笔记是通过向地址 发送 HTTP POST 请求,并在请求[体](https://www.rfc-editor.org/rfc/rfc9112#name-message-body)中以 JSON 格式发送新笔记的所有信息来完成的。 - - 为了方便地访问数据,我们需要 express [json-parser](https://expressjs.com/en/api.html) 的帮助,它可以通过命令 _app.use(express.json())_ 来使用。 + +为了方便地访问数据,我们需要借助 Express [json-parser](https://expressjs.com/en/api.html),可以通过命令 _app.use(express.json())_ 来使用它。 - 让我们激活 json-parser 并实现一个初始处理程序来处理 HTTP POST 请求。 +让我们启用 json-parser 并实现处理 HTTP POST 请求的处理函数的初始版本。 ```js const express = require('express') @@ -880,60 +708,60 @@ app.post('/api/notes', (request, response) => { - 事件处理函数可以访问 _request_ 对象的 body 属性中的数据。 +事件处理函数可以从 _request_ 对象的 body 属性中获取数据。 - - 如果没有 json-parser,body 属性将是未定义的。json-parser 的功能是将请求的 JSON 数据转化为 JavaScript 对象,然后在调用路由处理程序之前将其附加到 _request_ 对象的 body 属性。 + +如果没有启用 json-parser,body 属性将是 undefined。json-parser 会获取请求的 JSON 数据,将其转化为 JavaScript 对象,然后在调用路由处理程序之前将该对象附加到 _request_ 对象的 body 属性上。 - 就目前而言,应用除了将收到的数据打印到控制台并在响应中送回外,并没有对其做任何处理。 +目前而言,应用只是将收到的数据打印到控制台并在响应中发回,并没有对数据做任何处理。 - - 在我们实现其余的应用逻辑之前,让我们用 Postman 验证数据是否真的被服务器收到。除了在 Postman 中定义 URL 和请求类型外,我们还必须定义在 body 中发送的数据。 + +在我们实现应用的其余逻辑之前,让我们用 Postman 验证服务端是否确实收到了数据。在 Postman 中,除了定义 URL 和请求类型外,我们还必须定义 body 中发送的数据: -![](../../images/3/14x.png) +![](../../images/3/14new.png) - 应用将我们在请求中发送的数据打印到控制台。 +应用将我们在请求中发送的数据打印到控制台: -![](../../images/3/15new.png) +![](../../images/3/15c.png) - - **NB** 当你在后端工作时,保持运行应用的终端始终可见 。由于 Nodemon 的存在,我们对代码的任何改动都会重新启动应用。如果你关注控制台,你将立即能够发现应用中出现的错误。 + +**注:**在后端编程时,始终保持应用运行的终端可见。任何对代码的更改都会重启开发服务端,所以通过关注控制台,你可以立即注意到应用的代码是否出现了错误。 -![](../../images/3/16.png) +![](../../images/3/16_25.png) - - 同样,检查控制台也很有用,可以确保后端在不同的情况下表现得像我们期望的那样,比如当我们用 HTTP POST 请求发送数据时。当然,当应用还在开发时,在代码中添加大量的 console.log 命令是个好主意。 + +类似的,检查控制台也很有用,可以确保后端在不同的情况下,比如当我们用 HTTP POST 请求发送数据时,都像我们期望的那样运行。当然,在开发应用时,在代码中添加大量的 console.log 命令也是可取的。 - 问题的一个潜在原因是请求中 Content-Type 头的设置不正确。如果正文的类型没有被正确定义,这种情况就会发生在 Postman 上。 +可能造成问题的原因有请求中 Content-Type 标头的值不正确。如果在 Postman 上没有正确定义请求体的类型,这种情况就会发生: -![](../../images/3/17x.png) +![](../../images/3/17new.png) - Content-Type 头被设置为 text/plain。 +Content-Type 标头的值会设为 text/plain: -![](../../images/3/18x.png) +![](../../images/3/18new.png) - 服务器似乎只收到一个空对象。 +服务端显示只收到一个空对象: -![](../../images/3/19.png) +![](../../images/3/19_25.png) - -如果头中没有正确的值,服务器将不能正确地解析数据。它甚至不会尝试猜测数据的格式,因为有 [大量](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types) 潜在的 Content-Types。 + +如果标头中没有正确的值,服务端就不能正确解析数据。服务端甚至不会尝试去猜数据的格式,因为 Content-Types 可能的值[太多](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types)了。 - 如果你使用的是 VS Code,那么你应该安装上一章中的 REST 客户端, 如果你还没有安装的话 。POST请求可以像这样使用REST客户端发送: +如果你用的是 VS Code,但是还没有安装上一章的 REST Client 的话,那么你应该现在安装。可以这么使用 REST Client 发送 POST 请求: -![](../../images/3/20eb.png) +![](../../images/3/20new.png) - 我们为这个请求创建了一个新的 create\_note.rest 文件。请求的格式是按照 [文档中的说明](https://github.com/Huachao/vscode-restclient/blob/master/README.md#usage)。 +我们为请求新建了一个 create\_note.rest 文件。请求的格式按[文档中的说明](https://github.com/Huachao/vscode-restclient/blob/master/README.md#usage)。 - 与 Postman 相比,REST 客户端的一个好处是,请求可以方便地在项目库的根部获得,而且可以分发给开发团队的每个人。你也可以使用`###`分隔符在同一个文件中添加多个请求。 +REST Client 相较于 Postman 的一个优点是,可以轻松在项目库的根目录获得请求,而且请求可以分发给开发团队的每个人。你还可以在一个文件中用 `###` 分隔符添加多个请求: ``` GET http://localhost:3001/api/notes/ @@ -949,37 +777,28 @@ content-type: application/json ``` - Postman 也允许用户保存请求,但情况可能变得相当混乱,特别是当你在多个不相关的项目上工作时。 - - - > **重要的附注**。 - - > - - > 有时当你在调试时,你可能想找出在 HTTP 请求中设置了哪些头文件。实现这一目的的方法之一是通过 _request_ 对象的 [get](http://expressjs.com/en/4x/api.html#req.get) 方法,该方法可用于获取单个头的值。_request_ 对象也有 headers 属性,它包含一个特定请求的所有头信息。 - - > - - - > 如果你不小心在顶行和指定 HTTP 头信息的行之间添加了一个空行,VS REST 客户端就会出现问题。在这种情况下,REST 客户端解释为所有的头信息都是空的,这导致后端服务器不知道它所收到的数据是 JSON 格式的。 - - > - - - 如果在你的代码中的某个时刻,你用 _console.log(request.headers)_ 命令打印所有的请求头,你就能发现这个丢失的 Content-Type 头。 +Postman 也允许用户保存请求,但情况可能变得相当混乱,特别是当你在同时处理多个不相关的项目时。 +> +> **重要的附注** +> +> 有时当你在调试时,你可能想找出 HTTP 请求中设了哪些标头。一种方法是通过 _request_ 对象的 [get](http://expressjs.com/en/4x/api.html#req.get) 方法获取单个标头的值。_request_ 对象还有 headers 属性,包含某个请求的所有标头。 +> +> 在 VS REST Client 中,如果你不小心在第一行和指定 HTTP 标头的行之间添加了一个空行,就会出现问题。在这种情况下,REST Client 会将其解释为所有的标头都是空的,这会导致后端服务端不知道收到的数据是 JSON 格式的。 +> +> 如果你在代码中的某处用 _console.log(request.headers)_ 命令打印请求的所有标头,你就能发现 Content-Type 标头丢失。 - 让我们回到应用。一旦我们知道应用正确地接收了数据,就是最后处理请求的时候了。 +让我们回到应用。一旦我们知道应用正确接收了数据,就可以完成处理请求的代码了: ```js app.post('/api/notes', (request, response) => { const maxId = notes.length > 0 - ? Math.max(...notes.map(n => n.id)) + ? Math.max(...notes.map(n => Number(n.id))) : 0 const note = request.body - note.id = maxId + 1 + note.id = String(maxId + 1) notes = notes.concat(note) @@ -987,20 +806,18 @@ app.post('/api/notes', (request, response) => { }) ``` + +我们需要给这个笔记一个唯一的 id。首先,我们找出当前列表中最大的 id 号赋值给 _maxId_ 变量。然后将新笔记的 id 定义为 _maxId + 1_ 的字符串形式。这种方法并不推荐,但我们现在先这么用着,我们很快就会换另一种方法。 - - 我们需要一个唯一的 id 给这个笔记。首先,我们找出当前列表中最大的 ID 号码,并将其分配给 _maxId_ 变量。然后,新笔记的 id 被定义为 _maxId+1_。这种方法实际上是不推荐的,但我们现在将继续使用它,因为我们很快就会取代它。 - - - - 目前的版本仍然有一个问题,即 HTTP POST 请求可以被用来添加具有任意属性的对象。让我们通过定义 content 属性不得为空来改进这个应用。importantdate 属性将被赋予默认值。所有其他属性都被丢弃。 + +目前的版本仍有问题,HTTP POST 请求可以用来添加具有任意属性的对象。让我们改进应用,定义 content 属性不得为空。important 属性默认赋 false 值。其他所有属性都丢弃: ```js const generateId = () => { const maxId = notes.length > 0 - ? Math.max(...notes.map(n => n.id)) + ? Math.max(...notes.map(n => Number(n.id))) : 0 - return maxId + 1 + return String(maxId + 1) } app.post('/api/notes', (request, response) => { @@ -1015,7 +832,6 @@ app.post('/api/notes', (request, response) => { const note = { content: body.content, important: body.important || false, - date: new Date(), id: generateId(), } @@ -1025,13 +841,11 @@ app.post('/api/notes', (request, response) => { }) ``` - - 为笔记生成新的 ID 号码的逻辑已经被提取到一个单独的 _generateId_ 函数中。 +为笔记生成新 id 号的逻辑已经提取到专门的 _generateId_ 函数中。 - - - 如果收到的数据缺少 content 属性的值,服务器将以状态代码 [400 bad request](https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.1) 响应请求。 + +如果收到的数据缺失 content 属性的值,服务端将以状态码 [400 bad request](https://www.rfc-editor.org/rfc/rfc9110.html#name-400-bad-request) 响应请求: ```js if (!body.content) { @@ -1041,193 +855,169 @@ if (!body.content) { } ``` + +注意,调用 return 至关重要,否则代码会执行到最后,错误格式的笔记会被保存到应用中。 - - 注意,调用 return 是至关重要的,因为否则代码会执行到最后,错误的笔记会被保存到应用中。 - - - - 如果内容属性有一个值,笔记将基于收到的数据。如前所述,在服务器上生成时间戳比在浏览器中生成时间戳更好,因为我们不能相信运行浏览器的主机有正确的时钟设置。现在,date 属性的生成是由服务器完成的。 - + +如果 content 属性有值,将基于收到的数据新建笔记。 - 如果 重要的 属性丢失,我们将默认其值为 。默认值目前是以一种看起来相当奇怪的方式生成的。 +如果 important 属性丢失,我们将默认其值为 false。默认值目前是以一种看起来相当奇怪的方式生成的: ```js important: body.important || false, ``` - - - 如果保存在 _body_ 变量中的数据有 important 属性,表达式将计算为其值。如果该属性不存在,那么表达式将计算为 false,这在垂直线的右侧被定义。 - + +如果 _body_ 变量保存的数据有 important 属性,表达式将计算其值并转换为布尔值。如果 important 属性不存在,那么表达式将计算为竖线*||*右侧定义的 false。 - > 确切地说,当 重要 属性是 false 时,那么 body.important || false 表达式实际上将从右侧返回 false... - +> 准确地说,当 important 属性为 false 时,body.important || false 表达式实际上返回的是右边的 false…… - 你可以在 [这个 github 仓库](https://github.com/fullstack-hy2020/part3-notes-backend/tree/part3-1) 的 part3-1 分支中找到我们当前应用的全部代码。 - - - -目前应用状态的代码具体在分支 [part3-1](https://github.com/fullstack-hy2020/part3-notes-backend/tree/part3-1)。 +你可以在[这个 GitHub 仓库](https://github.com/fullstack-hy2020/part3-notes-backend/tree/part3-1)的 part3-1 分支中找到我们当前应用的全部代码。 ![](../../images/3/21.png) - 如果你克隆了这个项目,在用 _npm start_ 或 _npm run dev_ 启动应用之前,运行 _npm install_ 命令。 - - 在我们进入练习之前还有一件事。生成 ID 的函数目前看起来是这样的。 +在我们开始练习之前还有一件事。生成 id 的函数目前看起来是这样的: ```js const generateId = () => { const maxId = notes.length > 0 - ? Math.max(...notes.map(n => n.id)) + ? Math.max(...notes.map(n => Number(n.id))) : 0 - return maxId + 1 + return String(maxId + 1) } ``` - - 该函数主体包含了一行看起来有点耐人寻味的内容。 +函数体包含了一行看起来有点耐人寻味的内容: ```js -Math.max(...notes.map(n => n.id)) +Math.max(...notes.map(n => Number(n.id))) ``` - - 这一行代码到底发生了什么?notes.map(n => n.id) 创建一个新的数组,其中包含了所有笔记的 ID。[Math.max](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/max) 返回传递给它的数字的最大值。然而,notes.map(n => n.id) 是一个 数组 ,所以它不能直接作为一个参数给 _Math.max_。数组可以通过使用 " 三点 "[spread](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax) 语法 ... 转换为单个数字。 + +这行代码中到底发生了什么?notes.map(n => Number(n.id)) 新建了一个包含所有笔记 id 的数字形式的数组。[Math.max](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/max) 返回传递给它所有数字的最大值。然而,notes.map(n => Number(n.id)) 是一个数组,所以它不能直接作为一个参数给 _Math.max_。数组可以通过使用“三点”[展开](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax)语法 ... 转换为各个数字。
+ +### 练习 3.1.~3.6. -### Exercises 3.1.-3.6. - - - **NB:** 建议将这部分的所有练习都放到一个新的专用 git 仓库中,并将你的源代码放在仓库的根部。否则你会在练习 3.10 中遇到问题。 - - - - **NB:** 因为这不是一个前端项目,而且我们没有使用 React,所以应用 没有用 create-react-app 创建 。你用 npm init 命令来初始化这个项目,在这部分材料的前面已经演示过了。 + +**注:**因为这不是一个前端项目,我们也没有用 React,所以应用不是用 create vite@latest -- --template react 创建的。用教材本章节前面演示过的 npm init 命令来初始化这个项目。 + +**注意:**因为“node\_modules”是用“npm init”创建的,所以在你用“git add .”打算让 git 开始跟踪代码时,也会包含这个目录。因此,请创建一个名为“.gitignore”的文件并写入“node\_modules”,以便 git 在你每次尝试添加、提交或推送到远程仓库时都忽略这个目录。 - **强烈建议:** 当你在处理后端代码时,始终关注运行你的应用的终端中发生的事情。 - - -#### 3.1: Phonebook backend step1 +**强烈建议:**当你在处理后端代码时,始终关注应用运行的终端中发生了什么。 + +#### 3.1:电话簿后端 第 1 步 - 实现一个 Node 应用,从地址 返回一个硬编码的电话簿条目列表。 - +实现一个从地址 返回一个电话簿记录的硬编码列表的 Node 应用。 -数据: +数据: ```js [ - { - "id": 1, - "name": "Arto Hellas", + { + "id": "1", + "name": "Arto Hellas", "number": "040-123456" }, - { - "id": 2, - "name": "Ada Lovelace", + { + "id": "2", + "name": "Ada Lovelace", "number": "39-44-5323523" }, - { - "id": 3, - "name": "Dan Abramov", + { + "id": "3", + "name": "Dan Abramov", "number": "12-43-234345" }, - { - "id": 4, - "name": "Mary Poppendieck", + { + "id": "4", + "name": "Mary Poppendieck", "number": "39-23-6423122" } ] ``` - GET 请求后在浏览器中的输出。 +GET 请求后浏览器中的输出: ![](../../images/3/22e.png) - 注意路由 api/persons 中的正斜杠不是一个特殊字符,和字符串中的其他字符一样。 +注意路由 api/persons 中的正斜杠不是特殊字符,它和字符串中的其他字符一样。 - 应用必须用 _npm start_ 命令启动。 +应用必须用 _npm start_ 命令启动。 - 应用还必须提供一个 _npm run dev_ 命令,该命令将运行应用,并在做出改变并保存到源代码中的文件时重新启动服务器。 +应用还必须提供一个 _npm run dev_ 命令来运行应用,并在源代码文件发生更改并保存时重启服务端。 -#### 3.2: Phonebook backend step2 + +#### 3.2:电话簿 第 2 步 - 在地址 上实现一个页面,大致是这样的。 +在地址 上实现一个类似这样的页面: ![](../../images/3/23x.png) - - 该页面必须显示收到请求的时间和处理请求时电话簿中的条目数量。 - -#### 3.3: Phonebook backend step3 +该页面必须显示收到请求的时间,以及处理请求时,电话簿中有多少记录。 + +#### 3.3:电话簿 第 3 步 - 实现显示单个电话簿条目信息的功能。获取一个 ID 为 5 的人的数据的网址应该是 。 +实现显示单个电话簿记录的信息的功能。获取 id 为 5 的人的数据的 URL 应该是 - 如果没有找到给定 ID 的条目,服务器必须以适当的状态码进行响应。 - -#### 3.4: Phonebook backend step4 +如果没有找到给定 id 的记录,服务端必须响应合适的状态码。 + +#### 3.4:电话簿后端 第 4 步 - 实现功能,使其有可能通过向电话簿条目的唯一 URL 发出 HTTP DELETE 请求来删除单个电话簿条目。 - +实现可以通过向电话簿记录的唯一 URL 发送 HTTP DELETE 请求来删除单个电话簿条目的功能。 - 测试你的功能是否能与 Postman 或 Visual Studio Code REST 客户端一起工作。 - - -#### 3.5: Phonebook backend step5 +用 Postman 或 Visual Studio Code REST Client 测试你的功能是否正确。 + +#### 3.5:电话簿后端 第 5 步 - 扩展后端,使新的电话簿条目可以通过 HTTP POST 请求添加到地址 。 - +扩展后端,使得可以通过向地址 发送 HTTP POST 请求添加电话簿记录。 - 用 [Math.random](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/random) 函数为电话簿条目生成一个新的 ID。为你的随机值使用一个足够大的范围,这样创建重复 ID 的可能性就很小。 - - -#### 3.6: Phonebook backend step6 - - +用 [Math.random](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/random) 函数为添加的电话簿记录生成一个新 id。让随机值的范围足够大,这样创建重复 id 的可能性就足够小。 + +#### 3.6: 电话簿后端 第 6 步 - 实现创建新条目的错误处理。请求不允许成功,如果。 +实现新建记录的错误处理。如果出现下列情况,则请求失败: - - 名称或数字缺失 +- 名字或号码缺失 - - 名字已经存在于电话簿中 - +- 名字已经存在于电话簿中 - 用适当的状态代码响应类似这样的请求,同时发回解释错误原因的信息,例如。 +对于这样的请求,用适当的状态码响应,同时发回解释错误原因的信息,例如: ```js { error: 'name must be unique' } @@ -1237,77 +1027,65 @@ Math.max(...notes.map(n => n.id))
+ +### 关于 HTTP 请求类型 -### About HTTP request types - - - [HTTP 标准](https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html) 谈到了与请求类型有关的两个属性:**安全** 和 **空闲**。 + +[HTTP 标准](https://www.rfc-editor.org/rfc/rfc9110.html#name-common-method-properties)提到了关于请求类型的两个属性:**安全性**和**幂等性**。 - HTTP GET 请求应该是 安全的 。 +HTTP GET 请求应该是安全的 - > 特别是,约定俗成的是,GET 和 HEAD 方法不应具有除检索以外的行动意义。这些方法应该被认为是 " 安全的 "。。 - +> 特别是已经约定俗成的,GET 和 HEAD 方法不应具有除获取数据以外的任何意义。GET 和 HEAD 方法应被认为是“安全的”。 - - 安全意味着执行的请求不能在服务器中引起任何 副作用 。我们所说的副作用是指数据库的状态不能因为请求而改变,而且响应必须只返回服务器上已经存在的数据。 + +安全性意味着执行这类请求不能在服务端引起任何副作用。这里的副作用是指数据库的状态不能因请求而改变,而且响应必须只返回服务端上已有的数据。 + +还没有办法能保证 GET 请求是安全的,这只是 HTTP 标准定义的一个建议。通过在 API 中遵守 RESTful 原则,就总能以安全的方式使用 GET 请求。 - - 没有什么能保证 GET 请求实际上是 安全的 ,这实际上只是 HTTP 标准中定义的一个建议。通过在我们的 API 中坚持 RESTful 原则,GET 请求实际上总是以一种 安全 的方式被使用。 - - - - HTTP 标准还定义了请求类型 [HEAD](https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.4),这应该是安全的。在实践中,HEAD 的工作方式应该与 GET 完全一样,但它不返回任何东西,只返回状态代码和响应头。当你发出 HEAD 请求时,响应体将不会被返回。 - + +HTTP 标准定义的请求类型中,应该是安全的还有 [HEAD](https://www.rfc-editor.org/rfc/rfc9110.html#name-head)。实际上,HEAD 的效果应该与 GET 完全一样,但不返回除状态码和响应标头外的任何信息。当你发送 HEAD 请求时,不会有响应体返回。 - 除了 POST,所有的 HTTP 请求都应该是 idempotent。 +除了 POST,所有 HTTP 请求都应该是幂等的 - > 方法也可以具有 " 同位素 " 的属性,即(除了错误或过期问题)N>0 个相同的请求的副作用与单个请求相同。GET、HEAD、PUT 和 DELETE 等方法都有这个属性 。 - - - - 这意味着,如果一个请求有副作用,那么无论这个请求被发送多少次,结果都应该是一样的。 - +> 方法也可以具有“幂等性”,即(除了错误或过期的问题) N > 0 个相同的请求的副作用与单个请求的副作用相同。GET、HEAD、PUT 和 DELETE 方法都有这一属性 - - 如果我们向 url /api/notes/10 发出 HTTP PUT 请求,并随请求发送数据 { content:"no side effects!", important: true },无论发送多少次请求,结果都是一样的。 + +这意味着如果一个请求确实会产生副作用,那么无论发送多少次该请求,结果都应该是一样的。 + +如果我们向 URL /api/notes/10 发送 HTTP PUT 请求,并随请求发送数据 { content:"no side effects!", important: true },那么无论发送多少次请求,结果都应该是一样的。 - 就像 GET 请求的 安全 一样, 空闲 也只是 HTTP 标准中的一个建议,并不是简单地基于请求类型就能保证的。然而,当我们的 API 遵守 RESTful 原则时,那么 GET、HEAD、PUT 和 DELETE 请求的使用方式就是 idempotent。 - +和 GET 请求的安全性一样,幂等性也只是 HTTP 标准中的一个建议,并不是简单根据请求类型就能保证的。然而,当我们的 API 遵守 RESTful 原则时,就能以幂等的方式使用 GET、HEAD、PUT 和 DELETE 请求。 - POST 是唯一的 HTTP 请求类型,既不 安全 也不 空闲 。如果我们向 /api/notes 发送 5 个不同的 HTTP POST 请求,其正文为 {content:"many same", important: true},服务器上产生的 5 个笔记都会有相同的内容。 - +POST 是唯一的既不安全也不幂等的 HTTP 请求类型。如果我们向 /api/notes 发送 5 个 HTTP POST 请求,每个请求的请求体都是 {content:"many same", important: true},那么服务端会产生 5 个相同内容的笔记。 -### Middleware - - - 我们之前使用的快递 [json-parser](https://expressjs.com/en/api.html) 是一个所谓的 [中间件](http://expressjs.com/en/guide/using-middleware.html)。 + +### 中间件 + +之前使用的 Express [json-parser](https://expressjs.com/en/api.html) 是一个[中间件](http://expressjs.com/en/guide/using-middleware.html)。 - 中间件是可以用来处理 _request_ 和 _response_ 对象的函数。 - - - 我们之前使用的 json-parser 从请求中获取原始数据,这些数据存储在 _request_ 对象中,将其解析为一个 JavaScript 对象,并将其作为一个新的属性 body 分配给 _request_ 对象。 +中间件是可以用来处理 _request_ 和 _response_ 对象的函数。 + +我们之前使用的 json-parser 从请求中获取存储在 _request_ 对象中的原始数据,将其解析为一个 JavaScript 对象,并将其赋值给 _request_ 对象作为其新属性 body。 - - 在实践中,你可以同时使用几个中间件。当你有多个中间件时,它们会按照在 Express 中被使用的顺序一个一个地被执行。 - + +实际上,你可以同时使用多个中间件。当你有多个中间件时,它们会按照应用代码中的顺序一个一个地执行。 - 让我们来实现我们自己的中间件,它可以打印出发送到服务器的每个请求的信息。 - +让我们来实现我们自己的中间件,来打印出发送到服务端的每个请求的信息。 - 中间件是一个接收三个参数的函数。 +中间件是一个接收三个参数的函数: ```js const requestLogger = (request, response, next) => { @@ -1320,25 +1098,23 @@ const requestLogger = (request, response, next) => { ``` - 在函数体的最后,调用作为参数传递的 _next_ 函数。这个 _next_ 函数将控制权交给下一个中间件。 +函数体在最后调用了作为参数传递的 _next_ 函数。_next_ 函数将控制权交给下一个中间件。 - 中间件是这样被使用的。 +中间件是这样用的: ```js app.use(requestLogger) ``` - - 中间件函数的调用顺序是它们被 Express 服务器对象的 _use_ 方法所使用的顺序。请注意,json-parser 是在 _requestLogger_ 中间件之前被使用的,因为否则在执行记录器的时候,request.body 将不会被初始化。 - + +记住,中间件函数是按照 JavaScript 引擎遇到它们的顺序调用的。注意 _json-parser_ 要放在 _requestLogger_ 之前,否则在执行 _requestLogger_ 的时候,request.body 还没有初始化! - - 如果我们想让中间件函数在路由事件处理程序被调用前执行,那么就必须在路由之前使用这些中间件函数。也有一些情况,我们想在路由之后定义中间件函数。在实践中,这意味着我们要定义的中间件函数只有在没有路由处理 HTTP 请求时才会被调用。 + +如果我们想让路由事件处理函数执行中间件函数,那么就必须在路由之前使用这些中间件函数。有时,我们想在路由之后才使用中间件函数。我们只在没有路由处理函数处理 HTTP 请求时才会调用路由之后的中间件函数。 - - - 让我们在路由之后添加以下中间件,用于捕捉向不存在的路由发出的请求。对于这些请求,中间件将返回一个 JSON 格式的错误信息。 + +让我们在路由之后添加以下中间件,用于捕捉向不存在的路由发出的请求。对于这些请求,中间件将返回一个 JSON 格式的错误信息。 ```js const unknownEndpoint = (request, response) => { @@ -1348,49 +1124,47 @@ const unknownEndpoint = (request, response) => { app.use(unknownEndpoint) ``` - - 你可以在 [这个 github 仓库](https://github.com/fullstack-hy2020/part3-notes-backend/tree/part3-2) 的 part3-2 分支中找到我们当前应用的全部代码。 +你可以在[这个 GitHub 仓库](https://github.com/fullstack-hy2020/part3-notes-backend/tree/part3-2)的 part3-2 分支中找到我们当前应用的全部代码。
-### Exercises 3.7.-3.8. + +### 练习 3.7.~3.8. -#### 3.7: Phonebook backend step7 + +#### 3.7:电话簿后端 第 7 步 - 将 [morgan](https://github.com/expressjs/morgan) 中间件添加到你的应用中进行记录。根据 tiny 配置,将信息记录到你的控制台。 +将 [morgan](https://github.com/expressjs/morgan) 中间件添加到你的应用中来记录日志。将 morgan 配置成按 tiny 配置项将信息记录到你的控制台。 - Morgan 的文档不是最好的,你可能需要花一些时间来弄清楚如何正确地配置它。然而,世界上的大多数文档都属于同一类别,所以无论如何,学会破译和解释神秘的文档是件好事。 - +morgan 的文档并非最好,你可能需要花一些时间来弄清楚如何正确配置它。然而,世界上的大多数文档都是这个样子,所以无论如何,学会理解和解释难解的文档都是件好事。 - 摩根和其他所有的库一样,通过 _npm install_ 命令来安装。使用 Morgan 的方式和配置其他中间件一样,都是使用 _app.use_ 命令。 - - -#### 3.8*: Phonebook backend step8 +安装 morgan 的方式和其他所有库一样,都是用 _npm install_ 命令。使用 morgan 的方式和其他所有中间件一样,都是用 _app.use_ 命令。 + +#### 3.8*:电话簿后端 第 8 步 - 配置 morgan,使它也显示 HTTP POST 请求中发送的数据。 +配置 morgan,使它也显示 HTTP POST 请求中发送的数据: ![](../../images/3/24.png) - 注意,即使在控制台中记录数据也是危险的,因为它可能包含敏感数据,并可能违反当地的隐私法(如欧盟的 GDPR)或商业标准。在这个练习中,你不必担心隐私问题,但在实践中,尽量不要记录任何敏感数据。 +注意记录数据是危险的,即使是记录到控制台中也如此,因为数据中可能包含敏感数据,从而可能违反当地的隐私法规(如欧盟的 GDPR)或商业标准。在这道练习中,你不必担心隐私问题,但在工作中,尽量不要记录任何敏感数据。 - 这个练习可能相当有挑战性,尽管解决方案不需要大量的代码。 - +这个练习可能相当有挑战性,尽管答案不需要大量的代码。 - 这个练习可以用几种不同的方式完成。其中一个可能的解决方案是利用这两种技术。 +这道练习可以用几种不同的方式完成。一种解法是利用这两项技巧: - - [创建新令牌](https://github.com/expressjs/morgan#creating-new-tokens) +- [新建词法单元](https://github.com/expressjs/morgan#creating-new-tokens) - - [JSON.stringify](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify) +- [JSON.stringify](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify)
diff --git a/src/content/3/zh/part3b.md b/src/content/3/zh/part3b.md index e7532be4a4c..39d79233ee5 100644 --- a/src/content/3/zh/part3b.md +++ b/src/content/3/zh/part3b.md @@ -8,12 +8,12 @@ lang: zh
- 接下来让我们把我们在[第二章节](/zh/part2)中制作的前端连接到我们自己的后端。 +接下来让我们把后端和[第 2 章节](/zh/part2)中做的前端连接起来。 - - 在上一部分中,前端可以从我们作为后端的json-server中询问笔记列表,地址是http://localhost:3001/notes 。 - - 我们的后端现在有一个稍微不同的url结构,因为笔记可以在http://localhost:3001/api/notes 。让我们改变src/services/notes.js中的属性__baseUrl__,像这样。 + +在上一章节中,前端可以从作为后端的 json-server 中,也就是从地址 请求笔记列表。 + +后端现在的 URL 结构稍有不同,可以找到笔记的地址是 。让我们改变前端应用 src/services/notes.js 中的 __baseUrl__ 属性,如下: ```js import axios from 'axios' @@ -29,60 +29,60 @@ const getAll = () => { export default { getAll, create, update } ``` - - 我们也需要改变App.js中效果中指定的url。 - -```js - useEffect(() => { - axios - .get('http://localhost:3001/api/notes') - .then(res => { - setNotes(res.data) - }) - }, []) -``` - - - 现在到 前端的GET请求由于某些原因不能工作。 +现在前端向 的 GET 请求由于某些原因无法进行: ![](../../images/3/3ae.png) - - 这里发生了什么?我们可以从浏览器和postman访问后端,没有任何问题。 +这里发生了什么?我们明明可以从浏览器和 Postman 访问后端,没有任何问题。 + + +### 同源策略和 CORS + + +问题在于一个叫做“_同源策略_”的东西。一个 URL 的“源”是由协议(又称“模式”)、主机名和端口组成的。 + +```text +http://example.com:80/index.html + +protocol: http +host: example.com +port: 80 +``` -### Same origin policy and CORS + +当你访问一个网站(如 )时,浏览器会向托管该网站(example.com)的服务器发出请求。服务端发回的响应是一个 HTML 文件,HTML 文件可能包含一个或多个外部资源的引用,这些外部资源可能托管在与 example.com 相同的服务器上,也可能托管在其他网站上。当浏览器在 HTML 源码中看到 URL 引用时,就会发出请求。如果请求的 URL 与 HTML 源码的 URL 相同,浏览器便会正常处理响应。但如果所请求资源的 URL 与 HTML 源码的不同源(模式、主机、端口任一不同),浏览器就会检查 _Access-Control-Allow-Origin_ 响应标头。如果该标头包含 HTML 源码的 URL 和通配符 _*_,那么浏览器就会处理响应;否则浏览器会拒绝处理并抛出错误。 - - 问题在于一个叫做CORS的东西,或者说跨源资源共享。 + +同源策略是浏览器实现的一种旨在防止会话劫持等其他安全漏洞的安全机制。 - - 根据[维基百科](https://en.wikipedia.org/wiki/Cross-origin_resource_sharing)。 + +为了允许合法的跨源请求(请求不同源的 URL),W3C 提出了一种名为 CORS(跨源资源共享,Cross-Origin Resource Sharing)的机制。根据[维基百科](https://en.wikipedia.org/wiki/Cross-origin_resource_sharing): - > 跨源资源共享(CORS)是一种机制,它允许网页上的限制性资源(如字体)从第一个资源所来自的域之外的另一个域被请求。一个网页可以自由嵌入跨源图像、样式表、脚本、iframe和视频。某些 "跨域 "请求,特别是Ajax请求,在默认情况下是被同源安全策略所禁止的。 +> 跨源资源共享(CORS)是一种允许网页上的限制性资源(如字体)从所托管的域之外的域请求的机制。网页可以自由嵌入跨源图像、样式表、脚本、内联框架和视频。某些“跨域”请求,特别是 Ajax 请求,在默认情况下会被同源安全策略所禁止。 - -在我们的环境中,问题在于,默认情况下,在浏览器中运行的应用的JavaScript代码只能与同一[来源](https://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy)的服务器通信。 - -因为我们的服务器在localhost 3001端口,而我们的前端在localhost 3000端口,它们没有相同的起源。 + +问题在于,默认情况下,浏览器中运行的应用的 JavaScript 代码只能与同[源](https://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy)的服务端通信。 + +因为我们的服务端在 localhost 的 3001 端口,而我们的前端在 localhost 的 5173 端口,它们没有相同的源。 - - 请记住,[同源策略](https://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy)和 CORS 并不是专门针对 React 或 Node。它们实际上是网络应用操作的普遍原则。 + +记住,[同源策略](https://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy)和 CORS 并不是 React 或 Node 特有的。它们是关于 web 应用安全运行的通用原则。 - 我们可以通过使用 Node's [cors](https://github.com/expressjs/cors) 中间件来允许来自其他原点的请求。 +我们可以通过使用 Node 的 [cors](https://github.com/expressjs/cors) 中间件来允许来自其他的请求。 -在你的后端仓库中,用命令安装cors。 +在你后端的仓库中,用下列命令安装 cors ```bash npm install cors ``` -取中间件来使用,并允许来自所有源的请求。 +使用中间件,允许来自所有源的请求: ```js const cors = require('cors') @@ -90,36 +90,52 @@ const cors = require('cors') app.use(cors()) ``` - - 前端就可以工作了!然而,改变笔记重要性的功能还没有在后端实现。 + +**注:**当你启用 cors 的时候,你应该考虑下打算怎么设置 cors。对于我们应用的情况,因为不希望把后端暴露给整个生产环境,所以让后端只对特定的源(如前端)启用 cors 会更合理。 - - 你可以从[Mozillas页面](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS)上阅读更多关于CORS的信息。 + +现在前端的大多数功能都能用了!后端还没有实现更改笔记重要性的功能,所以自然前端更改笔记重要性的功能还用不了。我们将在后面修复。 + + +你可以在 [Mozilla 的网页](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS)上阅读更多关于 CORS 的信息。 - 我们的应用的设置现在看起来如下。 +我们现在应用的配置类似这样: -![](../../images/3/100.png) +![](../../images/3/100_25.png) - 在浏览器中运行的react应用现在从运行在localhost:3001的node/express-server获取数据。 -### Application to the Internet +浏览器中运行的 React 应用现在从运行在 localhost:3001 的 Node/Express 后端获取数据。 - - 现在整个堆栈已经准备好了,让我们把我们的应用移到互联网上。我们将使用古老的[Heroku](https://www.heroku.com)来完成。 + +### 把应用部署到互联网上 - - >如果你以前从未使用过Heroku,你可以从[Heroku文档](https://devcenter.heroku.com/articles/getting-started-with-nodejs)中找到说明,或者通过谷歌搜索。 + +既然全栈已经准备好了,让我们把应用部署到互联网上。 - - 在后端项目的根目录下添加一个名为Procfile的文件,告诉Heroku如何启动应用。 + +现今可用于在互联网上托管应用的服务日益增多。像 PaaS(平台即服务,Platform as a Service)这类对开发者友好的服务会负责安装运行环境(例如 Node.js),可能还会提供数据库等各种服务。 -```bash -web: npm start -``` + +在过去十几年里,[Heroku](http://heroku.com) 一直统治着 PaaS 领域。不幸的是,Heroku 的免费版服务在 2022 年 11 月 27 日结束了。这对许多开发者,尤其是学生,非常不利。如果你愿意花点钱,Heroku 仍然是非常切实可行的选择。他们也有[学生计划](https://www.heroku.com/students)来为学生提供一些免费额度。 + + +我们在此介绍两个服务:[Fly.io](https://fly.io/) 和 [Render](https://render.com/)。Fly.io 服务可以更灵活地配置,但最近也要付费了。Render 提供一定的免费计算时间,所以如果你想不花钱完成本课程,那就选 Render。在某些情况下 Render 也更易上手,因为 Render 不需要在你自己的机器上安装任何软件。 + + +还有一些其他免费的托管选项也可用于本课程,至少对于除了第 11 章节(CI/CD)以外的所有内容都没有问题,第 11 章节有一道练习可能会在其他平台上比较麻烦。 + + +部分课程参与者还使用过以下服务: + +- [Replit](https://replit.com) +- [CodeSandBox](https://codesandbox.io) + + +如果你知道易用且免费的 Node.js 托管服务,请告诉我们! - -在index.js文件的底部改变我们应用使用的端口的定义,像这样。 + +不管是 Fly.io 还是 Render,我们都需要在后端 index.js 最下面,将应用使用端口的定义改成: ```js const PORT = process.env.PORT || 3001 // highlight-line @@ -128,133 +144,230 @@ app.listen(PORT, () => { }) ``` - - 现在我们使用的是[环境变量](https://en.wikipedia.org/wiki/Environment_variable) _PORT_中定义的端口,如果环境变量_PORT_未定义,则使用3001端口。 - -Heroku根据环境变量来配置应用的端口。 + +现在我们使用的是[环境变量](https://en.wikipedia.org/wiki/Environment_variable) _PORT_ 中定义的端口,如果未定义环境变量 _PORT_,则使用端口 3001。Fly.io 和 Render 都可以基于环境变量设置应用的端口。 - - 在项目目录下创建一个Git仓库,并添加.gitignore,内容如下 +#### Fly.io + + +注意,你可能需要向 Fly.io 提供你的信用卡号! + + +如果你决定使用 [Fly.io](https://fly.io/),那么从按照[这份指南](https://fly.io/docs/hands-on/install-flyctl/)安装 flyctl 可执行文件开始。然后,你需要[创建一个 Fly.io 账号](https://fly.io/docs/hands-on/sign-up/)。 + + +从在命令行运行以下命令[登录](https://fly.io/docs/hands-on/sign-in/)开始 ```bash -node_modules +fly auth login ``` - -在 https://devcenter.heroku.com/ ,创建Heroku账户 - - 使用命令安装Heroku包:npm install -g heroku - - 用命令heroku create创建一个Heroku应用,将你的代码提交到版本库,并用命令git push heroku main将其移到Heroku。 - - 如果一切顺利,应用就能工作。 + +注意如果你的电脑上命令 _fly_ 不可用,你可以试试更长的版本 _flyctl_。例如在 MacOS 上,两种命令都可用。 -![](../../images/3/25ea.png) + +如果你无法在你的电脑上运行 flyctl,你可以试试 Render(见下节),它不需要你在电脑上安装任何软件。 - - 如果没有,可以通过命令heroku logs阅读heroku日志来发现问题。 + +在应用的根目录运行下列命令来初始化应用 - - > **NB** 至少在开始的时候,随时注意heroku的日志是很好的。最好的方法是使用命令heroku logs -t,它可以在服务器上发生任何事情时将日志打印到控制台。 +```bash +fly launch --no-deploy +``` - - > **NB** 如果你从一个git仓库部署,而你的代码不在主分支上(例如,如果你正在改变上一课的[notes repo](https://github.com/fullstack-hy2020/part3-notes-backend/tree/part3-2)),你将需要运行_git push heroku HEAD:master_。如果你已经做了推送到heroku,你可能需要运行_git push heroku HEAD:main --force_。 + +给应用取一个名字,或者让 Fly.io 自动生成一个。选择运行应用的服务器的地区。不要为应用创建 Postgres 数据库,也不要创建 Upstash Redis 数据库,这些都不需要。 - - 前端也可以和Heroku的后端一起工作。你可以通过把前端的后端地址改为Heroku中的后端地址,而不是http://localhost:3001来检查。 + +Fly.io 会在应用的根目录创建一个名为 fly.toml 的文件,我们可以配置这个文件。为了让应用能正常启动并运行,我们可能需要对配置做一点小的补充: - - 接下来的问题是,我们如何将前端部署到互联网上?我们有多种选择。接下来让我们来看看其中的一个。 +```bash +[build] + +[env] + PORT = "3001" # add this + +[http_service] + internal_port = 3001 # ensure that this is same as PORT + force_https = true + auto_stop_machines = true + auto_start_machines = true + min_machines_running = 0 + processes = ["app"] +``` -### Frontend production build + +我们已经在 [env] 部分定义了环境变量 PORT,从而让应用使用正确的端口(在 [http_service] 中定义)来启动服务端。 - - 到目前为止,我们一直在开发模式中运行React代码。在开发模式下,应用被配置为给出清晰的错误信息,立即向浏览器渲染代码变化,等等。 + +现在可以把应用部署到 Fly.io 服务器了。使用以下命令: - - 当应用被部署时,我们必须创建一个[生产构建](https://reactjs.org/docs/optimizing-performance.html#use-the-production-build)或一个为生产而优化的应用版本。 +```bash +fly deploy +``` - - 用create-react-app创建的应用的生产构建可以用[npm run build](https://github.com/facebookincubator/create-react-app#npm-run-build-or-yarn-build)命令创建。 + +如果一切顺利,应用应该会启动并运行。可以用下列命令在浏览器中打开应用 - - **注意:**在撰写本文时(2022年1月20日)create-react-app有一个错误,导致以下错误 _TypeError: MiniCssExtractPlugin不是一个构造函数_ 。 +```bash +fly apps open +``` - - 从[这里](https://github.com/facebook/create-react-app/issues/11930)可以找到一个可能的修正。在文件package.json中添加以下内容 + +一个特别重要的命令是 _fly logs_。该命令可以用来查看服务端日志。最好始终保持日志可见! -```json -{ - // ... - "resolutions": { - "mini-css-extract-plugin": "2.4.5" - } -} + +**注:**Fly 可能会为你的应用创建 2 台机器。如果出现这种情况,应用中的数据状态在不同请求间可能不一致,也就是说,你会有两台有各自独立的 notes 变量的机器,你可能 POST 到一台机器,然后下一次 GET 可能去到另一台机器。你可以用命令“$ fly scale show”检查机器数量,如果 COUNT 大于 1,则可以用“$ fly scale count 1”强制让 COUNT 为 1。也可以在仪表板上查看机器数量。 + + +**注意:**在某些情况下(原因尚不明确),运行 Fly.io 命令有出现过问题,特别是在 Windows WSL(Windows Subsystem for Linux)上。如果以下命令只是挂起,没有任何反应 + +```bash +flyctl ping -o personal ``` - - 然后运行命令 + +说明你的电脑出于某种原因无法连接到 Fly.io。如果遇到这种情况,[这里](https://github.com/fullstack-hy2020/misc/blob/master/fly_io_problem.md) 有一种可能的解决办法。 + +如果下面命令的输出类似这样: + +```bash +$ flyctl ping -o personal +35 bytes from fdaa:0:8a3d::3 (gateway), seq=0 time=65.1ms +35 bytes from fdaa:0:8a3d::3 (gateway), seq=1 time=28.5ms +35 bytes from fdaa:0:8a3d::3 (gateway), seq=2 time=29.3ms +... ``` -rm -rf package-lock.json -rm -rf node_modules -npm cache clean --force -npm install + + +那么说明没有连接问题! + + +每当你对应用做出更改时,可以用命令把新版本部署到生产环境: + +```bash +fly deploy ``` - - 在这些_npm run build_之后,应该可以工作。 +#### Render + + +注意,你可能需要向 Render 提供信用卡号! + + +下面假设你已经使用 GitHub 账号[登录](https://dashboard.render.com/)了。 + + +登录后,创建一个新“web service”: + +![](../../images/3/r1.png) + + +然后将应用的仓库连接到 Render: - - 让我们从前端项目的根部运行这个命令。 +![](../../images/3/r2.png) - - 这将创建一个名为build的目录(其中包含我们应用的唯一HTML文件,index.html),该目录包含static。我们应用的[Minified]()版本的JavaScript代码将被生成到static目录中。即使应用的代码在多个文件中,所有的JavaScript都将被最小化为一个文件。事实上,所有应用的依赖性代码也将被压缩到这个文件中。 + +看起来连接要求应用的仓库是公开的。 + + +接下来定义基本配置。如果应用在仓库的根目录下,则需在 Root directory 中填写正确的路径: + +![](../../images/3/r3.png) + + +之后,应用将在 Render 上启动。仪表板会显示应用的状态和运行的 URL: + +![](../../images/3/r4.png) + + +根据[文档](https://render.com/docs/deploys),每次提交到 GitHub 都会自动重新部署应用。不过因为什么原因,不会总是重新部署。 + + +幸运的是,也可以手动重新部署应用: + +![](../../images/3/r5.png) + + +同样,可以在仪表板中查看应用的日志: + +![](../../images/3/r7.png) + + +我们现在从日志中发现应用在端口 10000 启动。应用的代码通过环境变量 PORT 获取正确的端口,因此必须将后端的 index.js 文件更新成: + +```js +const PORT = process.env.PORT || 3001 // highlight-line +app.listen(PORT, () => { + console.log(`Server running on port ${PORT}`) +}) +``` + + +### 构建前端的生产版本 + + +目前为止,我们一直在开发模式中运行 React 代码。在开发模式下,应用会配置成给出清晰的错误信息,立即向浏览器渲染代码变化,等等。 + + +当部署应用时,我们必须构建一个[生产版本](https://vitejs.dev/guide/build.html)或者说一个为生产优化的应用版本。 + + +用 Vite 创建的应用的生产版本可以用命令 [npm run build](https://vitejs.dev/guide/build.html) 构建。 + + +让我们在[第 2 章节](/zh/part2)开发的笔记前端项目的根目录运行这个命令。 + + +这会创建一个名为 dist 的目录,其中只包含应用的 HTML 文件(index.html)和目录 assets。应用的 JavaScript 代码的[极简化]()版本会在 dist 目录中生成。即使应用的代码在多个文件中,所有 JavaScript 都会被极简化进一个文件。应用所有依赖项的所有代码也会被极简化进这单个文件中。 - 分解后的代码可读性不强。代码的开头如下所示: +极简化的代码不是很可读。代码的开头类似这样: ```js !function(e){function r(r){for(var n,f,i=r[0],l=r[1],a=r[2],c=0,s=[];c +### 由后端提供静态文件 - - 部署前端的一个选择是将生产构建(build目录)复制到后端仓库的根目录,并配置后端以显示前端的主页(文件build/index.html)作为其主页面。 + +部署前端的一个选择是将构建的生产版本(dist 目录)复制到后端的根目录中,并配置后端来让主页显示为前端的主页(文件 dist/index.html)。 - 我们首先把前端的生产构建复制到后端的根目录下。在Mac或Linux电脑上,复制可以在前端目录下用命令完成 +我们首先把前端的生产版本复制到后端的根目录下。在 Mac 或 Linux 电脑上,可以在前端目录下用命令复制 ```bash cp -r build ../notes-backend ``` - 如果你使用的是Windows电脑,你可以用[copy](https://www.windows-commandline.com/windows-copy-command-syntax-examples/)或[xcopy](https://www.windows-commandline.com/xcopy-command-syntax-examples/)命令代替。否则,只需进行复制和粘贴。 +如果你使用的是 Windows 电脑,你可以用 [copy](https://www.windows-commandline.com/windows-copy-command-syntax-examples/) 或 [xcopy](https://www.windows-commandline.com/xcopy-command-syntax-examples/) 命令。再或者,直接复制粘贴。 - 后端目录现在应该是这样的。 +后端目录现在应该是这样的: -![](../../images/3/27ea.png) +![](../../images/3/27v.png) - - 为了使express显示静态内容,页面index.html和它获取的JavaScript等,我们需要express的一个内置的中间件,叫做[static](http://expressjs.com/en/starter/static-files.html)。 + +为了使 Express 显示获取的静态内容、页面 index.html 和 JavaScript 等,我们需要一个 Express 内置的中间件,叫做 [static](http://expressjs.com/en/starter/static-files.html)。 当我们在中间件的声明中加入以下内容时 + ```js -app.use(express.static('build')) +app.use(express.static('dist')) ``` - -每当express收到一个HTTP GET请求时,它将首先检查build目录中是否包含一个与请求地址相对应的文件。如果找到了正确的文件,express将返回它。 + +每当 Express 收到一个 HTTP GET 请求时,它首先会检查 dist 目录是否包含与请求地址对应的文件。如果找到了正确的文件,Express 就会返回这个文件。 - - 现在,对地址www.serversaddress.com/index.htmlwww.serversaddress.com的HTTP GET请求将显示React前端。对地址www.serversaddress.com/api/notes的GET请求将由后端代码处理。 + +现在,对地址 www.serversaddress.com/index.htmlwww.serversaddress.com 的 HTTP GET 请求将显示 React 前端。对地址 www.serversaddress.com/api/notes 的 GET 请求将由后端代码处理。 - 由于我们的情况,前端和后端都在同一个地址,我们可以将_baseUrl_声明为一个[相对](https://www.w3.org/TR/WD-html40-970917/htmlweb.html#h-5.1.2)URL。这意味着我们可以省去声明服务器的部分。 +由于我们前后端都在同一个地址的情况,我们可以将 _baseUrl_ 声明为一个[相对](https://www.w3.org/TR/WD-html40-970917/htmlweb.html#h-5.1.2) URL。这意味着我们可以省略声明服务端的部分。 ```js import axios from 'axios' @@ -268,222 +381,283 @@ const getAll = () => { // ... ``` - - 更改后,我们必须创建一个新的生产构建,并将其复制到后端仓库的根目录。 + +更改后,我们必须重新构建前端的生产版本,并将其复制到后端的根目录。 - 应用现在可以从后端地址使用。 +现在可以从后端的地址 使用应用: -![](../../images/3/28e.png) +![](../../images/3/28new.png) - 我们的应用现在的工作方式与我们在第0章节学习的[单页应用](/en/part0/fundamentals_of_web_apps#single-page-app)示例应用完全相同。 +我们的应用现在的运行方式与我们在第 0 章节学习的[单页应用](/zh/part0/web_应用的基础设施#single-page-app)的示例应用完全相同。 - -当我们用浏览器进入地址时,服务器从build仓库返回index.html文件。该文件的内容摘要如下。 + +当我们用浏览器进入 地址时,服务端从 dist 目录返回 index.html 文件。文件内容如下: ```html - - - React App - - - -
- - - + + + + + + + Vite + React + + + + +
+ + + ``` - - 该文件包含获取定义应用样式的CSS样式表的指令,以及两个script标签,指示浏览器获取应用的JavaScript代码--实际的React应用。 + +该文件包含获取定义应用样式的 CSS 样式表的指令,以及一个令浏览器获取应用的 JavaScript 代码——实际的 React 应用的 script 标签。 - - React代码从服务器地址获取注释,并将其渲染到屏幕上。服务器和浏览器之间的通信可以在开发者控制台的Network标签中看到。 + +React 代码从服务端地址 获取笔记,并将其渲染到屏幕上。服务端和浏览器之间的通信可以在开发者控制台的网络标签页中看到: -![](../../images/3/29ea.png) +![](../../images/3/29new.png) - 准备用于产品部署的设置看起来如下。 +准备用于产品部署的配置类似这样: ![](../../images/3/101.png) - - 与在开发环境中运行应用时不同,现在所有东西都在同一个节点/express-backend中,该节点在localhost:3001中运行。当浏览器进入页面时,文件index.html被渲染。这导致浏览器获取React应用的产品版本。一旦开始运行,它就从localhost:3001/api/notes这个地址获取json-data。 + +与在开发环境中运行应用时不同,现在所有东西都运行在 localhost:3001 的同一个 Node/Express 后端中。当浏览器进入页面时,文件 index.html 被渲染。这会让浏览器获取 React 应用的生产版本。一旦 React 应用开始运行,它就从地址 localhost:3001/api/notes 获取 JSON 数据。 + + +### 把整个应用部署到互联网上 + + +在确保应用的生产版本在本地正确运行后,我们就准备好将整个应用部署到选择的托管服务上了。 + + +对于 Fly.io,用以下命令重新部署 + +```bash +fly deploy +``` -### The whole app to internet + +注:项目目录下的 _.dockerignore_ 文件会列出不会在部署时上传的文件。可能会默认忽略 _dist_ 目录。如果出现这种情况,就从 _.dockerignore_ 文件中删除该目录的引用,以确保应用能正确部署。 - - 在确保应用的生产版本在本地运行后,将前端的生产构建提交到后端仓库,并再次将代码推送到Heroku。 + +对于 Render,在 git 中提交更改,并再次将代码推送到 GitHub。确保后端的 git 没有忽略 dist 目录。推送到 GitHub 可能就可以了。如果没有自动部署,就在 Render 的仪表板中选择“manual deploy”。 - - [应用](https://obscure-harbor-49797.herokuapp.com/)工作得很好,只是我们还没有在后端添加改变笔记重要性的功能。 + +应用运行得很完美,只是我们还没有在后端添加改变笔记重要性的功能。 -![](../../images/3/30ea.png) +![](../../images/3/30new.png) + + +**注:**还**无法**改变笔记的重要性,后端还没有实现这一功能。 - 我们的应用将笔记保存在一个变量中。如果应用崩溃或重新启动,所有的数据都会消失。 +我们的应用将所有笔记保存在一个变量中。如果应用崩溃或重启,所有数据就都会消失。 - 该应用需要一个数据库。在我们引入一个数据库之前,让我们先看一下几件事。 +应用需要一个数据库。在我们引入数据库之前,让我们先仔细看一下几件事情。 -现在的设置看起来如下。 +现在的配置类似这样: ![](../../images/3/102.png) - - 节点/express-backend现在驻扎在Heroku服务器上。当访问形式为https://glacial-ravine-74819.herokuapp.com/ 的根地址时,浏览器会加载并执行React应用,从Heroku服务器上获取json数据。 + +Node/Express 后端现在在 Fly.io/Render 的服务器上。当访问根地址时,浏览器就会加载并执行 React 应用,React 应用又会从 Fly.io/Render 服务器获取 JSON 数据。 -### Streamlining deploying of the frontend + +### 优化前端的部署 - 为了创建一个新的前端生产构建,不需要额外的手工工作,让我们在后端仓库的package.json中添加一些npm脚本。 +为了让构建一个新的前端生产版本不需要额外的手动操作,让我们在后端仓库的 package.json 中添加一些 npm 脚本。 + + +#### Fly.io 脚本 + + +脚本类似这样: ```json { "scripts": { - //... - "build:ui": "rm -rf build && cd ../part2-notes/ && npm run build && cp -r build ../notes-backend", - "deploy": "git push heroku main", - "deploy:full": "npm run build:ui && git add . && git commit -m uibuild && npm run deploy", - "logs:prod": "heroku logs --tail" + // ... + "build:ui": "rm -rf dist && cd ../notes-frontend/ && npm run build && cp -r dist ../notes-backend", + "deploy": "fly deploy", + "deploy:full": "npm run build:ui && npm run deploy", + "logs:prod": "fly logs" } } ``` - - 脚本 _npm run build:ui_ 构建前端,并将生产版本复制到后端仓库下。 _npm run deploy_释放当前的后端到heroku。 + +脚本 _npm run build:ui_ 构建前端,并将生产版本复制到后端仓库下。脚本 _npm run deploy_ 将当前的后端发布到 Fly.io。 + + +_npm run deploy:full_ 结合了 _npm run build:ui_ 和 _npm run deploy_ 两个脚本。 + + +还有一个脚本 _npm run logs:prod_ 来显示 Fly.io 的日志。 + + +注意脚本 build:ui 中的目录路径取决于前后端目录在文件系统中的位置。 + + +##### Windows 用户注意事项 + + +注意 `build:ui` 中标准的 shell 命令无法在 Windows 中原生运行。Windows 的 Powershell 运行的方式不同,这时脚本应写成 + +```json +"build:ui": "@powershell Remove-Item -Recurse -Force dist && cd ../frontend && npm run build && @powershell Copy-Item dist -Recurse ../backend", +``` + + +如果脚本在 Windows 上还是无法运行,确保你使用的是 Powershell 而非命令提示符(cmd)。如果你安装过 Git Bash 或者其他类 Linux 的终端,你也可以在 Windows 上运行类 Linux 命令。 - - _npm run deploy:full_结合了这两者,并包含必要的git命令来更新后端仓库。 +#### Render - - 还有一个脚本_npm run logs:prod_来显示heroku的日志。 + +注意:当你尝试将后端部署到 Render 时,确保后端有一个专门的仓库并通过 Render 部署该 GitHub 仓库。尝试通过你的 Fullstackopen 仓库部署通常会抛出“ERR path ....package.json”错误。 - - 注意,脚本build:ui中的目录路径取决于文件系统中存储库的位置。 +对于 Render,脚本类似这样 + +```json +{ + "scripts": { + //... + "build:ui": "rm -rf dist && cd ../frontend && npm run build && cp -r dist ../backend", + "deploy:full": "npm run build:ui && git add . && git commit -m uibuild && git push" + } +} +``` + + +脚本 _npm run build:ui_ 会构建前端并将生产版本复制到后端仓库下。_npm run deploy:full_ 还包含更新后端仓库必要的 git 命令。 + + +注意脚本 build:ui 中的目录路径取决于前后端目录在文件系统中的位置。 - >**NB** 在Windows上,npm脚本在cmd.exe中执行,作为默认的shell,不支持bash命令。为了让上述bash命令发挥作用,你可以将默认的shell改为Bash(在默认的Git for Windows安装中),方法如下。 +>**注** 在 Windows 上,npm 脚本的默认 shell 是 cmd.exe,npm 脚本默认在 cmd.exe 中执行,不支持 bash 命令。为了让上述 bash 命令生效,你可以将默认 shell 改为(Git for Windows 默认安装的)Bash,方法如下: ```md npm config set script-shell "C:\\Program Files\\git\\bin\\bash.exe" ``` - 另一个选择是使用 [shx](https://www.npmjs.com/package/shx)。 +另一个选择是使用 [shx](https://www.npmjs.com/package/shx)。 -### Proxy + +### 代理 - - 前端的变化导致它在开发模式下不再工作(当用_npm start_命令启动时),因为与后端的连接不起作用。 + +对前端的更改导致它在开发模式下(用 _npm run dev_ 命令启动时)无法运行,因为前端无法与后端连接。 -![](../../images/3/32ea.png) +![](../../images/3/32new.png) - 这是由于将后端地址改为相对的URL。 +这是由于后端地址改成了相对 URL: ```js const baseUrl = '/api/notes' ``` - - 因为在开发模式下,前端的地址是localhost:3000,对后端的请求会进入错误的地址localhost:3000/api/notes。后端是在localhost:3001。 + +因为在开发模式下,前端的地址是 localhost:5173,对后端的请求会发送到错误的地址 localhost:5173/api/notes。而后端是在 localhost:3001。 - - 如果该项目是用create-react-app创建的,这个问题很容易解决。只需在前端仓库的package.json文件中添加以下声明。 + +如果项目是用 Vite 创建的,那么问题很容易解决。只需在前端目录的 vite.config.js文件中添加以下声明就可以了: -```bash -{ - "dependencies": { - // ... - }, - "scripts": { - // ... +```js +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [react()], + // highlight-start + server: { + proxy: { + '/api': { + target: 'http://localhost:3001', + changeOrigin: true, + }, + } }, - "proxy": "http://localhost:3001" // highlight-line -} -``` + // highlight-end +}) - - 重启后,React开发环境将作为一个[代理](https://create-react-app.dev/docs/proxying-api-requests-in-development/)工作。如果React代码向http://localhost:3000的服务器地址做HTTP请求,而不是由React应用本身管理(即当请求不是关于获取应用的CSS或JavaScript),该请求将被重定向到http://localhost:3001的服务器。 +``` - - 现在前端也很好,在开发和生产模式下都能与服务器一起工作。 + +重启后,React 开发环境将充当[代理](https://vitejs.dev/config/server-options.html#server-proxy)。如果 React 代码向以 http://localhost:5173/api 开头的路径发送 HTTP 请求,那么请求将被转发到 http://localhost:3001 的服务端上。向其他路径发送的请求依然由开发服务端正常处理。 - - 我们的方法的一个消极方面是部署前端是多么的复杂。部署一个新的版本需要生成新的前端生产版本并将其复制到后端仓库。这使得创建一个自动化的[部署管道](https://martinfowler.com/bliki/DeploymentPipeline.html)更加困难。部署管道是指通过不同的测试和质量检查,将代码从开发者的电脑中转移到生产环境中的一种自动化和可控的方式。构建一个部署管道是本课程[第11部分](https://fullstackopen.com/en/part11)的主题。 + +现在前端也能正确运行。在开发模式和生产模式下都能与服务端一起运行。从前端的角度来看,因为所有的请求都是发送到 http://localhost:5173 的,也就是同源的,那么就不再需要后端的 cors 中间件了。因此,我们可以从后端的 index.js 文件中删除对 cors 库的引用,并从项目的依赖项中删除 cors: - -有多种方法来实现这个目标(例如,将后端和前端的代码[放在同一个仓库](https://github.com/mars/heroku-cra-node)),但我们现在不会去讨论这些。 +```bash +npm remove cors +``` - - 在某些情况下,将前端代码部署为自己的应用可能是明智的。对于用create-react-app创建的应用,这是[直接的](https://github.com/mars/create-react-app-buildpack)。 + +我们现在已经成功将整个应用部署到互联网上了。此外还有许多方法来实现部署。比如,将前端代码部署为自己的应用在某些情况下是一个明智的方法,因为这样可以方便实现自动化的[部署管道](https://martinfowler.com/bliki/DeploymentPipeline.html)。部署管道是指通过不同测试和质量检查,将代码从开发者的电脑转移到生产环境的一种自动化和可控的方式。本课程[第 11 章节](https://fullstackopen.com/en/part11)涵盖了这一话题。 - 后端的当前代码可以在[Github](https://github.com/fullstack-hy2020/part3-notes-backend/tree/part3-3)的分支part3-3中找到。前端代码的变化在[frontend repository](https://github.com/fullstack-hy2020/part2-notes/tree/part3-1)的part3-1分支中。 +当前的后端代码可以在 [Github](https://github.com/fullstack-hy2020/part3-notes-backend/tree/part3-3) 的 part3-3 分支中找到。前端代码的更改在 [frontend repository](https://github.com/fullstack-hy2020/part2-notes/tree/part3-1) 的part3-1分支中。
-### Exercises 3.9.-3.11. + +### 练习 3.9.~3.11 - 下面的练习不需要很多行的代码。然而,它们可能是具有挑战性的,因为你必须确切地了解正在发生什么和在哪里发生,而且配置必须恰到好处。 +下面的练习不需要很多行代码。但这些练习可能具有挑战性,因为你必须准确理解发生了什么事情,以及事情在哪里发生,并且配置必须正好正确。 -#### 3.9 phonebook backend step9 + +#### 3.9 电话簿后端 第 9 步 - 使后端与上一部分练习中的电话簿前端一起工作。先不要实现对电话号码进行修改的功能,这将在练习3.17中实现。 +使后端与上一章节练习中的电话簿前端一起运行。先不要实现更改电话号码的功能,我们会在练习 3.17 中实现。 - 你可能需要对前端做一些小的改动,至少要对后端的URL做一些改动。记得在你的浏览器中保持开放的开发者控制台。如果一些HTTP请求失败了,你应该从Network-tab中检查发生了什么。也要注意后端的控制台。如果你没有做前面的练习,在负责POST请求的事件处理程序中把请求数据或request.body打印到控制台是值得的。 +你可能要对前端做一些小的改动,至少要更改后端的 URL。记得在浏览器中保持开发者控制台打开。如果一些 HTTP 请求失败了,你应该在网络标签页中检查发生了什么。也要留意后端的控制台。如果你还没有做前面的练习,那么值得在负责 POST 请求的事件处理函数中把请求数据或 request.body 打印到控制台。 -#### 3.10 phonebook backend step10 + +#### 3.10 电话簿后端 第 10 步 - - 将后端部署到互联网上,例如部署到Heroku。 + +将后端部署到互联网上,比如部署到 Fly.io 或 Render。如果你用的是 Fly.io,那么应该在后端的根目录(也就是在和后端 package.json 所在的同一个目录下)运行命令。 - - **NB**命令_heroku_在系里的电脑和新生的笔记本上都可以使用。如果由于某些原因你不能[安装](https://devcenter.heroku.com/articles/heroku-cli)Heroku到你的电脑上,你可以使用命令[npx heroku](https://www.npmjs.com/package/heroku)。 + +**专业提示:**当你将应用部署到互联网时,至少在一开始时应**始终**留意应用的日志。 - 用浏览器和Postman或VS Code REST客户端测试已部署的后端,以确保其工作。 - - - **专业提示:**当你将你的应用部署到Heroku时,至少在开始时值得用命令heroku logs -t来关注heroku应用的日志,**在任何时候。 - - - 下面是一个典型问题的日志。Heroku无法找到应用的依赖项express。 - -![](../../images/3/33.png) - - - 原因是express包没有被npm install express命令安装,所以关于这个依赖的信息没有被保存到package.json文件中。 - - - 另一个典型的问题是,应用没有被配置为使用设置在环境变量PORT中的端口。 - -![](../../images/3/34.png) +用浏览器和 Postman 或 VS Code REST Client 测试部署好的后端,确保其正常运行。 - 在你的版本库根部创建一个README.md,并在其中添加一个在线应用的链接。 +在你的仓库的根目录创建一个 README.md,并在其中添加在线应用的链接。 -#### 3.11 phonebook full stack + +#### 3.11 全栈电话簿 - 为你的前端生成一个生产版本,并使用本章节介绍的方法将其添加到互联网应用中。 +为你的前端构建一个生产版本,并使用本章节介绍的方法将其添加到部署到互联网的应用中。 - - **NB** 确保目录build没有被gitignored + +同时,确保前端在本地(用命令 _npm run dev_ 启动的开发模式中)仍然可以运行。 - - 还要确保前端在本地仍然可以工作(在开发模式下,用_npm start_命令启动)。 + +如果你使用的是 Render,确保后端的 dist 目录没有在 .gitignore 中。 - - 如果你有问题让应用工作,请确保你的目录结构与[示例应用](https://github.com/fullstack-hy2020/part3-notes-backend/tree/part3-3)的结构一致。 + +**注:**在本章节的任何阶段,你都**不**应直接部署前端。整个章节都只部署后端。将前端构建的生产版本添加到后端仓库中,然后让后端来提供,如同[由后端提供静态文件](/zh/part3/把应用部署到互联网上#由后端提供静态文件)一节中所描述的那样。
diff --git a/src/content/3/zh/part3c.md b/src/content/3/zh/part3c.md index 180cf742385..01aa2bd8f94 100644 --- a/src/content/3/zh/part3c.md +++ b/src/content/3/zh/part3c.md @@ -8,33 +8,34 @@ lang: zh
-在我们进入关于在数据库中持久化数据的主题之前,我们先来看一下调试 Node 应用程序的几种不同方法。 +在我们开始主题——在数据库中持久化保存数据之前,我们先来看一下调试 Node 应用的几种不同方法。 -### Debugging Node applications + +### 调试 Node 程序 -调试 Node 应用程序比调试在浏览器中运行的 JavaScript 稍微困难一些。打印到控制台是一种经过验证的方法,值得一试。有些人认为应该使用更复杂的方法,但我不同意。即使是世界上顶级的开源开发人员也会使用这种方法。 +调试 Node 应用比调试浏览器中运行的 JavaScript 稍微困难一些。打印到控制台是一种经实践检验的方法,永远值得这么做。有些人认为应该使用更优雅的方法,但我不同意。即使是世界上的精英开源开发者也[使用](https://tenderlovemaking.com/2016/02/05/i-am-a-puts-debuggerer.html)这种[方法](https://swizec.com/blog/javascript-debugging-slightly-beyond-consolelog/)。 #### Visual Studio Code -在某些情况下,Visual Studio Code 的调试器可能很有用。您可以像这样以调试模式启动应用程序(在这个和接下来的几个图像中,注释中有一个名为“日期”的字段,在当前版本的应用程序中已被删除): +在某些情况下,Visual Studio Code 的调试器会很有用。你可以像这样以调试模式启动应用(在这张图片和接下来几张图片中,笔记有一个在当前版本的应用中已删除的 _date_ 字段): ![截图显示如何在 vscode 中启动调试器](../../images/3/35x.png) -请注意,应用程序不应该在另一个控制台中运行,否则端口将已经被占用。 +注意,不应该在另一个控制台中运行应用,否则会占用端口。 -__注意__:Visual Studio Code 的较新版本可能会将“Debug”更改为“Run”。此外,您可能需要配置您的 _launch.json_ 文件来开始调试。您可以通过选择下拉菜单上方的绿色播放按钮旁边的 _Add Configuration..._,然后选择 _Run "npm start" in a debug terminal_ 来进行配置。有关更详细的设置说明,请访问 Visual Studio Code 的[调试文档](https://code.visualstudio.com/docs/editor/debugging)。 +__注__ 新版的 Visual Studio Code 可能用的是_运行_而非_调试_。此外,你可能需要配置 _launch.json_ 文件来开始调试。你可以通过在绿色播放按钮旁边,_变量_菜单上方的下拉菜单中选择_添加配置…_,然后选择_在调试终端运行“npm start”_来进行配置。更详细的设置说明参见 Visual Studio Code 的[调试文档](https://code.visualstudio.com/docs/editor/debugging)。 -下面是一张截图,显示代码执行在保存新笔记的过程中被暂停: +下面的截图显示代码在保存新笔记的中途已暂停执行: ![断点处执行的vscode屏幕截图](../../images/3/36x.png) -代码执行在第 69 行的断点处停止。在控制台中,您可以看到 note 变量的值。在左上角的窗口中,您可以看到与应用程序状态相关的其他信息。 +代码执行在第 69 行的断点处停止。在控制台中,你可以看到 note 变量的值。在左上角的窗口中,你可以看到应用状态的其他相关信息。 顶部的箭头可以用于控制调试器的流程。 @@ -45,86 +46,75 @@ __注意__:Visual Studio Code 的较新版本可能会将“Debug”更改为 #### Chrome dev tools -您也可以通过在命令中启动应用程序来使用 Chrome 开发者控制台进行调试: +通过以下命令启动应用,也可以使用 Chrome 开发者控制台进行调试: ```bash node --inspect index.js ``` - -您还可以将 `--inspect` 标志传递给 `nodemon`: - -```bash -nodemon --inspect index.js -``` - -您可以通过点击 Chrome 开发者控制台中出现的绿色图标(node logo)来访问调试器: +你可以通过点击 Chrome 开发者控制台中的绿色图标——Node 的 logo——来访问调试器: ![带有绿色node标志图标的开发者工具](../../images/3/37.png) -调试器的界面与在 React 应用程序中的使用方式相同。可以使用Sources选项卡设置断点,代码执行将在断点处暂停。 +调试界面的用法与调试 React 应用时的用法相同。可以在源代码选项卡中设置断点,代码将在断点处暂停执行。 ![开发者工具的 Sources 选项卡,包含断点和监视变量](../../images/3/38eb.png) -应用程序的所有console.log消息都将出现在调试器的Console选项卡中。您还可以检查变量的值并执行自己的 JavaScript 代码。 +应用所有的 console.log 消息都将出现在调试器的控制台选项卡中。你还可以检查变量的值并执行自己的 JavaScript 代码。 ![开发者工具的控制台选项卡显示输入的笔记对象](../../images/3/39ea.png) -#### Question everything + +#### 怀疑一切 -调试全栈应用程序可能一开始看起来很棘手。很快,我们的应用程序除了前端和后端之外还将有一个数据库,而应用程序中可能存在许多潜在的错误。 +调试全栈应用一开始可能看起来很棘手。我们的应用除了前端和后端之外,很快还将又有一个数据库,应用中将有许多可能出现问题的地方。 -当应用程序"无法工作"时,我们首先必须找出问题实际发生在哪里。问题往往存在于您意想不到的地方,可能需要几分钟、几小时甚至几天才能找到问题的根源。 +当应用“无法运行”时,我们首先必须找出问题实际发生在哪里。问题往往存在于你意想不到的地方,并且可能要几分钟、几小时甚至几天才能找到问题的根源。 -关键是要有系统性。由于问题可能存在于任何地方,您必须对所有事物提出质疑,逐个排除所有可能性。记录到控制台、使用 Postman、调试器和经验都会有所帮助。 +关键是要有条不紊。由于问题可能存在于任何地方,你必须怀疑一切,逐个排除所有可能性。记录到控制台,借助 Postman、调试器和经验都会有所帮助。 -当出现错误时,最糟糕的策略就是继续编写代码。这将确保您的代码很快会有更多的错误,并且调试它们将变得更加困难。丰田生产系统的 [Jidoka](https://leanscape.io/principles-of-lean-13-jidoka/)(停止和修复)原则 在这种情况下也非常有效。 +当出现错误时,所有策略中最差的就是继续编写代码。这么做保证会让你的代码很快有更多的错误,并且更难以调试。丰田生产体系的[自动化](https://leanscape.io/principles-of-lean-13-jidoka/)(停止和修复)原则在这种情况下也非常有效。 ### MongoDB - -为了永久存储我们保存的笔记,我们需要一个数据库。赫尔辛基大学的大多数课程使用关系数据库。在本课程的大部分内容中,我们将使用 [MongoDB](https://www.mongodb.com/),这是一种所谓的 [文档数据库](https://en.wikipedia.org/wiki/Document-oriented_database)。 + +为了永久存储我们保存的笔记,我们需要一个数据库。赫尔辛基大学教授的大多数课程使用的都是关系数据库。在本课程的大部分章节中,我们将使用 [MongoDB](https://www.mongodb.com/),这是一种[文档数据库](https://en.wikipedia.org/wiki/Document-oriented_database)。 -选择使用 Mongo 作为数据库的原因是它相对于关系数据库来说更简单。本课程的 [第13部分](/zh/part13) 展示了如何构建使用关系数据库的 Node.js 后端。 +使用 Mongo 作为数据库的原因是它相对于关系数据库来说更简单。本课程的[第 13 章节](/zh/part13)展示了如何构建使用关系数据库的 Node.js 后端。 文档数据库与关系数据库在数据组织方式和支持的查询语言方面有所不同。文档数据库通常被归类为 [NoSQL](https://en.wikipedia.org/wiki/NoSQL) 的范畴。 -您可以从 [数据库导论课程](https://tikape-s18.mooc.fi/part7/) 的 [part7](https://tikape-s18.mooc.fi/part7/) 材料中了解有关文档数据库和 NoSQL 的更多信息。不幸的是,该材料目前仅提供芬兰语版本。 +你可以从 Introduction to Databases 课程[第 7 周](https://tikape-s18.mooc.fi/part7/)的教材中了解更多文档数据库和 NoSQL 的信息。不幸的是,该教材目前只有芬兰语。 -现在,请阅读 MongoDB 手册中关于 [集合(collections)](https://docs.mongodb.com/manual/core/databases-and-collections/) 和 [文档(documents)](https://docs.mongodb.com/manual/core/document/) 的章节,以了解文档数据库如何存储数据的基本概念。 +现在阅读 MongoDB 手册中关于[集合](https://docs.mongodb.com/manual/core/databases-and-collections/)和[文档](https://docs.mongodb.com/manual/core/document/)的章节,对文档数据库是如何存储数据的有一个基本概念。 -当然,您可以在计算机上安装和运行 MongoDB。然而,互联网上也有许多可用的 Mongo 数据库服务。在本课程中,我们首选的 MongoDB 提供商将是 [MongoDB Atlas](https://www.mongodb.com/atlas/database)。 - - -创建并登录到您的帐户后,让我们首先选择免费选项: - -![mongodb部署云数据库免费共享](../../images/3/mongo1.png) +当然,你可以在你自己的电脑上安装并运行 MongoDB。然而,互联网上也有许多可以利用的 Mongo 数据库服务。在本课程中,我们首选的 MongoDB 提供商是 [MongoDB Atlas](https://www.mongodb.com/atlas/database)。 - -选择云提供商和位置,并创建集群: + +创建你的帐户并登录后,让我们用首页上的按钮新建一个集群。在打开的页面中,选择免费计划,决定云服务提供商和数据中心,然后创建集群: ![选择共享、AWS 和区域的 MongoDB](../../images/3/mongo2.png) - -让我们等待集群准备就绪。这可能需要几分钟时间。 + +这里选择的云服务提供商是 AWS,地区是斯德哥尔摩(eu-north-1)。注意如果你选择了其他选项,你的数据库连接字符串会与本例中的略有不同。等待集群准备就绪。这可能需要几分钟时间。 -**注意**:在集群准备就绪之前,请不要继续进行。 +**注** 在集群准备就绪之前,先不要继续阅读。 -让我们使用security(安全)选项卡为数据库创建用户凭据。请注意,这些凭据与您用于登录 MongoDB Atlas 的凭据不同。这些凭据将用于您的应用程序连接到数据库。 +让我们使用 security 选项卡创建数据库的用户凭据。请注意,这些凭据不同于登录 MongoDB Atlas 的凭据。这些凭据是用来将你的应用连接到数据库的。 ![mongodb security quickstart](../../images/3/mongo3.png) @@ -134,59 +124,55 @@ nodemon --inspect index.js ![MongoDB 网络访问/添加 IP 访问列表](../../images/3/mongo4.png) -注意:如果对话框菜单对您而言不同,根据 MongoDB 文档,将 0.0.0.0 添加为 IP 地址也允许从任何地方访问。 +注:如果你的对话框菜单不同,根据 MongoDB 文档,将 0.0.0.0 添加为 IP 地址也会允许从任何地方访问。 - -最后,我们准备好连接到我们的数据库了。首先点击connect: + +终于,我们准备好连接到我们的数据库了。要连接到数据库,我们需要数据库连接字符串,在界面中选择 Connect,然后选择 Drivers,数据库连接字符串就在 Connect to your application 一节中: ![MongoDB 数据库部署连接](../../images/3/mongo5.png) - -然后选择:Connect to your application - -![MongoDB 连接应用程序](../../images/3/mongo6.png) - -视图显示了MongoDB URI,这是我们将提供给我们的应用程序的 MongoDB 客户端库的数据库地址。 +界面显示了 MongoDB URI,这是我们要在应用中提供给 MongoDB 客户端库的数据库地址: + +![MongoDB 连接应用](../../images/3/mongo6new.png) -地址看起来是这样子的: +地址类似这样: ```js -mongodb+srv://fullstack:thepasswordishere@cluster0.o1opl.mongodb.net/?retryWrites=true&w=majority +mongodb+srv://fullstack:thepasswordishere@cluster0.a5qfl.mongodb.net/?retryWrites=true&w=majority&appName=Cluster0 ``` 我们现在已经准备好使用数据库了。 -我们可以直接从我们的 JavaScript 代码中使用数据库,使用[官方的 MongoDB Node.js 驱动程序](https://mongodb.github.io/node-mongodb-native/),但是使用起来相当麻烦。我们将使用[Mongoose](http://mongoosejs.com/index.html)库,它提供了一个更高级的 API。 +我们可以在我们的 JavaScript 代码中用 [MongoDB 官方 Node.js 驱动](https://mongodb.github.io/node-mongodb-native/)来直接使用数据库,但是这个驱动用起来相当麻烦。因此我们将使用 [Mongoose](http://mongoosejs.com/index.html) 库,它提供了一个更高级的 API。 -Mongoose可以被描述为一个对象文档映射器(ODM),使用这个库将JavaScript对象保存为Mongo文档非常简单。 +Mongoose 可以当作一个对象文档映射器(ODM,Object Document Mapper),使用这个库后,将 JavaScript 对象保存为 Mongo 文档就简单了。 -让我们在笔记项目的后端中安装Mongoose: +让我们在笔记项目的后端中安装 Mongoose: ```bash npm install mongoose ``` -暂时先不要在后端添加任何与Mongo相关的代码。相反,我们可以通过在笔记后端应用程序的根目录下创建一个新文件mongo.js来创建一个练习应用程序: +暂时先不要在后端添加任何与 Mongo 相关的代码。让我们先在笔记后端应用的根目录下新建一个文件mongo.js来创建一个练习应用: ```js const mongoose = require('mongoose') -if (process.argv.length<3) { +if (process.argv.length < 3) { console.log('give password as argument') process.exit(1) } const password = process.argv[2] -const url = - `mongodb+srv://fullstack:${password}@cluster0.o1opl.mongodb.net/?retryWrites=true&w=majority` +const url = `mongodb+srv://fullstack:${password}@cluster0.a5qfl.mongodb.net/?retryWrites=true&w=majority&appName=Cluster0` mongoose.set('strictQuery',false) @@ -211,37 +197,36 @@ note.save().then(result => { ``` -**注意**:根据您在构建集群时选择的区域,MongoDB URI可能与上面提供的示例不同。您应该验证并使用从MongoDB Atlas生成的正确URI。 +**注:**根据你在构建集群时选择的地区,MongoDB URI 可能会与上面提供的示例不同。你应该验证并使用从 MongoDB Atlas 生成的正确 URI。 -代码还假设它将通过命令行参数传递从我们在MongoDB Atlas中创建的凭据中生成的密码。我们可以像这样访问命令行参数: +代码还假定我们在 MongoDB Atlas 中创建的凭据的密码将通过命令行参数传入。我们可以像这样访问命令行参数: ```js const password = process.argv[2] ``` -当使用命令node mongo.js yourPassword运行代码时,Mongo将向数据库添加一个新文档。 +当使用命令 node mongo.js yourPassword 运行代码时,Mongo 将向数据库添加一个新文档。 -**注意**:请注意,密码是为数据库用户创建的密码,而不是您的MongoDB Atlas密码。此外,如果您创建了一个带有特殊字符的密码,那么您需要对该密码进行[URL编码](https://docs.atlas.mongodb.com/troubleshoot-connection/#special-characters-in-connection-string-password)。 +**注:**请注意这里的密码是为数据库用户创建的密码,而不是 MongoDB Atlas 的密码。此外,如果你创建的密码带有特殊字符,那么你需要[用 URL 编码该密码](https://docs.atlas.mongodb.com/troubleshoot-connection/#special-characters-in-connection-string-password)。 -我们可以从MongoDB Atlas的浏览集合选项卡中查看数据库的当前状态。 +我们可以从 MongoDB Atlas 的 Database 选项卡的 Browse Collections 中查看数据库的当前状态。 ![MongoDB 数据库浏览集合按钮](../../images/3/mongo7.png) -正如视图所示,与笔记匹配的文档已添加到myFirstDatabase数据库中的notes集合中。 +正如视图所示,与笔记匹配的文档已添加到 myFirstDatabase 数据库中的 notes 集合中。 ![MongoDB 集合选项卡 db myfirst app notes](../../images/3/mongo8new.png) -让我们销毁默认数据库test,并通过修改URI中引用的数据库名称将其更改为noteApp: +让我们删除默认的数据库 test,并将连接字符串引用的数据库的名称更改为 noteApp,将 URI 改成: ```js -const url = - `mongodb+srv://fullstack:${password}@cluster0.o1opl.mongodb.net/noteApp?retryWrites=true&w=majority` +const url = `mongodb+srv://fullstack:${password}@cluster0.a5qfl.mongodb.net/noteApp?retryWrites=true&w=majority&appName=Cluster0` ``` @@ -250,12 +235,13 @@ const url = ![mongodb collections tab noteApp notes](../../images/3/mongo9.png) -数据现在存储在正确的数据库中。该视图还提供了create database(创建数据库)功能,可以用于从网站创建新数据库。这样创建数据库是不必要的,因为当应用程序尝试连接到尚不存在的数据库时,MongoDB Atlas会自动创建一个新数据库。 +数据现在存储在正确的数据库中。该界面还提供了 create database 功能,用于从网站新建数据库。没有必要这样新建数据库,因为当应用尝试连接到一个尚不存在的数据库时,MongoDB Atlas 会自动新建一个数据库。 -### Schema + +### 模式 -在与数据库建立连接后,我们为笔记定义了[schema](http://mongoosejs.com/docs/guide.html),并创建了相应的[model](http://mongoosejs.com/docs/models.html): +在建立数据库的连接后,我们定义了笔记的[模式](http://mongoosejs.com/docs/guide.html)和对应的[模型](http://mongoosejs.com/docs/models.html): ```js const noteSchema = new mongoose.Schema({ @@ -267,21 +253,22 @@ const Note = mongoose.model('Note', noteSchema) ``` -首先,我们定义了存储在 _noteSchema_ 变量中的笔记的[schema](http://mongoosejs.com/docs/guide.html)。该schema告诉 Mongoose 如何将笔记对象存储在数据库中。 +首先,我们定义了笔记的[模式](http://mongoosejs.com/docs/guide.html)并存储在 _noteSchema_ 变量中。该模式告诉 Mongoose 笔记对象是怎么存储在数据库中的。 -在 _Note_ 模型定义中,第一个"Note"参数是模型的单数名称。集合的名称将是小写复数形式的notes,因为[Mongoose的惯例](http://mongoosejs.com/docs/models.html)是自动将集合命名为复数形式(例如notes),当schema以单数形式(例如Note)引用它们时。 +在 _Note_ 模型定义中,第一个“Note”参数是模型单数形式的名称。集合的名称会是小写复数形式的 notes。这是因为 [Mongoose 的习惯](http://mongoosejs.com/docs/models.html)是当模式以单数形式(如 Note)引用集合时,会自动将集合命名为其复数形式(如 notes)。 -像Mongo这样的文档数据库是schemaless,这意味着数据库本身并不关心存储在数据库中的数据的结构。可以在同一集合中存储具有完全不同字段的文档。 +像 Mongo 这样的文档数据库是无模式的,这意味着数据库本身并不关心数据库中存储的数据的结构。可以在同一集合中存储字段完全不同的文档。 -Mongoose的思想是,存储在数据库中的数据在应用程序级别被赋予一个schema,该schema定义了存储在任何给定集合中的文档的形状。 +Mongoose 的思想是,在应用层面给定数据库中存储的数据的模式,来定义存储在任何给定集合中的文档的形状。 -### Creating and saving objects + +### 创建和保存对象 - -接下来,应用程序使用Note[model](http://mongoosejs.com/docs/models.html)创建一个新的笔记对象: + +接下来,应用借助 Note [模型](https://mongoosejs.com/docs/models.html)创建一个新的笔记对象: ```js const note = new Note({ @@ -290,11 +277,11 @@ const note = new Note({ }) ``` - -模型(Models)是所谓的构造函数,它根据提供的参数创建新的JavaScript对象。由于对象是用模型的构造函数创建的,因此它们具有模型的所有属性,这包括用于将对象保存到数据库的方法。 + +模型是根据提供的参数创建新的 JavaScript 对象的构造函数。由于对象是用模型的构造函数创建的,因此它们具有模型的所有属性,包括将对象保存到数据库的方法。 -将对象保存到数据库使用的是适当命名的 _save_ 方法,可以通过 _then_ 方法提供一个事件处理程序: +将对象保存到数据库使用顾名思义的 _save_ 方法,可以通过 _then_ 方法为 _save_ 方法提供一个事件处理函数: ```js note.save().then(result => { @@ -303,19 +290,20 @@ note.save().then(result => { }) ``` - -当对象保存到数据库时,提供给 _then_ 的事件处理程序会被调用。事件处理程序使用命令 mongoose.connection.close() 关闭数据库连接。如果不关闭连接,程序将永远不会结束执行。 + +当对象保存到数据库时,会调用提供给 _then_ 的事件处理函数。事件处理函数使用命令 mongoose.connection.close() 关闭数据库连接。如果不关闭连接,那么在程序结束之前,连接将一直打开。 -保存操作的结果在事件处理程序的 _result_ 参数中。当我们在数据库中存储一个对象时,结果并不那么有趣。如果你想在实现应用程序或在调试期间仔细查看它,你可以将对象打印到控制台。 +保存操作的结果在事件处理函数的 _result_ 参数中。当我们在数据库中存储一个对象时,保存操作的结果没什么有趣的。如果你想在实现应用或在调试时仔细查看它,可以将对象打印到控制台。 -我们也可以通过修改代码中的数据并再次执行程序来保存更多的笔记。 +让我们修改代码中的数据并再次执行程序来保存更多的笔记。 -**注意:**不幸的是,Mongoose的文档并不非常一致,部分文档在其示例中使用回调,其他部分使用其他样式,因此不建议直接从那里复制和粘贴代码。不建议在同一代码中混合使用promise和旧式的回调。 +**注:**不幸的是,Mongoose 的文档并不非常一致,部分文档在示例中使用回调函数,其他部分使用其他风格,因此不建议直接从那里复制和粘贴代码。不建议在同一份代码中混合使用 Promise 和传统的回调。 -### Fetching objects from the database + +### 从数据库中获取对象 让我们注释掉生成新笔记的代码,并用以下内容替换它: @@ -335,13 +323,13 @@ Note.find({}).then(result => { ![node mongo.js outputs notes as JSON](../../images/3/70new.png) -通过_Note_模型的[find](https://mongoosejs.com/docs/api/model.html#model_Model-find)方法从数据库中检索对象。该方法的参数是一个表示搜索条件的对象。由于参数是一个空对象{},我们得到了_notes_集合中存储的所有笔记。 +对象是通过 _Note_ 模型的 [find](https://mongoosejs.com/docs/api/model.html#model_Model-find) 方法从数据库中获取的。find 方法的参数是一个表示搜索条件的对象。由于参数是一个空对象{},我们得到了 _notes_ 集合中存储的所有笔记。 -搜索条件遵循Mongo搜索查询[syntax](https://docs.mongodb.com/manual/reference/operator/)。 +搜索条件遵循 Mongo 的搜索查询[语法](https://docs.mongodb.com/manual/reference/operator/)。 -我们可以限制我们的搜索只包括重要的笔记,像这样: +我们可以这么限制我们的搜索,使其只包含重要的笔记: ```js Note.find({ important: true }).then(result => { @@ -353,49 +341,51 @@ Note.find({ important: true }).then(result => {
-### Exercise 3.12. + +### 练习 3.12. -#### 3.12: Command-line database + +#### 3.12:命令行数据库 -使用MongoDB Atlas为电话簿应用程序创建一个基于云的MongoDB数据库。 +使用 MongoDB Atlas 为电话簿应用创建一个基于云的 MongoDB 数据库。 -在项目目录中创建一个mongo.js文件,该文件可以用于向电话簿添加条目,以及列出电话簿中所有已有的条目。 +在项目目录中创建一个mongo.js文件,用于向电话簿添加记录,以及列出电话簿中所有已有的记录。 -**注意:** 不要在你提交和推送到GitHub的文件中包含密码! +**注意:**不要在你提交和推送到 GitHub 的文件中包含密码! -应用程序应该如下工作。你通过传递三个命令行参数(第一个是密码)来使用程序,例如: +应用应该实现下列功能。你通过传递三个命令行参数(第一个是密码)来使用程序,例如: ```bash node mongo.js yourpassword Anna 040-1234556 ``` -因此,应用程序将打印: +运行后,应用将打印: ```bash added Anna number 040-1234556 to phonebook ``` -新的电话簿条目将被保存到数据库中。注意,如果名字包含空格字符,它必须被包含在引号中: +新的电话簿记录将被保存到数据库中。注意如果名字包含空格字符,就必须用引号引起来: ```bash node mongo.js yourpassword "Arto Vihavainen" 045-1232456 ``` -如果密码是给程序的唯一参数,意味着它像这样被调用: +如果密码是给程序的唯一参数,也就是这样调用: ```bash node mongo.js yourpassword ``` -那么程序应该显示电话簿中的所有条目: +那么程序应该显示电话簿中的所有记录: ``` phonebook: @@ -405,10 +395,10 @@ Ada Lovelace 040-1231236 ``` -你可以从[process.argv](https://nodejs.org/docs/latest-v18.x/api/process.html#process_process_argv)变量获取命令行参数。 +你可以从 [process.argv](https://nodejs.org/docs/latest-v18.x/api/process.html#process_process_argv) 变量获取命令行参数。 -**注意:不要在错误的地方关闭连接**。例如,以下代码将无法工作: +**注意:不要在错误的地方关闭连接**。例如,以下代码将无法正确运行: ```js Person @@ -421,7 +411,7 @@ mongoose.connection.close() ``` -在上面的代码中,mongoose.connection.close()命令将在Person.find操作开始后立即执行。这意味着数据库连接将立即关闭,执行永远不会到达Person.find操作完成和callback函数被调用的地方。 +在上面的代码中,mongoose.connection.close() 命令将在 Person.find 操作开始后立即执行。这会立即关闭数据库连接,从而永远不会执行到 Person.find 操作完成并调用回调函数的地方。 关闭数据库连接的正确位置是在回调函数的末尾: @@ -436,28 +426,27 @@ Person ``` -**注意:** 如果你用Person这个名字定义一个模型,mongoose会自动将关联的集合命名为people。 +**注意:**如果你将模型的名字定义为 Person,mongoose 会自动将关联的集合命名为 people
-### Connecting the backend to a database + +### 将后端连接到数据库 -现在我们已经有足够的知识开始在我们的笔记应用程序后端中使用Mongo。 +现在我们已经有足够的知识来在我们笔记应用的后端开始使用 Mongo。 -让我们通过复制粘贴Mongoose定义到index.js文件来快速开始: +让我们通过将 Mongoose 的定义复制粘贴到 index.js 文件来快速开始: ```js const mongoose = require('mongoose') -const password = process.argv[2] - // DO NOT SAVE YOUR PASSWORD TO GITHUB!! -const url = - `mongodb+srv://fullstack:${password}@cluster0.o1opl.mongodb.net/?retryWrites=true&w=majority` +const password = process.argv[2] +const url = `mongodb+srv://fullstack:${password}@cluster0.a5qfl.mongodb.net/noteApp?retryWrites=true&w=majority&appName=Cluster0` mongoose.set('strictQuery',false) mongoose.connect(url) @@ -471,7 +460,7 @@ const Note = mongoose.model('Note', noteSchema) ``` -让我们将获取所有笔记的处理器更改为以下形式: +让我们将处理获取所有笔记的函数改成: ```js app.get('/api/notes', (request, response) => { @@ -481,24 +470,17 @@ app.get('/api/notes', (request, response) => { }) ``` - -我们可以在浏览器中验证后端是否可以显示所有的文档: + +让我们用命令 node --watch index.js yourpassword 启动后端,于是我们可以在浏览器中验证后端是否正确显示所有保存到数据库中的笔记: ![api/notes in browser shows notes in JSON](../../images/3/44ea.png) -应用程序几乎完美地工作。前端假设每个对象在id字段中都有一个唯一的id。我们也不想将mongo版本控制字段\_\_v返回给前端。 - - -格式化Mongoose返回的对象的一种方法是[modify(修改)](https://stackoverflow.com/questions/7034848/mongodb-output-id-instead-of-id)模式的 _toJSON_ 方法,该方法在用该模式产生的模型的所有实例上使用。 - - -要modify(修改)该方法,我们需要更改模式的可配置选项,可以使用模式的set方法更改选项,更多关于此方法的信息请参见: 。有关 _toJSON_ 选项的更多信息,请参阅 。 +应用几乎完美地运行。只是前端假定每个对象都有的一个唯一 id 是在 id 字段中。我们也不想将 mongo 的版本控制字段 \_\_v 返回给前端。 + +更改 Mongoose 返回对象的格式的一种方法是[修改](https://stackoverflow.com/questions/7034848/mongodb-output-id-instead-of-id)模式的 _toJSON_ 方法,该方法应用于该模式产生的所有模型的所有实例。可以这么修改: - -有关 _transform_ 函数的更多信息,请参阅 。 - ```js noteSchema.set('toJSON', { transform: (document, returnedObject) => { @@ -510,10 +492,10 @@ noteSchema.set('toJSON', { ``` -尽管Mongoose对象的\_id属性看起来像一个字符串,但实际上它是一个对象。我们定义的 _toJSON_ 方法将其转换为字符串以确保安全。如果我们不做这个改变,一旦我们开始编写测试,它将在未来对我们造成更大的麻烦。 +尽管 Mongoose 对象的 \_id 属性看起来像一个字符串,但它实际上是一个对象。我们定义的 _toJSON_ 方法将其转换成字符串以确保安全。如果我们不这么改的话,一旦将来我们开始编写测试,对象形式的 *\_id* 属性就会给我们带来更大的危害。 -在处理器中不需要做任何改变: +处理函数不需要更改: ```js app.get('/api/notes', (request, response) => { @@ -524,14 +506,16 @@ app.get('/api/notes', (request, response) => { ``` -代码在格式化响应的笔记时将自动使用定义的 _toJSON_ 。 -### Database configuration into its own module +代码在将笔记格式化为响应的格式时会自动使用定义的 _toJSON_ 方法。 + + +### 将数据库配置移到自己的模块 -在我们将后端的其余部分重构为使用数据库之前,让我们将Mongoose特定的代码提取到它自己的模块中。 +在我们将后端的其余部分重构为使用数据库之前,让我们先将 Mongoose 特定的代码提取到它自己的模块中。 -让我们为模块创建一个名为models的新目录,并添加一个名为note.js的文件: +让我们为模块新建一个名为 models 的目录,并添加一个名为 note.js 的文件: ```js const mongoose = require('mongoose') @@ -540,8 +524,7 @@ mongoose.set('strictQuery', false) const url = process.env.MONGODB_URI // highlight-line -console.log('connecting to', url) // highlight-line - +console.log('connecting to', url) mongoose.connect(url) // highlight-start .then(result => { @@ -568,30 +551,27 @@ noteSchema.set('toJSON', { module.exports = mongoose.model('Note', noteSchema) // highlight-line ``` - -定义Node [modules(模块)](https://nodejs.org/docs/latest-v18.x/api/modules.html)的方式与在第2部分中定义[ES6 modules](/zh/part2/从渲染集合到模块学习#refactoring-modules)的方式略有不同。 + +代码与之前相比有一些变化。数据库连接 URL 现在通过 MONGODB_URI 环境变量传递给应用,因为将其硬编码到应用中并不明智: - -modules(模块)的公共接口是通过为 _module.exports_ 变量设置一个值来定义的。我们将值设置为Note模型。在模块内部定义的其他东西,如变量 _mongoose_ 和 _url_ ,对模块的用户来说将不可访问或不可见。 +```js +const url = process.env.MONGODB_URI +``` - -导入模块是通过在index.js中添加以下行来实现的: + +有许多方法定义环境变量的值。例如,我们可以在启动应用时定义: -```js -const Note = require('./models/note') +```bash +MONGODB_URI="your_connection_string_here" npm run dev ``` - -这样,_Note_ 变量将被赋值为模块定义的同一个对象。 + +我们稍后会学习一种更优雅的定义环境变量的方法。 -建立连接的方式有所改变: +建立连接的方式略有变化: ```js -const url = process.env.MONGODB_URI - -console.log('connecting to', url) - mongoose.connect(url) .then(result => { console.log('connected to MongoDB') @@ -601,56 +581,65 @@ mongoose.connect(url) }) ``` - -将数据库的地址硬编码到代码中并不是一个好主意,所以我们通过MONGODB_URI环境变量将数据库的地址传递给应用程序。 - -建立连接的方法现在被赋予了处理成功和失败的连接尝试的函数。两个函数只是将成功状态的消息记录到控制台: +建立连接的方法现在有了处理连接成功和失败的函数。两个函数都只是将连接成功与否的消息记录到控制台: ![node output when wrong username/password](../../images/3/45e.png) - -有许多方法可以定义环境变量的值。一种方法是在启动应用程序时定义它: + +定义 Node [模块](https://nodejs.org/docs/latest-v18.x/api/modules.html)的方式与在第 2 章节中定义 [ES6 模块](/zh/part2/渲染集合与模块#重构模块)的方式略有不同。 -```bash -MONGODB_URI=address_here npm run dev + +模块的公共接口是通过设定 _module.exports_ 变量的值来定义的。我们将值设置为 Note 模型。在模块内部定义的其他东西,如变量 _mongoose_ 和 _url_,对模块的用户而言将不可访问,也不可见。 + + +导入模块是通过在 index.js 中添加下面这一行来实现的: + +```js +const Note = require('./models/note') ``` - -更聪明的方法是使用[dotenv](https://github.com/motdotla/dotenv#readme)库。你可以用以下命令安装这个库: + +这样,_Note_ 变量将被赋值为模块定义的同一个对象。 + + +### 使用 dotenv 库定义环境变量 + + +一种更优雅地定义环境变量的方法是使用 [dotenv](https://github.com/motdotla/dotenv#readme) 库。你可以用以下命令安装这个库: ```bash npm install dotenv ``` -要使用这个库,我们在项目的根目录下创建一个.env文件。环境变量在文件内部定义,它可以像这样: +要使用这个库,我们要在项目的根目录下创建一个 .env 文件。环境变量在这个文件内定义,可以类似这样: ```bash -MONGODB_URI=mongodb+srv://fullstack:thepasswordishere@cluster0.o1opl.mongodb.net/noteApp?retryWrites=true&w=majority +MONGODB_URI=mongodb+srv://fullstack:thepasswordishere@cluster0.a5qfl.mongodb.net/noteApp?retryWrites=true&w=majority&appName=Cluster0 PORT=3001 ``` -我们也将服务器的硬编码端口添加到PORT环境变量中。 +我们也将硬编码的服务端端口添加到 PORT 环境变量中。 -**我们应该立即将.env文件添加到gitignore中,因为我们不希望公开发布任何机密信息!** +**应当立即将 .env 文件添加到 .gitignore 中,不要把任何秘密信息发布到网上!** ![.gitignore in vscode with .env line added](../../images/3/45ae.png) -在.env文件中定义的环境变量可以通过表达式require('dotenv').config()引入,你可以像引用普通环境变量一样在代码中引用它们,使用process.env.MONGODB_URI语法。 +.env 文件中定义的环境变量可以通过表达式 require('dotenv').config() 导入,然后你在代码中就可以像引用普通环境变量一样,用 process.env.MONGODB_URI 语法引用它们。 - -让我们以以下方式更改index.js文件: + +让我们在 index.js 文件的开头导入环境变量,这样就可以在整个应用中使用环境变量了。让我们将 index.js 文件更改为: ```js require('dotenv').config() // highlight-line const express = require('express') -const app = express() const Note = require('./models/note') // highlight-line +const app = express() // .. const PORT = process.env.PORT // highlight-line @@ -660,39 +649,38 @@ app.listen(PORT, () => { ``` -在导入note模型之前导入dotenv非常重要。这确保了在导入其他模块的代码之前,.env文件中的环境变量在全局范围内可用。 +重要的是 dotenv 要在 note 模型之前导入。这能确保在导入其他模块的代码之前,.env 文件中的环境变量在全局范围内(译注:包括其他导入的模块内)都可用。 -### Important note to Fly.io users + +#### 关于在 Fly.io 和 Render 中定义环境变量的重要注意事项 - -因为GitHub不是与Fly.io一起使用的,所以当应用程序被部署时,.env文件也会被传到Fly.io服务器。因此,文件中定义的环境变量将在那里可用。 + +**Fly.io 用户:**因为 Fly.io 不与 GitHub 一起使用,所以当部署应用时,.env 文件也会传到 Fly.io 服务器上。因此,文件中定义的环境变量在 Fly.io 也可用。 -然而,[更好的选择](https://community.fly.io/t/clarification-on-environment-variables/6309)是通过在项目根目录创建 _.dockerignore_ 文件,内容如下 +然而,[更好的选择](https://community.fly.io/t/clarification-on-environment-variables/6309)是通过在项目根目录创建 _.dockerignore_ 文件来防止 .env 被复制到 Fly.io,*.dockerignore* 的内容如下 ```bash .env ``` -并使用以下命令从命令行设置环境值: +并在命令行用以下命令设置环境变量的值: ```bash -fly secrets set MONGODB_URI="mongodb+srv://fullstack:thepasswordishere@cluster0.o1opl.mongodb.net/noteApp?retryWrites=true&w=majority" +fly secrets set MONGODB_URI="mongodb+srv://fullstack:thepasswordishere@cluster0.a5qfl.mongodb.net/noteApp?retryWrites=true&w=majority&appName=Cluster0" ``` - -由于PORT也在我们的.env中定义,所以实际上在Fly.io中忽略该文件是至关重要的,否则应用程序将在错误的端口启动。 - - -在使用Render时,通过在仪表板中定义适当的环境变量给出数据库url: + +**Render 用户:**在使用Render时,数据库 URL 通过在仪表板中定义适当的环境变量提供: ![browser showing render environment variables](../../images/3/render-env.png) - -只需将以mongodb+srv://开头的URL设置到_value_字段。 + +只需将 _value_ 字段设为以 mongodb+srv:// 开头的 URL。 -### Using database in route handlers + +### 在路由处理函数中使用数据库 接下来,让我们将后端的其余功能更改为使用数据库。 @@ -704,7 +692,7 @@ fly secrets set MONGODB_URI="mongodb+srv://fullstack:thepasswordishere@cluster0. app.post('/api/notes', (request, response) => { const body = request.body - if (body.content === undefined) { + if (!body.content) { return response.status(400).json({ error: 'content missing' }) } @@ -720,7 +708,7 @@ app.post('/api/notes', (request, response) => { ``` -笔记对象是用 _Note_ 构造函数创建的。响应在 _save_ 操作的回调函数内部发送。这确保只有在操作成功时才发送响应。我们稍后会讨论错误处理。 +笔记对象是用 _Note_ 构造函数创建的。响应是在 _save_ 操作的回调函数内发送的。这确保只有在操作成功时才发送响应。我们稍后会讨论如何处理错误。 回调函数中的 _savedNote_ 参数是保存的新创建的笔记。响应中发送回来的数据是用 _toJSON_ 方法自动创建的格式化版本: @@ -730,7 +718,7 @@ response.json(savedNote) ``` -使用Mongoose的[findById](https://mongoosejs.com/docs/api/model.html#model_Model-findById)方法,获取单个笔记的操作变为以下形式: +通过使用 Mongoose 的 [findById](https://mongoosejs.com/docs/api/model.html#model_Model-findById) 方法,获取单个笔记的操作变为以下形式: ```js app.get('/api/notes/:id', (request, response) => { @@ -740,64 +728,100 @@ app.get('/api/notes/:id', (request, response) => { }) ``` -### Verifying frontend and backend integration + +### 验证前后端的整合 -当后端的功能被扩展时,首先使用**浏览器、Postman或VS Code REST客户端**测试后端是个好主意。接下来,让我们在启用数据库后尝试创建一个新的笔记: +当后端扩展后,首先使用**浏览器、Postman 或 VS Code REST Client** 测试后端是明智的。接下来,让我们在启用数据库后尝试创建一个新的笔记: ![VS code rest client doing a post](../../images/3/46new.png) -只有在后端的所有内容都经过验证并正常工作后,才是测试前端与后端是否协同工作的好时机。仅通过前端进行测试效率极低。 +只有在后端的所有内容都经过验证并正确运行后,才是测试前端与后端是否协同工作的时候。仅通过前端进行测试效率极低。 -逐个集成前端和后端的功能可能是个好主意。首先,我们可以实现从数据库获取所有笔记的功能,并通过浏览器中的后端端点进行测试。然后,我们可以验证前端是否能与新的后端一起工作。一旦所有东西看起来都在工作,我们就会转向下一个功能。 +逐个集成前后端的功能可能是个好主意。首先,我们可以实现从数据库获取所有笔记的功能,然后在浏览器中通过后端端点进行测试。然后,我们可以验证前端是否能与新的后端一起正确运行。一旦所有东西看起来都正确,我们就可以继续下一个功能。 -一旦我们引入数据库,查看数据库中持久化的状态是非常有用的,例如,从MongoDB Atlas的控制面板中查看。在开发过程中,像我们之前写的mongo.js这样的小型Node助手程序往往非常有帮助。 +一旦我们引入数据库,查看数据库中持久化的状态是非常有用的,比如通过 MongoDB Atlas 的控制面板查看。在开发过程中,小型 Node 辅助程序,比如我们之前写的 mongo.js,往往非常有帮助。 -你可以在part3-4分支的[这个GitHub仓库](https://github.com/fullstack-hy2020/part3-notes-backend/tree/part3-4)中找到我们当前应用程序的完整代码。 +你可以在[这个 GitHub 仓库](https://github.com/fullstack-hy2020/part3-notes-backend/tree/part3-4)的 part3-4 分支中找到我们当前应用的完整代码。 + + +### 真正的全栈开发者的誓言 + + +现在又是练习的时候了。我们的应用的复杂性现在又上升了一个阶段,因为除了前端和后端,我们还有一个数据库。 + +的确有很多可能的错误来源。 + + +所以我们应该再次扩展我们的誓言: + + +全栈开发极其困难,因此我会尽一切可能使其变得更容易 + + +- 我会始终打开浏览器的开发者控制台 + +- 我会使用浏览器开发者工具的网络标签页,确保前后端按预期通信 + +- 我会持续留意服务端的状态,确保前端发送的数据按预期保存 + +- 我会留意数据库:后端是否以正确的格式保存数据 + +- 我小步前进 + +- 我会写大量的 _console.log_ 语句,以确保我理解代码的行为,并借此定位问题 + +- 如果我的代码不能正确运行,我不会写更多的代码。相反,我会开始删除代码直到它能正确运行,或者直接回到一切都还正常的状态 + +- 当我在课程的 Discord 频道或其他地方寻求帮助时,我会恰当地陈述我的问题,参见[这里](https://fullstackopen.com/en/part0/general_info#how-to-get-help-in-discord)了解如何寻求帮助
-### Exercises 3.13.-3.14. + +### 练习 3.13.~3.14. -以下练习相当简单,但如果你的前端停止与后端一起工作,那么找出并修复错误可能会相当有趣。 +以下练习相当简单,但如果你的前端无法与后端一起运行,那么寻找并修复错误的过程可能会相当有趣。 -#### 3.13: Phonebook database, step 1 + +#### 3.13:电话簿数据库,第 1 步 -更改所有电话簿条目的获取方式,使数据从数据库中获取。 +将获取所有电话簿记录的方式更改为从数据库中获取数据。 -在更改后,验证前端是否正常工作。 +在更改后,验证前端是否依然正确运行。 - -在接下来的练习中,将所有Mongoose特定的代码写入其自己的模块,就像我们在[数据库配置到自己的模块](/zh/part3/将数据存入_mongo_db#database-configuration-into-its-own-module)一章中做的那样。 + +在接下来的练习中,将所有 Mongoose 特定的代码写入其自己的模块,就像我们在[将数据库配置到自己的模块](/zh/part3/将数据存入_mongo_db#将数据库配置移到自己的模块)一章中做的那样。 -#### 3.14: Phonebook database, step 2 + +#### 3.14:电话簿数据库,第 2 步 -更改后端,使新的号码保存到数据库中。验证更改后你的前端是否仍然工作。 +更改后端,使新的号码保存到数据库中。在更改后,验证前端是否依然正确运行。 -在这个阶段,你可以忽略是否已经有一个人在数据库中与你要添加的人同名。 +在这个阶段,你可以先不考虑数据库中是否已经有一个人与你要添加的人同名的情况。
-### Error handling + +### 错误处理 -如果我们尝试访问一个不存在的笔记的URL,例如,其中5c41c90e84d891c15dfa3431不是存储在数据库中的id,那么响应将为 _null_ 。 +如果我们尝试访问一个不存在的笔记的 URL,例如 ,其中 5c41c90e84d891c15dfa3431 不是存储在数据库中的 id,那么响应将为 _null_ 。 -让我们改变这种行为,如果给定id的笔记不存在,服务器将以HTTP状态码404未找到来响应请求。此外,让我们实现一个简单的catch块来处理findById方法返回的promise被拒绝的情况: +让我们改变这种行为,如果给定 id 的笔记不存在,服务器将以 HTTP 状态码 404 not found 来响应请求。此外,让我们实现一个简单的 catch 块来处理 findById 方法返回的 Promise 被拒绝的情况: ```js app.get('/api/notes/:id', (request, response) => { @@ -821,10 +845,10 @@ app.get('/api/notes/:id', (request, response) => { ``` -如果在数据库中没有找到匹配的对象, _note_ 的值将为 _null_ ,并执行 _else_ 块。这将导致一个带有状态码404 not found的响应。如果 findById 方法返回的 promise 被拒绝,响应将有状态码500内部服务器错误。控制台会显示关于错误的更详细的信息。 +如果数据库中没有找到匹配的对象,_note_ 的值将为 _null_ 并执行 _else_ 块。这将导致响应状态码 404 not found。如果 findById 方法返回的 Promise 被拒绝,响应的状态码将是 500 internal server error。控制台会显示更详细的错误信息。 -除了不存在的笔记,还有一个需要处理的错误情况。在这种情况下,我们试图获取一个错误类型的_id_,也就是说,_id_与Mongo标识符格式不匹配。 +除了不存在的笔记,还有一个需要处理的错误情况。在这种情况下,我们试图获取一个错误类型的 _id_,也就是说,_id_ 与 Mongo 的标识符 *_id* 的格式不匹配。 如果我们发出以下请求,我们将得到下面的错误消息: @@ -841,10 +865,10 @@ Body: {} ``` -给出一个格式错误的id作为参数,findById方法将抛出错误,导致返回的promise被拒绝。这将导致在catch块中定义的回调函数被调用。 +如果给出一个格式错误的 id 作为参数,findById 方法将抛出错误,导致返回的 Promise 被拒绝。这会调用 catch 块中定义的回调函数。 -让我们对catch块中的响应做一些小的调整: +让我们对 catch 块中的响应做一些小调整: ```js app.get('/api/notes/:id', (request, response) => { @@ -864,19 +888,19 @@ app.get('/api/notes/:id', (request, response) => { ``` -如果id的格式不正确,那么我们将进入在_catch_块中定义的错误处理程序。适合这种情况的状态码是[400 Bad Request](https://www.rfc-editor.org/rfc/rfc9110.html#name-400-bad-request),因为这种情况完全符合描述: +如果 id 的格式不正确,那么我们会以在 _catch_ 块中定义的错误处理程序结束。适合这种情况的状态码是 [400 Bad Request](https://www.rfc-editor.org/rfc/rfc9110.html#name-400-bad-request),因为这种情况完全符合其描述: -> 400 (Bad Request) 状态码表示服务器不能或不会处理请求,因为有些东西被认为是客户端错误(例如,请求语法格式错误,请求消息帧格式无效,或请求路由欺骗)。 +> 400(Bad Request)状态码表示服务器不能或不会处理请求,因为服务端认为某些东西是客户端错误(例如,请求语法格式错误、请求消息帧格式无效,或请求路由欺骗)。 我们还在响应中添加了一些数据,以便解释错误的原因。 -在处理Promises时,几乎总是添加错误和异常处理的好主意。否则,你会发现自己在处理奇怪的错误。 +在处理 Promise 时,添加错误和异常处理几乎总是明智的。否则,你会发现自己在处理奇怪的错误。 -在错误处理程序中打印引发异常的对象永远不是个坏主意: +在错误处理程序中打印引发异常的对象永远不会错: ```js .catch(error => { @@ -886,20 +910,21 @@ app.get('/api/notes/:id', (request, response) => { ``` -错误处理程序被调用的原因可能完全不同于你预期的。如果你将错误记录到控制台,你可能会从长时间和令人沮丧的调试会话中解救出来。此外,大多数现代服务在你部署应用程序时都支持某种形式的日志系统,你可以用来检查这些日志。如前所述,Fly.io就是其中之一。 +导致错误处理程序被调用的原因可能完全不同于你所预期的。如果你将错误记录到控制台,你可能会从长时间令人沮丧的调试会话中解救出来。此外,大多数你部署应用的现代服务都支持某种形式的日志系统,你可以借此来检查这些日志。如前所述,Fly.io 就是其中之一。 -每次你在一个有后端的项目上工作时,关注后端的控制台输出是至关重要的。如果你在一个小屏幕上工作,只需要在背景中看到一小部分输出就足够了。任何错误消息都会引起你的注意,即使控制台在后端很远: +每次你在处理一个有后端的项目时,关注后端的控制台输出至关重要。如果你的屏幕比较小,只需要在背景中看到一小部分输出就足够了。任何错误消息都会引起你的注意,即使控制台在很后面也如此: ![sample screenshot showing tiny slice of output](../../images/3/15b.png) -### Moving error handling into middleware + +### 将错误处理移至中间件 -我们在其他代码中编写了错误处理程序的代码。有时这可能是一个合理的解决方案,但有些情况下,最好在一个地方实现所有的错误处理。如果我们稍后想向像[Sentry](https://sentry.io/welcome/)这样的外部错误跟踪系统报告与错误相关的数据,这可能特别有用。 +我们在其他代码中编写了错误处理函数的代码。有些情况下这可能是一个合理的解决方案,但有些情况下,最好在一个地方实现所有的错误处理。如果我们后面想向诸如 [Sentry](https://sentry.io/welcome/) 这样的外部错误跟踪系统报告与错误相关的数据,这可能特别有用。 -让我们更改/api/notes/:id路由的处理程序,使其使用next函数将错误传递下去。下一个函数作为第三个参数传递给处理程序: +让我们更改 /api/notes/:id 路由的处理函数,使用 next 函数来传递错误。next 函数作为第三个参数传递给处理函数: ```js app.get('/api/notes/:id', (request, response, next) => { // highlight-line @@ -915,11 +940,11 @@ app.get('/api/notes/:id', (request, response, next) => { // highlight-line }) ``` - -向前传递的错误作为一个参数给到next函数。如果next没有参数被调用,那么执行将简单地移动到下一个路由或中间件。如果next函数带有参数被调用,那么执行将继续到错误处理中间件。 + +传递的错误作为参数传给 next 函数。如果调用 next 时没有传递参数,那么就将简单继续执行下一个路由或中间件。如果调用 next 函数时有一个参数,那么将继续执行错误处理中间件 -Express的[(error handlers)错误处理器](https://expressjs.com/en/guide/error-handling.html)是定义了一个接受四个参数的函数的中间件。我们的错误处理器看起来像这样: +Express 的[错误处理函数](https://expressjs.com/en/guide/error-handling.html)是一个定义为接受四个参数的函数的中间件。我们的错误处理函数类似这样: ```js const errorHandler = (error, request, response, next) => { @@ -937,15 +962,16 @@ app.use(errorHandler) ``` -错误处理器检查错误是否为CastError异常,如果是,我们知道错误是由Mongo的无效对象id引起的。在这种情况下,错误处理器将使用作为参数传递的响应对象向浏览器发送响应。在所有其他错误情况下,中间件将错误传递给默认的Express错误处理器。 +错误处理函数检查错误是否为 CastError 异常,我们知道这个错误是由 Mongo 的无效对象 id 引起的。在这种情况下,错误处理函数将使用作为参数传递的 response 对象向浏览器发送响应。对于其他所有错误情况,中间件将错误传递给默认的 Express 错误处理函数。 -注意,错误处理中间件必须是最后加载的中间件,所有的路由都应该在错误处理器之前注册! +注意,错误处理中间件必须是最后加载的中间件,并且所有的路由都应该在错误处理函数之前注册! -### The order of middleware loading + +### 加载中间件的顺序 - -中间件的执行顺序与它们被加载到express的app.use函数的顺序相同。因此,定义中间件时需要小心。 + +中间件的执行顺序与它们通过 _app.use_ 函数被加载到 Express 的顺序相同。因此,定义中间件时需要小心。 正确的顺序是: @@ -976,7 +1002,8 @@ app.use(errorHandler) ``` -json-parser中间件应该是加载到Express中的第一个中间件。如果顺序是以下的: +json-parser 中间件应该是最先加载到 Express 中的中间件。如果顺序是这样的话: + ```js app.use(requestLogger) // request.body is undefined! @@ -990,13 +1017,10 @@ app.use(express.json()) ``` -那么,HTTP请求发送的JSON数据在logger中间件或POST路由处理器中将不可用,因为在这个点上 _request.body_ 将是 _undefined_。 - - -同样重要的是,处理不支持的路由的中间件是加载到Express中的最后一个中间件,就在错误处理器之前。 +那么,HTTP 请求发送的 JSON 数据在 logger 中间件和 POST 路由处理函数中都将不可用,因为这时 _request.body_ 还是 _undefined_。 - -例如,以下加载顺序会导致问题: + +同样重要的是,处理不支持的路由的中间件应当在定义完所有端点之后才加载,只在错误处理函数之前。例如,以下加载顺序会导致问题: ```js const unknownEndpoint = (request, response) => { @@ -1012,15 +1036,16 @@ app.get('/api/notes', (request, response) => { ``` -现在,未知端点的处理是在HTTP请求处理器之前进行的。由于未知端点处理器对所有请求都以404 unknown endpoint响应,所以在未知端点中间件发送响应后,不会调用任何路由或中间件。唯一的例外是错误处理器,它需要在未知端点处理器之后,放在最后。 +现在,未知端点的处理是在 HTTP 请求处理函数之前进行的。由于未知端点处理函数对所有请求都以 404 unknown endpoint 响应,所以在未知端点中间件发送响应后,不会调用任何路由或中间件。唯一的例外是错误处理函数需要放在最后,在未知端点处理函数之后。 -### Other operations + +### 其他操作 -让我们为我们的应用程序添加一些缺失的功能,包括删除和更新单个笔记。 +让我们为我们的应用添加一些缺失的功能,包括删除和更新单个笔记。 -从数据库删除笔记的最简单方法是使用[findByIdAndDelete](https://mongoosejs.com/docs/api/model.html#Model.findByIdAndDelete())方法: +从数据库删除笔记最简单的方法是使用 [findByIdAndDelete](https://mongoosejs.com/docs/api/model.html#Model.findByIdAndDelete()) 方法: ```js app.delete('/api/notes/:id', (request, response, next) => { @@ -1033,111 +1058,108 @@ app.delete('/api/notes/:id', (request, response, next) => { ``` -在删除资源的两种"成功"情况下,后端都以 204 no content 的状态码响应。这两种不同的情况是删除存在的笔记,和删除数据库中不存在的笔记 _result_ 回调参数可以用于检查是否实际删除了资源,如果我们认为有必要,我们可以使用这个信息为这两种情况返回不同的状态码。任何发生的异常都会传递给错误处理器。 +对于删除资源的两种“成功”情况,后端都以 204 no content 的状态码响应。这两种不同的情况是删除存在的笔记,和删除数据库中不存在的笔记。_result_ 回调参数可以用于检查是否实际删除了资源,并且如果我们认为有必要的话,我们也可以根据 _result_ 的信息为两种情况返回不同的状态码。任何发生的异常都会传递给错误处理函数。 - -使用[findByIdAndUpdate](https://mongoosejs.com/docs/api/model.html#model_Model-findByIdAndUpdate)方法可以轻松地切换笔记的重要性。 + +让我们实现更新单条笔记的功能,允许更改笔记的重要性。更新笔记的功能实现如下: ```js app.put('/api/notes/:id', (request, response, next) => { - const body = request.body + const { content, important } = request.body - const note = { - content: body.content, - important: body.important, - } + Note.findById(request.params.id) + .then(note => { + if (!note) { + return response.status(404).end() + } + + note.content = content + note.important = important - Note.findByIdAndUpdate(request.params.id, note, { new: true }) - .then(updatedNote => { - response.json(updatedNote) + return note.save().then((updatedNote) => { + response.json(updatedNote) + }) }) .catch(error => next(error)) }) ``` - -在上面的代码中,我们还允许编辑笔记的内容。 + +要更新的笔记首先通过 _findById_ 方法从数据库中获取。如果数据库中没有具有给定 id 的对象,变量 _note_ 的值将为 _null_,会以状态码 404 Not Found 响应查询。 - -注意,findByIdAndUpdate方法接收的是一个常规的JavaScript对象作为参数,而不是一个用Note构造函数创建的新笔记对象。 + +如果找到了具有给定 id 的对象,会用请求中提供的数据更新对象的 _content_ 和 _important_ 字段,然后用 _save()_ 方法将修改后的笔记保存到数据库。最后发送更新后的笔记来响应 HTTP 请求。 - -关于使用findByIdAndUpdate方法有一个重要的细节。默认情况下,事件处理器的updatedNote参数接收的是[没有修改的](https://mongoosejs.com/docs/api/model.html#model_Model-findByIdAndUpdate)原始文档。我们添加了可选的{ new: true }参数,这将导致我们的事件处理器被调用时,使用新的修改过的文档而不是原始文档。 + +值得注意的一点是,代码现在包含嵌套的 Promise,在外层的 _.then_ 方法内部又定义了另一个 [Promise 链](https://javascript.info/promise-chaining): - -在直接使用Postman或VS Code REST客户端测试后端后,我们可以验证它似乎是工作的。前端也似乎能够使用数据库与后端一起工作。 +```js + .then(note => { + if (!note) { + return response.status(404).end() + } - -你可以在part3-5分支的[这个GitHub仓库](https://github.com/fullstack-hy2020/part3-notes-backend/tree/part3-5)中找到我们当前应用程序的完整代码。 -### A true full stack developer's oath + note.content = content + note.important = important - -现在又是练习的时候了。我们的应用程序的复杂性现在又上升了一个阶段,因为除了前端和后端,我们还有一个数据库。 + // highlight-start + return note.save().then((updatedNote) => { + response.json(updatedNote) + }) + // highlight-end +``` - -的确,有很多可能的错误来源。 + +通常不推荐这样做,这会使代码难以阅读。然而这样做至少在本例中能正确运行,因为这样做能确保 _save()_ 方法之后的 _.then_ 块只有在数据库中找到了具有给定 id 的笔记并调用了 _save()_ 方法时才会执行。在本课程的第四章节,我们将学习 async/await 语法,来更简单、更清晰地处理这类情况。 - -所以我们应该再次扩展我们的誓言: + +在直接使用 Postman 或 VS Code REST Client 测试后端后,我们可以验证它似乎能正确运行。前端也显示能与使用数据库的后端一起正确运行。 - -全栈开发是极其困难的,这就是为什么我会使用所有可能的手段来使它变得更容易 - - - -- 我会一直打开浏览器开发者控制台 -- 我会使用浏览器开发工具的网络标签,确保前端和后端的通信符合我的预期 -- 我会不断关注服务器的状态,确保前端发送到那里的数据按我预期的方式保存 -- 我会关注数据库:后端是否以正确的格式保存数据 -- 我会以小步骤前进 -- 我会写很多的_console.log_语句,以确保我理解代码的行为,并帮助定位问题 -- 如果我的代码不能工作,我不会写更多的代码。相反,我开始删除代码,直到它工作,或者只是返回到一切都还在工作的状态 -- 当我在课程的Discord频道或其他地方寻求帮助时,我会合适地提出我的问题,看[这里](https://fullstackopen.com/en/part0/general_info#how-to-get-help-in-discord)了解如何寻求帮助。 + +你可以在[这个 GitHub 仓库](https://github.com/fullstack-hy2020/part3-notes-backend/tree/part3-5)的 part3-5 分支中找到我们当前应用的完整代码。
-### Exercises 3.15.-3.18. + +### 练习 3.15.~3.18. -#### 3.15: Phonebook database, step 3 + +#### 3.15:电话簿数据库,第 3 步 -更改后端,使得删除电话簿条目在数据库中得到反映。 +更改后端,使得删除电话簿记录能在数据库中显示。 -在进行更改后,验证前端是否仍然工作。 +在进行更改后,验证前端是否仍然正确运行。 -#### 3.16: Phonebook database, step 4 + +#### 3.16:电话簿数据库,第 4 步 -将应用程序的错误处理移动到新的错误处理中间件。 +将应用的错误处理移到新的错误处理中间件。 -#### 3.17*: Phonebook database, step 5 + +#### 3.17*:电话簿数据库,第 5 步 -如果用户试图为电话簿中已有姓名的人创建新的电话簿条目,前端将尝试通过向条目的唯一URL发送HTTP PUT请求来更新现有条目的电话号码。 +如果用户试图为姓名已存在于电话簿中的人创建新的电话簿记录,前端将尝试通过向记录的唯一 URL 发送 HTTP PUT 请求来更新现有记录的电话号码。 -修改后端以支持这个请求。 +修改后端以支持这类请求。 -在进行更改后,验证前端是否工作。 +在进行更改后,验证前端是否正确运行。 -#### 3.18*: Phonebook database step 6 + +#### 3.18*:电话簿数据库 第 6 步 -也试试更新api/persons/:idinfo路由的处理,以使用数据库,并验证它们是否可以直接使用浏览器、Postman或VS Code REST客户端工作。 +将 api/persons/:idinfo 路由的处理也更新为使用数据库,并验证它们是否可以直接在浏览器、Postman 或 VS Code REST Client 上正确运行。 -从浏览器查看单个电话簿条目应该是这样的: +从浏览器查看单个电话簿记录应该类似这样: ![screenshot of browser showing one person with api/persons/their_id](../../images/3/49.png) diff --git a/src/content/3/zh/part3d.md b/src/content/3/zh/part3d.md index 1e69b10cdb7..e0124d66a9e 100644 --- a/src/content/3/zh/part3d.md +++ b/src/content/3/zh/part3d.md @@ -8,13 +8,13 @@ lang: zh
-我们通常希望对存储在应用程序数据库中的数据应用一些约束。我们的应用程序不应接受缺少或空的content属性的笔记。在路由处理器中检查笔记的有效性: +我们通常希望对存储在应用程序数据库中的数据应用一些约束。我们的应用程序不应接受 content 属性缺失或为空的笔记。现在笔记的有效性在路由处理函数中检查: ```js app.post('/api/notes', (request, response) => { const body = request.body // highlight-start - if (body.content === undefined) { + if (!body.content) { return response.status(400).json({ error: 'content missing' }) } // highlight-end @@ -24,10 +24,10 @@ app.post('/api/notes', (request, response) => { ``` -如果笔记没有content属性,我们将以400 bad request的状态码响应请求。 +如果笔记没有 content 属性,我们就以 400 bad request 的状态码响应请求。 -在数据存储到数据库之前验证数据格式的一种更智能的方法是使用Mongoose提供的[验证](https://mongoosejs.com/docs/validation.html)功能。 +在数据存储到数据库之前验证数据格式的一种更智能的方法是使用 Mongoose 提供的[验证](https://mongoosejs.com/docs/validation.html)功能。 我们可以为模式中的每个字段定义特定的验证规则: @@ -46,13 +46,13 @@ const noteSchema = new mongoose.Schema({ ``` -现在,content字段要求至少为五个字符长,并且被设置为必需,意味着它不能缺失。我们没有对important字段添加任何约束,所以它在模式中的定义没有改变。 +现在 content 字段要求至少五个字符长,并且被设为 required,意味着字段不能缺失。我们没有对 important 字段添加任何约束,所以它在模式中的定义没有改变。 -minLengthrequired验证器是Mongoose提供的[内置](https://mongoosejs.com/docs/validation.html#built-in-validators)验证器。如果没有一个内置的验证器能满足我们的需求,Mongoose的[自定义验证器](https://mongoosejs.com/docs/validation.html#custom-validators)功能允许我们创建新的验证器。 +minLengthrequired 验证器是 Mongoose 提供的[内置](https://mongoosejs.com/docs/validation.html#built-in-validators)的验证器。如果内置的验证器全都无法满足我们的需求,我们还可以用 Mongoose 的[自定义验证器](https://mongoosejs.com/docs/validation.html#custom-validators)功能创建新的验证器。 -如果我们试图在数据库中存储一个违反了某些约束的对象,操作将会抛出异常。让我们改变我们创建新笔记的处理器,使其将任何可能的异常传递给错误处理中间件: +如果我们试图在数据库中存储一个违反了某些约束的对象,操作将会抛出异常。让我们更改创建新笔记的处理函数,来将任何可能的异常传递给错误处理中间件: ```js app.post('/api/notes', (request, response, next) => { // highlight-line @@ -72,7 +72,7 @@ app.post('/api/notes', (request, response, next) => { // highlight-line ``` -让我们扩展错误处理器以处理这些验证错误: +让我们扩展错误处理函数来处理这些验证错误: ```js const errorHandler = (error, request, response, next) => { @@ -89,94 +89,79 @@ const errorHandler = (error, request, response, next) => { ``` -当对象验证失败时,我们从Mongoose返回以下默认错误消息: +当对象验证失败时,我们返回以下 Mongoose 的默认错误消息: ![postman显示错误消息](../../images/3/50.png) - -我们注意到后端现在有一个问题:在编辑笔记时没有进行验证。 -这个问题可以解决,[update-validators 文档](https://mongoosejs.com/docs/validation.html#update-validators)解释说,在执行findOneAndUpdate和相关方法时,默认不会运行验证。 - - -但是要修复这个问题很简单。让我们也稍微改写一下路由代码: - -```js -app.put('/api/notes/:id', (request, response, next) => { - const { content, important } = request.body // highlight-line - - Note.findByIdAndUpdate( - request.params.id, - { content, important }, // highlight-line - { new: true, runValidators: true, context: 'query' } // highlight-line - ) - .then(updatedNote => { - response.json(updatedNote) - }) - .catch(error => next(error)) -}) -``` - -### Deploying the database backend to production + +### 将数据库后端部署到生产环境 -该应用程序应该能在Fly.io/Render上按原样工作。由于到目前为止我们只对后端进行了修改,所以我们不需要生成前端的新生产构建。 +该应用程序应该能在 Fly.io/Render 上按原样工作。由于到目前为止我们只对后端进行了修改,所以我们不需要构建前端的新生产版本。 -在dotenv中定义的环境变量只会在后端不处于生产模式时使用,即在Fly.io或Render中。 +dotenv 中定义的环境变量只会在后端不处于生产模式,即不在 Fly.io 或 Render 中时使用。 -对于生产环境,我们需要在托管我们应用的服务中设置数据库URL。 +对于生产环境,我们需要在托管我们应用的服务中设置数据库 URL。 -在Fly.io中,可以通过_fly secrets set_命令来完成: +在 Fly.io 中,可以通过 _fly secrets set_ 命令来完成: ```bash -fly secrets set MONGODB_URI='mongodb+srv://fullstack:thepasswordishere@cluster0.o1opl.mongodb.net/noteApp?retryWrites=true&w=majority' +fly secrets set MONGODB_URI='mongodb+srv://fullstack:thepasswordishere@cluster0.a5qfl.mongodb.net/noteApp?retryWrites=true&w=majority' ``` -当应用正在开发过程中,很可能会出现一些失败的情况。例如,当我第一次部署带有数据库的应用时,一个笔记都没有看到: +在开发应用的过程中,很可能会出现一些失败的情况。例如,当我第一次部署带有数据库的应用时,一个笔记都没有看到: ![浏览器显示没有出现任何笔记](../../images/3/fly-problem1.png) -浏览器控制台的网络标签页显示,获取笔记的请求并未成功,请求只是在_pending_状态下停留了很长时间,直到最后以502状态码失败。 +浏览器控制台的网络标签页显示并未成功获取笔记,请求只是在 _pending_ 状态下停留了很长时间,最后以 502 状态码失败。 -浏览器控制台必须始终保持打开状态! +必须始终打开浏览器控制台! -同时,持续关注服务器日志也非常重要。当我打开 _fly logs_ 查看日志时,问题就显而易见了: +同时,持续关注服务端日志也非常重要。当我用 _fly logs_ 打开日志时,问题就显而易见了: ![fly.io服务器日志显示连接到未定义](../../images/3/fly-problem3.png) -数据库URL是 _undefined_ ,所以忘记了执行 *fly secrets set MONGODB\_URI* 命令。 +数据库 URL 是 _undefined_ ,所以是忘记执行 *fly secrets set MONGODB\_URI* 命令了。 + + +你还需要在 MongoDB Atlas 中将 fly.io 应用的 IP 地址添加到白名单中。否则 MongoDB 会拒绝连接。 + + +遗憾的是,fly.io 不会给你的应用提供一个专门的 IPv4 地址,所以你需要在 MongoDB Atlas 中允许所有的 IP 地址。 -在使用Render时,可以通过在仪表板中定义适当的环境变量来提供数据库URL: +在使用 Render 时,可以通过在仪表板中定义适当的环境变量来提供数据库 URL: ![render仪表板显示MONGODB_URI环境变量](../../images/3/render-env.png) -Render仪表板显示服务器日志: +Render 仪表板显示服务端日志: ![render仪表板上有箭头指向在端口10000上运行的服务器](../../images/3/r7.png) -你可以在[此GitHub仓库](https://github.com/fullstack-hy2020/part3-notes-backend/tree/part3-6)的part3-6分支中找到我们当前应用的完整代码。 +你可以在[这个 GitHub 仓库](https://github.com/fullstack-hy2020/part3-notes-backend/tree/part3-6)的 part3-6 分支中找到我们当前应用的完整代码。
-### Exercises 3.19.-3.21. + +### 练习 3.19.~3.21. -#### 3.19*: Phonebook database, step 7 + +#### 3.19*:电话簿数据库,第 7 步 -将验证扩展,使得存储在数据库中的名称至少需要三个字符长。 +扩展验证,使存储在数据库中的名字需要至少三个字符长。 扩展前端,使其在发生验证错误时显示某种形式的错误消息。可以通过添加一个 catch 块来实现错误处理,如下所示: @@ -194,13 +179,14 @@ personService ``` -你可以显示由Mongoose返回的默认错误消息,尽管它们并不像它们可能的那样易读: +你可以显示 Mongoose 返回的默认错误消息,尽管这些错误信息并没有那么易读: ![电话簿屏幕截图显示人员验证失败](../../images/3/56e.png) -**注意:**在 _update_ 操作中,mongoose验证器默认是关闭的。[阅读文档](https://mongoosejs.com/docs/validation.html)以确定如何启用它们。 +**注意:**在 update 操作中,mongoose 验证器默认是关闭的。[阅读文档](https://mongoosejs.com/docs/validation.html)来确定如何启用它们。 -#### 3.20*: Phonebook database, step 8 + +#### 3.20*:电话簿数据库,第 8 步 为你的电话簿应用添加验证,确保电话号码的格式正确。电话号码必须: @@ -210,58 +196,81 @@ personService - eg. 09-1234556 and 040-22334455 are valid phone numbers - eg. 1234556, 1-22334455 and 10-22-334455 are invalid --> -- 长度为8或更多 -- 由两部分组成,两部分由-分隔,第一部分有两个或三个数字,第二部分也由数字组成 - - 例如,09-1234556和040-22334455是有效的电话号码 - - 例如,1234556,1-22334455和10-22-334455是无效的电话号码 +- 8 个字符或更长 +- 由两部分组成,由“-”分隔,第一部分有两个或三个数字,第二部分也都由数字组成 + - 例如,09-1234556 和 040-22334455 是有效的电话号码 + - 例如,1234556、1-22334455 和 10-22-334455 是无效的电话号码 使用[自定义验证器](https://mongoosejs.com/docs/validation.html#custom-validators)来实现验证的第二部分。 -如果HTTP POST请求试图添加一个电话号码无效的人,服务器应该以适当的状态码和错误消息响应。 +如果 HTTP POST 请求试图添加一个电话号码无效的人,服务端应该响应合适的状态码和错误消息。 -#### 3.21 Deploying the database backend to production + +#### 3.21 将数据库后端部署到生产环境 -Generate a new "full stack" version of the application by creating a new production build of the frontend, and copying it to the backend repository. Verify that everything works locally by using the entire application from the address . -通过创建前端的新的生产构建,生成应用程序的新的"全栈"版本,并将其复制到后端仓库。验证本地的所有操作是否正常,通过从地址使用整个应用程序。 + +通过构建前端的新生产版本,并复制到后端仓库,来生成应用程序的新“全栈”版本。通过在地址 使用整个应用程序,验证所有操作在本地是否正常。 将最新版本推送到 Fly.io/Render,并验证那里的所有操作是否也正常。 - -**NOTE**: 你应该将后端部署到云服务。如果你使用的是Fly.io,命令应该在后端的根目录中运行(也就是在后端的package.json所在的目录中)。如果使用的是Render,后端必须位于你的仓库的根目录中。 - - -在这一部分的任何阶段,你都不应该直接部署前端。整个部分都是部署后端仓库,没有其他的。 + +**注:**在本章节的任何阶段,你都**不**应直接部署前端。整个章节都只部署后端。将前端构建的生产版本添加到后端仓库中,然后让后端来提供,如同[由后端提供静态文件](/zh/part3/把应用部署到互联网上#由后端提供静态文件)一节中所描述的那样。
-### Lint + +### lint -在我们进入下一部分之前,我们介绍一个重要的工具,叫做[lint]()。维基百科对lint的描述如下: +在我们进入下一章节之前,我们来介绍一个重要的工具,叫做 [lint]()。维基百科对 lint 的描述如下: -> 一般来说,lint或者linter是任何检测和标记编程语言中错误的工具,包括样式错误。术语 _lint-like behavior_ 有时用于标记可疑语言使用的过程。Lint类 的工具通常对源代码进行静态分析。 +> 一般来说,lint 或 linter 是任何检测并标记编程语言中的错误,包括样式错误的工具。术语“lint 类行为”有时用于标记语言的可疑用法的过程。lint 类工具通常对源代码进行静态分析。 -在编译的静态类型语言如Java中,像NetBeans这样的IDE可以指出代码中的错误,甚至是编译错误之外的错误。像[checkstyle](https://checkstyle.sourceforge.io)这样的用于执行[静态分析](https://en.wikipedia.org/wiki/Static_program_analysis)的附加工具,可以用来扩展IDE的能力,也可以指出与样式相关的问题,如缩进。 +对于编译型静态类型语言,比如 Java,NetBeans 等 IDE 可以指出代码中的错误,甚至不止是编译错误。像 [checkstyle](https://checkstyle.sourceforge.io) 这样用于执行[静态分析](https://en.wikipedia.org/wiki/Static_program_analysis)的附加工具,可以用来扩展 IDE 的能力,来指出与样式相关的问题,如缩进。 -在JavaScript领域,目前主导的静态分析(又名"linting")工具是[ESlint](https://eslint.org/)。 +在 JavaScript 领域,目前领头的静态分析工具(又称“linting”)是[ESlint](https://eslint.org/)。 - -让我们使用以下命令将ESlint作为开发依赖项安装到notes后端项目中: + +让我们将 ESLint 添加为后端的开发依赖项。开发依赖项是只在开发应用的过程中需要的工具。比如,和测试有关的工具就是开发依赖项。当应用以生产模式运行时,就不需要开发依赖项了。 + + +使用以下命令将 ESlint 作为开发依赖项安装到后端项目中: ```bash -npm install eslint --save-dev +npm install eslint @eslint/js --save-dev +``` + + +package.json 文件的内容会这么变化: + +```js +{ + //... + "dependencies": { + "dotenv": "^16.4.7", + "express": "^5.1.0", + "mongoose": "^8.11.0" + }, + "devDependencies": { // highlight-line + "@eslint/js": "^9.22.0", // highlight-line + "eslint": "^9.22.0" // highlight-line + } +} ``` + +该命令在文件中添加了 devDependencies 一节,并在其中添加了 eslint@eslint/js,同时在 node_modules 目录中安装了所需的库。 + -之后我们可以用以下命令初始化一个默认的ESlint配置: +之后我们可以用以下命令初始化默认的 ESlint 配置: ```bash npx eslint --init @@ -272,87 +281,106 @@ npx eslint --init ![ESlint初始化的终端输出](../../images/3/52new.png) - -配置将会保存在 _.eslintrc.js_ 文件中。我们将在 _env_ 配置中将 _browser_ 改为 _node_: + +配置将会保存在 _eslint.config.mjs_ 文件中。 + + +### 格式化配置文件 + + +让我们重设配置文件 _eslint.config.mjs_ 的格式,将它从当前的格式改为: ```js -module.exports = { - "env": { - "commonjs": true, - "es2021": true, - "node": true // highlight-line +import globals from 'globals' + +export default [ + { + files: ['**/*.js'], + languageOptions: { + sourceType: 'commonjs', + globals: { ...globals.node }, + ecmaVersion: 'latest', }, - "overrides": [ - { - "env": { - "node": true - }, - "files": [ - ".eslintrc.{js,cjs}" - ], - "parserOptions": { - "sourceType": "script" - } - } - ], - "parserOptions": { - "ecmaVersion": "latest" - }, - "rules": { - } -} + }, +] ``` - -让我们稍微修改一下配置。安装一个[插件](https://eslint.style/packages/js),该插件定义了一套与代码风格相关的规则: + +到目前为止,ESLint 配置文件在 _files_ 选项中定义了 _["\*\*/\*.js"]_,告诉 ESLint 要检查项目目录中的所有 JavaScript 文件。_languageOptions_ 属性指定 ESLint 应支持的语言特性相关的选项,其中我们将 _sourceType_ 设为“commonjs”。这表示我们项目中的 JavaScript 代码使用 CommonJS 模块系统,从而使 ESLint 能以相应的方法分析代码。 + + +_globals_ 属性指定预定义的全局变量。这里使用的展开运算符告诉 ESLint 包含 _globals.node_ 设定中定义的所有全局变量,比如 _process_。对于浏览器端代码,我们会在这里定义 _globals.browser_,来允许浏览器特有的全局变量,比如 _window_ 和 _document_。 + +最后,_ecmaVersion_ 属性设为“latest”。这将 ECMAScript 版本设为最新可用版本,意味着 ESLint 能理解并正确检查最新的 JavaScript 语法和特性。 +我们希望同时使用 [ESLint 推荐的](https://eslint.org/docs/latest/use/configure/configuration-files#using-predefined-configurations)和自定义的设置。之前安装的 _@eslint/js_ 包提供了 ESLint 的预定义配置。我们可以在配置文件中导入并启用: + +```js +import globals from 'globals' +import js from '@eslint/js' // highlight-line +// ... + +export default [ + js.configs.recommended, // highlight-line + { + // ... + }, +] ``` + + +我们已将 _js.configs.recommended_ 添加到配置数组的最上面,这确保 ESLint 的推荐设置会在我们自定义的选项之前先应用。 + + +让我们继续构建配置文件。安装一个定义了一套与代码样式相关的规则的[插件](https://eslint.style/packages/js): + +```bash npm install --save-dev @stylistic/eslint-plugin-js ``` - -启用插件并添加一个扩展定义和四个代码风格规则: + +导入并启用插件,并添加这四条代码样式规则: ```js -module.exports = { +import globals from 'globals' +import js from '@eslint/js' +import stylisticJs from '@stylistic/eslint-plugin-js' // highlight-line + +export default [ + { // ... - 'plugins': [ - '@stylistic/js' - ], - 'extends': 'eslint:recommended', - 'rules': { - '@stylistic/js/indent': [ - 'error', - 2 - ], - '@stylistic/js/linebreak-style': [ - 'error', - 'unix' - ], - '@stylistic/js/quotes': [ - 'error', - 'single' - ], - '@stylistic/js/semi': [ - 'error', - 'never' - ], - } -} + // highlight-start + plugins: { + '@stylistic/js': stylisticJs, + }, + rules: { + '@stylistic/js/indent': ['error', 2], + '@stylistic/js/linebreak-style': ['error', 'unix'], + '@stylistic/js/quotes': ['error', 'single'], + '@stylistic/js/semi': ['error', 'never'], + }, + // highlight-end + }, +] ``` - -扩展 _eslint:recommended_ 将一套[推荐的规则](https://eslint.org/docs/latest/rules/)添加到项目中。此外,还添加了关于缩进、换行、连字符和分号的规则。这四条规则都在[Eslint样式插件](https://eslint.style/packages/js)中定义了。 + +[plugins](https://eslint.org/docs/latest/use/configure/plugins) 属性提供了一种可以通过添加自定义规则、配置以及其他核心 ESLint 库中没有的功能来扩展 ESLint 功能的方法。我们已安装并启用了 _@stylistic/eslint-plugin-js_,它为 ESLint 添加了 JavaScript 样式的规则。此外,还添加了关于缩进、换行、引号和分号的规则,这四条规则都是在 [Eslint styles plugin](https://eslint.style/packages/js) 中定义的。 + +**Windows 用户注意事项:**样式规则中将换行样式设为了 _unix_。建议无论使用什么操作系统都使用 Unix 样式的换行符(_\n_),这样文件可以兼容大多数现代操作系统,并且在多人处理同一文件时更方便协作。如果使用 Windows 样式的换行符,ESLint 会产生如下错误:Expected linebreaks to be 'LF' but found 'CRLF'。遇到这种情况时,按照[这份指南](https://stackoverflow.com/questions/48692741/how-can-i-make-all-line-endings-eols-in-all-files-in-visual-studio-code-unix)将 Visual Studio Code 配置为使用 Unix 样式换行。 + + +### 运行 linter -可以使用以下命令检查和验证像 _index.js_ 这样的文件: +要检查和验证某个文件,比如 _index.js_,可以使用以下命令: ```bash npx eslint index.js ``` -我们建议为linting创建一个单独的 _npm script_: +建议为 linting 创建一个专门的 _npm 脚本_: ```json { @@ -370,126 +398,182 @@ npx eslint index.js 现在,_npm run lint_ 命令将检查项目中的每个文件。 - -当运行命令时,dist 目录中的文件也会被检查,我们不希望这种情况发生、我们可以通过在项目的根目录中创建一个[.eslintignore](https://eslint.org/docs/latest/use/configure/ignore#the-eslintignore-file) 文件来实现这一点,文件的内容如下: + +当运行命令时,dist 目录中的文件也会被检查。我们不希望这种情况发生,我们可以通过添加一个对象,并在其 [ignores](https://eslint.org/docs/latest/use/configure/ignore) 属性声明一个我们要忽略的目录和文件的数组来实现这一点。 -```bash -dist +```js +// ... +export default [ + js.configs.recommended, + { + files: ['**/*.js'], + // ... + }, + // highlight-start + { + ignores: ['dist/**'], + }, + // highlight-end +] ``` -这将导致整个dist目录不被ESlint检查。 +这会使得整个 dist 目录都不被 ESlint 检查。 -Lint对我们的代码有很多意见: +lint 对我们的代码有很多意见: ![ESlint错误的终端输出](../../images/3/53ea.png) - -我们暂时不去修复这些问题。 - - -从命令行执行linter的更好替代方案是将eslint-plugin配置到编辑器中,这将连续运行linter。通过使用插件,你将立即在代码中看到错误。你可以在[这里](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint)找到更多关于Visual Studio ESLint插件的信息。 + +相比于从命令行执行 linter,更好的替代方案是给编辑器配置 _eslint 插件_,来持续不断地运行 linter。使用这个插件后,你将立即在代码中看到错误。你可以在[这里](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint)找到更多关于 Visual Studio ESLint 插件的信息。 -VS Code的ESlint插件会用红线下划出风格违规: +VS Code ESlint 插件会用红线划出违反样式的地方: ![VScode ESLint插件显示错误的截图](../../images/3/54a.png) -这使得错误很容易被发现并立即修复。 +这会让错误很容易发现,从而立即修复。 + + +### 添加更多样式规则 - -ESlint有大量的[规则](https://eslint.org/docs/rules/),这些规则通过编辑 .eslintrc.js 文件就可以很容易地使用。 + +ESlint 有大量易于使用的[规则](https://eslint.org/docs/rules/),只要编辑 _eslint.config.mjs_ 文件即可使用。 - -让我们添加[eqeqeq](https://eslint.org/docs/rules/eqeqeq)规则,如果用非三等号运算符检查等式,它会发出警告。该规则是在配置文件的rules字段下添加的。 + +让我们添加 [eqeqeq](https://eslint.org/docs/rules/eqeqeq) 规则,如果相等不是用三等号*===*检查的,eqeqeq 规则就会警告我们。将该规则添加到配置文件的 rules 字段下。 ```js -{ +export default [ // ... - 'rules': { + rules: { // ... - 'eqeqeq': 'error', + eqeqeq: 'error', // highlight-line }, -} + // ... +] ``` -在我们进行这项工作的同时,让我们对规则进行一些其他更改。 +让我们顺便对规则进行一些其他更改。 - + 让我们阻止行尾的不必要的[尾随空格](https://eslint.org/docs/rules/no-trailing-spaces),要求[大括号前后始终有一个空格](https://eslint.org/docs/rules/object-curly-spacing),并且也要求箭头函数的函数参数中一致使用空格。 ```js -{ +export default [ // ... - 'rules': { + rules: { // ... - 'eqeqeq': 'error', + eqeqeq: 'error', + // highlight-start 'no-trailing-spaces': 'error', - 'object-curly-spacing': [ - 'error', 'always' - ], - 'arrow-spacing': [ - 'error', { 'before': true, 'after': true } - ] + 'object-curly-spacing': ['error', 'always'], + 'arrow-spacing': ['error', { before: true, after: true }], + // highlight-end }, -} +] ``` - -我们的默认配置从eslint:recommended中使用了一堆预定的规则: + +我们的默认配置使用了这里预定义的一串规则: -```bash -'extends': 'eslint:recommended', +```js +// ... + +export default [ + js.configs.recommended, + // ... +] ``` - -这包括一个关于 _console.log_ 命令的警告规则。可以通过在配置文件中将其"值"定义为0来[禁用](https://eslint.org/docs/latest/use/configure/rules)一条规则。我们暂时为no-console规则这样做。 + +这里包括一条警告 _console.log_ 命令的规则,而我们不想使用这条规则。禁用一条规则可以通过在配置文件中定义其“value”为 0 或 _off_ 来完成。让我们在此期间对 _no-console_ 规则这样做。 ```js -{ - // ... - 'rules': { +[ + { // ... - 'eqeqeq': 'error', - 'no-trailing-spaces': 'error', - 'object-curly-spacing': [ - 'error', 'always' - ], - 'arrow-spacing': [ - 'error', { 'before': true, 'after': true } - ], - 'no-console': 0 // highlight-line + rules: { + // ... + eqeqeq: 'error', + 'no-trailing-spaces': 'error', + 'object-curly-spacing': ['error', 'always'], + 'arrow-spacing': ['error', { before: true, after: true }], + 'no-console': 'off', // highlight-line + }, }, -} +] ``` - -**注意** 当你对.eslintrc.js文件进行更改时,建议从命令行运行linter。这将验证配置文件是否正确格式化: + +禁用 no-console 规则使我们能够使用 console.log 语句而不使 ESLint 将其标记为问题。在开发期间需要调试代码时这尤其有用。下面是包含迄今为止我们所做的所有修改的完整配置文件: + +```js +import globals from 'globals' +import js from '@eslint/js' +import stylisticJs from '@stylistic/eslint-plugin-js' + +export default [ + js.configs.recommended, + { + files: ['**/*.js'], + languageOptions: { + sourceType: 'commonjs', + globals: { ...globals.node }, + ecmaVersion: 'latest', + }, + plugins: { + '@stylistic/js': stylisticJs, + }, + rules: { + '@stylistic/js/indent': ['error', 2], + '@stylistic/js/linebreak-style': ['error', 'unix'], + '@stylistic/js/quotes': ['error', 'single'], + '@stylistic/js/semi': ['error', 'never'], + eqeqeq: 'error', + 'no-trailing-spaces': 'error', + 'object-curly-spacing': ['error', 'always'], + 'arrow-spacing': ['error', { before: true, after: true }], + 'no-console': 'off', + }, + }, + { + ignores: ['dist/**'], + }, +] +``` + + +**注** 当你对 _eslint.config.mjs_ 文件进行更改时,推荐从命令行运行 linter。这将验证配置文件的格式是否正确: + +![npm run lint 在终端的输出](../../images/3/lint2.png) -如果你的配置文件中有什么错误,lint插件可能会表现得相当不稳定。 +如果你的配置文件中有什么错误,lint 插件可能会表现得相当不稳定。 -许多公司定义编码标准,这些标准通过ESlint配置文件在整个组织中强制执行。不建议反复重新发明轮子,采用别人项目中的现成配置可能是个好主意。最近,许多项目通过采用Airbnb的[ESlint](https://github.com/airbnb/javascript/tree/master/packages/eslint-config-airbnb)配置,采纳了Airbnb的[Javascript风格指南](https://github.com/airbnb/javascript)。 +许多公司都会定义编码标准,并通过 ESlint 配置文件在整个组织中强制实施。不推荐反复重新发明轮子,采用别人项目中现成的配置是个明智的选择。最近,许多项目都采用了 Airbnb 的[Javascript 样式指南](https://github.com/airbnb/javascript),并使用了 Airbnb 的 [ESlint](https://github.com/airbnb/javascript/tree/master/packages/eslint-config-airbnb) 配置。 -你可以在 part3-7 分支的[这个GitHub仓库](https://github.com/fullstack-hy2020/part3-notes-backend/tree/part3-7)中找到我们当前应用程序的全部代码。 +你可以在[这个 GitHub 仓库](https://github.com/fullstack-hy2020/part3-notes-backend/tree/part3-7)的 part3-7 分支中找到我们当前应用程序的全部代码。
-### Exercise 3.22. + +### 练习 3.22. -#### 3.22: Lint configuration + +#### 3.22:lint 配置 -将ESlint添加到你的应用程序中,并修复所有的警告。 +将 ESlint 添加到你的应用程序中,并修复所有警告。 -这是课程的这一部分的最后一个练习。现在是时候将你的代码推送到GitHub,并在[练习提交系统](https://studies.cs.helsinki.fi/stats/courses/fullstackopen)中标记你已完成的所有练习了。 +这是课程这一章节的最后一道练习。现在是时候将你的代码推送到 GitHub,并在[练习上交系统](https://studies.cs.helsinki.fi/stats/courses/fullstackopen)中标记你完成的所有练习了。
diff --git a/src/content/partnavigation/partnavigation.js b/src/content/partnavigation/partnavigation.js index 9afba370eac..92a3976724c 100644 --- a/src/content/partnavigation/partnavigation.js +++ b/src/content/partnavigation/partnavigation.js @@ -176,9 +176,9 @@ module.exports = { }, 3: { a: 'Node.js 与 Express', - b: '把应用部署到网上', - c: '将数据存入MongoDB', - d: 'ESLint与代码检查', + b: '把应用部署到互联网上', + c: '将数据存入 MongoDB', + d: '验证与 ESLint', }, 4: { a: '从后端结构到测试入门',