Express 教程 3：使用数据库  (Mongoose) - 学习 Web 开发

Express 教程 3：使用数据库 (Mongoose)
=============================

* 上一页
* Overview: Express Nodejs
* 下一页

本文简要介绍了数据库以及 Node/Express 应用的数据库集成。然后演示了 Mongoose 为 LocalLibrary 提供数据库访问的方式。还讲解了对象模式（Schema）和模型（Model）的声明方式、主要域的类型、基础验证机制。同时还简短演示了访问模型数据的一些方法。

|  |  |
| --- | --- |
| 前提： | Express 教程 2: 创建站点骨架，了解数据库基础知识。 |
| 目标： | 使用 Mongoose 设计建造模型。 |

概览
--

图书馆的员工会使用 LocalLibrary 网站来保存藏书和借阅者的信息。会员会浏览和查找所需藏书，找到后预约或借阅。为了更高效地存取信息，网站将使用数据库。

Express 应用支持多款数据库，执行新建（**C**reate）、读取（**R**ead）、更新（**U**pdate）和删除（**D**elete）操作 (CRUD) 操作也有诸多途径。本教程先做一个不完全简介，然后对教程选用的机制进行详细介绍。

### 我可以使用什么数据库？

Express 应用可以使用 Node 支持的所有数据库（Express 本身不支持数据库管理的任何具体行为/需求）。有许多 流行的选择，包括 PostgreSQL、MySQL、Redis、SQLite 和 MongoDB。

选用数据库应考虑以下因素：进入生产状态用时/学习曲线、性能、复制/备份的易用度、成本、社区支持，等等。这些数据库各有千秋，但绝大多数都足以胜任 LocalLibrary 这样中小规模的网站了。

更多信息请参阅：数据库集成（Express 文档）。

### 与数据库交互的最佳方式是什么？

与数据库交互有两种方法：

* 使用数据库的原生查询语言（例如 SQL）
* 使用对象数据模型（Object Data Model，简称 ODM）或对象关系模型（Object Relational Model，简称 ORM）。ODM / ORM 能将网站中的数据表示为 JavaScript 对象，然后将它们映射到底层数据库。一些 ORM 只适用某些特定数据库，还有一些是普遍适用的。

使用 SQL 或其他受到支持的查询语言才能达到最佳性能。ODM 通常慢一些，因为在对象和数据库格式之间存在一层用于映射的翻译代码，使它不一定会选用最高性能的数据库查询（尤其是普遍使用级别的 ODM，它必须在各类数据库功能方面做出更大的折衷）。

使用 ORM 的好处是：程序员可以继续用 JavaScript 对象的思维而不用转向数据库语义的思维。在（同一个或不同网站）使用不同数据库时尤为明显。使用 ORM 还可以更方便地对数据进行验证和检查。

**备注：** 使用 ODM / ORM 通常可以降低开发和维护成本！除非你非常熟悉本地查询语言，或者项目对性能要求很高，否则强烈推荐使用 ODM。

### 我应该使用哪种 ORM/ODM ?

NPM 站点上有许多 ODM / ORM 解决方案（另请参阅 NPM 站点上的 odm 和 orm 标签列表）。

以下是迄今（2018 年 12 月）几种流行的解决方案：

* Mongoose：一款为异步工作环境设计的 MongoDB 对象建模工具。
* Waterline：从基于 Express 的 Sails 框架中提取的 ORM。它提供了一套统一的 API 来访问众多不同的数据库，其中包括 Redis，mySQL，LDAP，MongoDB 和 Postgres。
* Bookshelf：同时提供基于 promise 和传统回调两套接口，支持事务处理、渴求式/嵌套渴求式关系加载、多态关联，以及对一对一，一对多和多对多关系。支持 PostgreSQL、MySQL 和 SQLite3。
* Objection：以尽可能简单的方式使用 SQL 和底层数据库引擎的全部功能（支持 SQLite3、Postgres 和 MySQL）。
* Sequelize：基于 promise 的 Node.js 版 ORM，它支持 PostgreSQL、MySQL、MariaDB、SQLite 和 MSSQL，并提供可靠的事务支持、关系、复本读取等功能。
* Node ORM2：一款 Node.js 对象关系管理系统。支持 MySQL、SQLite 以及 Progress，可以帮助你用面向对象的方法操作数据库。
* JugglingDB：一款 Node.js 版跨数据库的 ORM。它为多数流行数据库提供了统一接口，当前支持 MySQL、SQLite3、Postgres、MongoDB、Redis 和 js-memory-storage（自写引擎，仅供测试用）。

一般来说，选择解决方案应该考虑功能和“社区活跃度”（下载量、贡献数、bug 报告、文档质量，等）。在撰写本文时，Mongoose 是最受欢迎的 ODM，选用 MongoDB 数据库时，它是一个合理的选择。

### 在 LocalLibrary 中使用 Mongoose 和 MongoDb

我们将在本地图书馆示例（以及本主题的其余部分）中使用 Mongoose ODM 来访问图书馆数据。Mongoose 作为 MongoDB（面向文档数据模型的开源 NoSQL 数据库）的前端。MongoDB 数据库里，“集合”中的“文档” 类似于 关系数据库里“表”中的“行”。

这种 ODM 和数据库的结合方式在 Node 社区中非常流行，一定程度上是因为文档存储和查询系统与 JSON 十分相似，因此 JavaScript 开发人员会非常熟悉。

**备注：** 使用 Mongoose 无需事先了解 MongoDB，但是部分 Mongoose 文档 对于熟悉 MongoDB 的朋友会更易于使用和理解。

下面将介绍如何为 LocalLibrary 网站 定义和访问 Mongoose 模式和模型。

设计 LocalLibrary 模型
------------------

在开始投入模型编写之前，有必要先思考一下：本网站需要存储什么数据？不同对象之间的关系是怎样的？

图书馆需要存储藏书信息（书名、摘要、作者、种类、ISBN），藏书副本信息（全站唯一 ID，借出状态，等）。还可能需要存储作者姓名之外的更多信息，以及多个作者的信息。还希望数据库内容能够根据书名、作者姓名、种类和编目进行排序。

有必要为每个“对象”（一组相关信息）设计独立的模型。本示例的关键对象包括书籍、书籍副本和作者。

也许还希望使用模型而不是站点代码来表示选项表（比如下拉列表），在选项无法预知或可能更改时更推荐模型方式。很明显，藏书类型（比如科幻小说、法语诗歌，等）就是这种情况。

确定模型和字段后还要考虑它们之间的关系，以下 UML 图显示了本示例即将定义的模型（框图）。如上所述，我们为藏书（一般细节）、藏书副本（系统）和作者创建了模型。还有一个可以动态选择的书籍种类模型。对于 `BookInstance:status`，我们不会为它建立模型，而是将可能的值直接编入站点代码中，因为我们不希望这些值发生变化。下图每个框都包括模型名、字段名和类型，还有方法及其返回类型。

下图还展示了模型之间的关系以及重复度（Multiplicity）。重复度就是图中两框间连线两端的数字，表示两个模型之间存在的关系的数量（最大值和最小值）。例如，`Book` 框和 `Genre` 框之间有连线说明二者之间存在关系，`Book` 模型端的数字（0..\*）表示一个种类必包括零种或多种藏书（多少都可以），而 `Genre` 端的数字表示一种藏书可以有零个或多个种类。

**备注：** 正如下文 Mongoose 入门 中所讲，通常应该把定义文档/模型关系的字段置于同一模型中（仍可通过在搜索相关 `_id` 来回寻模型间的关系）。以下的 Book 模式中定义了 Book/Genre 和 Book/Author 关系，BookInstance 模式中定义了 Book/BookInstance 关系。这样做是简便起见，但稍存歧义，让这些字段存在于其他模式中也是可以的。

![Mongoose Library Model with correct cardinality](/zh-CN/docs/Learn/Server-side/Express_Nodejs/mongoose/library_website_-_mongoose_express.png)

**备注：** 下面是一段入门知识，讲解如何定义和使用模型。请在阅读时想想将如何构建上图中的模型。

Mongoose 入门
-----------

这一段将简要介绍如何将 Mongoose 连接到 MongoDB 数据库，如何定义模式和模型，以及如何进行基本查询。

**备注：** 本入门受到 npm 上的 Mongoose 快速入门 和 Mongoose 官方文档 的“深度影响”。

### 安装 Mongoose 和 MongoDB

Mongoose 像任何其他依赖项一样，使用 NPM 将其安装在你的项目（**package.json**）中。请在项目文件夹中运行下面的命令以完成安装：

```bash
npm install mongoose
```

安装 Mongoose 会添加所有依赖项，包括 MongoDB 数据库驱动程序，但不会安装 MongoDB 本身。要安装 MongoDB 服务器，可以 点击下载 各操作系统的安装程序在本地安装。也可以使用云端 MongoDB 实例。

**备注：** 本教程选用 mLab 提供的 沙箱级 云端“数据库即服务”（Database as a Service，DBaaS）。它适用于开发环境，且部署过程与操作系统无关（DBaaS 也适用于生产环境）。

### 连接到 MongoDB

Mongoose 需要连接到 MongoDB 数据库。可以 `require()` 之，并通过 `mongoose.connect()` 连接到本地数据库，如下。

```js
// 导入 mongoose 模块
const mongoose = require("mongoose");

// 设置默认 mongoose 连接
const mongoDB = "mongodb://127.0.0.1/my\_database";
mongoose.connect(mongoDB);
// 让 mongoose 使用全局 Promise 库
mongoose.Promise = global.Promise;
// 取得默认连接
const db = mongoose.connection;

// 将连接与错误事件绑定（以获得连接错误的提示）
db.on("error", console.error.bind(console, "MongoDB 连接错误："));
```

可以用 `mongoose.connection` 取得默认的 `Connection` 对象。一旦连接，`Connection` 实例将触发打开事件。

**备注：** 可以使用 `mongoose.createConnection()` 创建其他连接。该函数与 `connect()` 的参数（数据库 URI，包括主机地址、数据库名、端口、选项等）一致，并返回一个 `Connection` 对象。

### 定义和添加模型

模型使用 `Schema` 接口进行定义。 `Schema` 可以定义每个文档中存储的字段，及字段的验证要求和默认值。还可以通过定义静态和实例辅助方法来更轻松地处理各种类型的数据，还可以像使用普通字段一样使用数据库中并不存在的虚拟属性（稍后讨论）。

`mongoose.model()` 方法将模式“编译”为模型。模型就可以用来查找、创建、更新和删除特定类型的对象。

**备注：** MongoDB 数据库中，每个模型都映射至一组文档。这些文档包含 `Schema` 模型定义的字段名/模式类型。

#### 定义模式

下面的代码片段中定义了一个简单的模式。首先 `require()` mongoose，然后使用 `Schema` 构造器创建一个新的模式实例，使用构造器的对象参数定义各个字段。

```js
// 获取 Mongoose
const mongoose = require("mongoose");

// 定义一个模式
var Schema = mongoose.Schema;

var SomeModelSchema = new Schema({
  a\_string: String,
  a\_date: Date,
});
```

上面示例只有两个字段（一个字符串和一个日期），接下来将展示其他字段类型、验证和其他方法。

#### 创建模型

使用 `mongoose.model()` 方法从模式创建模型：

```js
// 定义模式
const Schema = mongoose.Schema;

const SomeModelSchema = new Schema({
  a\_string: String,
  a\_date: Date,
});

// 使用模式“编译”模型
const SomeModel = mongoose.model("SomeModel", SomeModelSchema);
```

第一个参数是为模型所创建集合的别名（Mongoose 将为 SomeModel 模型创建数据库集合），第二个参数是创建模型时使用的模式。

**备注：** 定义模型类后，可以使用它们来创建、更新或删除记录，以及通过查询来获取所有记录或特定子集。我们将在以下“使用模型”部分展示，包括创建视图的情况。

#### 模式类型（字段）

模式可以包含任意数量的字段，每个字段代表 MongoDB 文档中的一段存储区域。下面是一个模式的示例，其中有许多常见字段类型和声明方式：

```js
const schema = new Schema({
  name: String,
  binary: Buffer,
  living: Boolean,
  updated: { type: Date, default: Date.now },
  age: { type: Number, min: 18, max: 65, required: true },
  mixed: Schema.Types.Mixed,
  \_someId: Schema.Types.ObjectId,
  array: [],
  ofString: [String], // 其他类型也可使用数组
  nested: { stuff: { type: String, lowercase: true, trim: true } },
});
```

大多数 模式类型（ SchemaType，字段名之后的描述符）都是自解释的。除了：

* `ObjectId`：表示数据库中某一模型的特定实例。例如，一本书可能会使用它来表示其作者对象。它实际只包含指定对象的唯一 ID（`_id`） 。可以使用 `populate()` 方法在需要时提取相关信息。
* Mixed：任意模式类型。
* []：对象数组。以在此类模型上执行 JavaScript 数组操作（`push`、`pop`、`unshift`等）。上例中有一个没有指定类型的对象数组和一个 `String` 对象数组，数组中的对象可以是任意类型的。

代码还展示了声明字段的两种方法：

* 字段名和类型名作为键 - 值对（就像 `name`、`binary` 和 `living`）。
* 字段名后跟一个对象，在对象中定义 `type` 和字段的其他选项，可以是：
    + 默认值。
    + 内置验证器（例如最大/最小值）和自定义验证函数。
    + 该字段是否必需。
    + 是否将 `String` 字段自动转换为小写、大写，或截断两端空格（例如`{ type: String, lowercase: true, trim: true }`)

关于选项的更多信息请参阅 模式类型（Mongoose 英文文档）。

#### 验证

Mongoose 提供内置的和自定义的验证器，以及同步的和异步的验证器。你可以在所有情况下，指定可接受的范围或值，以及验证失败的错误消息。

内置的验证器包括：

* 所有 模式类型 都具有内置的 required 验证器。用于指定当前字段是否为保存文档所必需的。
* Number 有数值范围验证器 min 和 max。
* String 有：
    + enum：指定当前字段允许值的集合。
    + match：指定字符串必须匹配的正则表达式。
    + 字符串的最大长度 maxlength 和最小长度 minlength

以下是类型验证器和错误消息的设定方法（从 Mongoose 文档稍作修改而来）：

```js
const breakfastSchema = new Schema({
  eggs: {
    type: Number,
    min: [6, "鸡蛋太少"],
    max: 12,
  },
  drink: {
    type: String,
    enum: ["咖啡", "茶"],
  },
});
```

字段验证的完整信息请参阅 验证（Mongoose 英文文档）。

#### 虚拟属性

虚拟属性是可以获取和设置、但不会保存到 MongoDB 的文档属性。getter 可用于格式化或组合字段，而 setter 可用于将单个值分解为多个值从而便于存储。文档中的示例，从名字和姓氏字段构造（并解构）一个全名虚拟属性，这比每次在模板中使用全名更简单，更清晰。

**备注：** 我们将使用库中的一个虚拟属性，用路径和记录的 `_id` 来为每个模型记录定义唯一的 URL。

更多信息请参阅 虚拟属性（Mongoose 英文文档）。

#### 方法和查询助手

模式支持 实例方法、静态方法 和 查询助手。实例方法和静态方法外表很相似，但有本质区别，实例方法针对特定记录，且可以访问当前对象。查询助手可用于扩展 Mongoose 的 链式查询 API（例如，在 `find()`、`findOne()` 和 `findById()` 方法外还可以添加一个“`byName`”查询）。

### 使用模型

就可以使用创建好的模式来创建模型。模型即数据库中可以搜索的一类文档，模型的实例即可以存取的单个文档。

以下是简介。更多信息请参阅：模型（Mongoose 英文文档）。

#### 创建和修改文档

可以通过定义模型的实例并调用 `save()` 来创建记录。以下示例假定 `SomeModel` 是用现有模式创建的模型（只有一个字段 "`name`" ）：

```js
// 创建一个 SomeModel 模型的实例
const awesome_instance = new SomeModel({ name: "牛人" });

// 传递回调以保存这个新建的模型实例
awesome_instance.save(function (err) {
  if (err) {
    return handleError(err);
  } // 已保存
});
```

记录的创建（以及更新、删除和查询）操作是异步的，可以提供一个回调函数在操作完成时调用。由于 API 遵循错误参数优先的惯例，因此回调的第一个参数必为错误值（或 `null`）。如果 API 需要返回一些结果，则将结果作为第二个参数。

还可以使用 `create()`，在定义模型实例的同时将其保存。回调的第一个参数返回错误，第二个参数返回新建的模型实例。

```js
SomeModel.create({ name: "也是牛人" }, function (err, awesome\_instance) {
  if (err) {
    return handleError(err);
  } // 已保存
});
```

每个模型都有一个相关的连接（使用 `mongoose.model()` 时将做为默认连接）。可以通过创建新连接并对其调用 `.model()`，从而在另一个数据库上创建文档。

可以使用“圆点”加字段名来访问、修改新记录中的字段。操作后必须调用 `save()` 或 `update()` 以将改动保存回数据库。

```js
// 使用圆点来访问模型的字段值
console.log(awesome_instance.name); // 控制台将显示 '也是牛人'

// 修改字段内容并调用 save() 以修改记录
awesome_instance.name = "酷毙了的牛人";
awesome_instance.save(function (err) {
  if (err) {
    return handleError(err);
  } // 已保存
});
```

#### 搜索记录

可以使用查询方法搜索记录，查询条件可列在 JSON 文档中。以下代码展示了如何在数据库中找到所有网球运动员，并返回运动员姓名和年龄字段。这里只指定了一个匹配字段（运动项目，`sport`），也可以添加更多条件，指定正则表达式，或去除所有条件以返回所有运动员。

```js
const Athlete = mongoose.model("Athlete", yourSchema);

// SELECT name, age FROM Athlete WHERE sport='Tennis'
Athlete.find({ sport: "Tennis" }, "name age", function (err, athletes) {
  if (err) {
    return handleError(err);
  } // 'athletes' 中保存一个符合条件的运动员的列表
});
```

若像上述代码那样指定回调，则查询将立即执行。搜索完成后将调用回调。

**备注：** Mongoose 中所有回调都使用 `callback(error, result)` 模式。如果查询时发生错误，则参数 `error` 将包含错误文档，`result` 为 `null`。如果查询成功，则 `error`为 `null`，查询结果将填充至 `result` 。

若未指定回调，则 API 将返回 Query 类型的变量。可以使用该查询对象来构建查询，随后使用 `exec()` 方法执行（使用回调）。

```js
// 寻找所有网球运动员
const query = Athlete.find({ sport: "Tennis" });

// 查找 name, age 两个字段
query.select("name age");

// 只查找前 5 条记录
query.limit(5);

// 按年龄排序
query.sort({ age: -1 });

// 以后某个时间运行该查询
query.exec(function (err, athletes) {
  if (err) {
    return handleError(err);
  } // athletes 中保存网球运动员列表，按年龄排序，共 5 条记录
});
```

上面的查询条件定义在 `find()` 方法中。也可以使用 `where()` 函数来执行此操作，可以使用点运算符（`.`）将所有查询链接在一起。以下代码与上述的查询基本相同，还添加了年龄范围的附加条件。

```js
Athlete.find()
  .where("sport")
  .equals("Tennis")
  .where("age")
  .gt(17)
  .lt(50) // 附加 WHERE 查询
  .limit(5)
  .sort({ age: -1 })
  .select("name age")
  .exec(callback); // 回调函数的名字是 callback
```

`find()` 方法会取得所有匹配记录，但通常你只想取得一个。以下方法可以查询单个记录：

* `findById()`：用指定 `id` 查找文档（每个文档都有一个唯一 `id`）。
* `findOne()`：查找与指定条件匹配的第一个文档。
* `findByIdAndRemove()`、`findByIdAndUpdate()`、`findOneAndRemove()`、 `findOneAndUpdate()`：通过 `id` 或条件查找单个文档，并进行更新或删除。以上是更新和删除记录的便利函数。

**备注：** 还有一个 `count()` 方法，可获取匹配条件的项目的个数。在只期望获得记录的个数而不想获取实际的记录时可以使用。

查询还能做更多。请参阅 查询（Mongoose 英文文档）。

#### 文档间协同——population

可以使用 `ObjectId` 模式字段来创建两个文档/模型实例间一对一的引用，（一组 `ObjectIds` 可创建一对多的引用）。该字段存储相关模型的 id。如果需要相关文档的实际内容，可以在查询中使用 `populate()` 方法，将 id 替换为实际数据。

例如，以下模式定义了作者和简介。每个作者可以有多条简介，我们将其表示为一个 `ObjectId` 数组。每条简介只对应一个作者。“`ref`”（黑体字）告知模式分配哪个模型给该字段。

```js
const mongoose = require("mongoose");
const Schema = mongoose.Schema;

const authorSchema = Schema({
  name: String,
  stories: [{ type: Schema.Types.ObjectId, ref: "Story" }],
});

const storySchema = Schema({
  author: { type: Schema.Types.ObjectId, ref: "Author" },
  title: String,
});

const Story = mongoose.model("Story", storySchema);
const Author = mongoose.model("Author", authorSchema);
```

可以通过分配 `_id` 值来保存对相关文档的引用。下面我们创建一个作者、一条简介，并将新简介的 `author` 字段设置为新建作者的 id。

```js
const wxm = new Author({ name: "司马迁" });

wxm.save(function (err) {
  if (err) {
    return handleError(err);
  }

  // 现在库中有了作者司马迁，我们来新建一条简介
  const story = new Story({
    title: "司马迁是历史学家",
    author: wxm._id, // author 设置为作者 司马迁 的 \_id。ID 是自动创建的。
  });

  story.save(function (err) {
    if (err) {
      return handleError(err);
    } // 司马迁有了一条简介
  });
});
```

现在简介文档通过作者文档的 ID 引用了作者。可使用 `populate()` 在简介中获取作者信息，如下所示。

```js
Story.findOne({ title: "司马迁是历史学家" })
  .populate("author") // 使用作者 id 填充实际作者信息
  .exec(function (err, story) {
    if (err) {
      return handleError(err);
    }
    console.log("作者是 %s", story.author.name);
    // 控制台将打印 "作者是 司马迁"
  });
```

**备注：** 目光敏锐的读者可能会注意到，新的简介添加了作者，但并没有添加到 `stories` 数组中。那么怎样才能得到指定作者的所有简介？考虑把作者添加到 `stories` 数组中，但会导致作者和简介相关信息的要在两处进行维护。

更好的方法是获取作者的 `_id`，然后使用 `find()` 在所有简介的作者字段中搜索。

```js
Story.find({ author: wxm._id }).exec(function (err, stories) {
  if (err) {
    return handleError(err);
  } // 返回所有 author 字段的值为 司马迁 id 的简介
});
```

以上是本教程中“项目间协同”需要了解的所有内容。更多详细信息请参阅 Population（Mongoose 英文文档）。

### 一模式（模型）一文件

虽然创建模式和模型没有文件结构的限制，但强烈建议将单一模式定义在单一模块（文件）中，并通过导出方法来创建模型。如下所示：

```js
// 文件：./models/somemodel.js

// Require Mongoose
const mongoose = require("mongoose");

// 定义一个模式
const Schema = mongoose.Schema;

const SomeModelSchema = new Schema({
  a\_string: String,
  a\_date: Date,
});

// 导出函数来创建 "SomeModel" 模型类
module.exports = mongoose.model("SomeModel", SomeModelSchema);
```

然后就可以在其他文件中，`require` 并使用该模型。下面是通过 `SomeModel` 模块来获取所有实例的方法。

```js
// 通过 require 模块来创建 SomeModel 模型
const SomeModel = require("../models/somemodel");

// 使用 SomeModel 对象（模型）来查找所有的 SomeModel 记录
SomeModel.find(callback_function);
```

架设 MongoDB 数据库
--------------

我们已经初步了解了 Mongoose 以及设计模型的方法，现在该开始搭建 LocalLibrary 网站了。第一件事就是设置 MongoDB 数据库，来存储图书馆的数据。

本教程将使用 mLab 免费版“沙盒”云数据库。这一版不适用于生产环境，因为它没有冗余设计，但非常适合进行开发和原型设计。选用它是因为它免费且易于设置，并且 mLab 是一家流行的数据库服务供应商，也是生产环境数据库的理想选择（撰写本文时（2019 年 1 月），国内流行的云数据库解决方案有 阿里云、腾讯云、百度云 等）。

**备注：** 也可以下载并安装 对应系统的安装包，设置本地版 MongoDB 数据库。多数指令和使用云数据库是一样的，除了连接时数据库的 URL。

**备注：** 目前 mLab 网站在国内速度很慢，若遇到无法正常注册或登录的情况可以考虑本地版 MongoDB。

首先 用 mLab 创建一个账户（这是免费的，只需要输入基本联系信息，并同意服务条款）。

登录后将进入 mLab 主屏幕：

1. 单击 *MongoDB Deployments*（MongoDB 部署）部分中的 **Create New（新建）**。
 ![](/zh-CN/docs/Learn/Server-side/Express_Nodejs/mongoose/mlabcreatenewdeployment.png)
2. 将打开 Cloud Provider（云服务提供商）选择屏幕。

 ![MLab - screen for new deployment](/zh-CN/docs/Learn/Server-side/Express_Nodejs/mongoose/mlab_new_deployment_form_v2.png)
    * 在 Plan Type（方案类型）部分中，选择 SANDBOX（Free）免费沙箱方案。
    * 从 *Cloud Provider*（云服务提供商）部分选择任意提供商。不同地区适用不同提供商（显示在选定的计划类型下面）。
    * 点击 \*\*Continue（继续）\*\*按钮。
3. 此时将打开 *Select Region*（选择区域）屏幕。

 ![Select new region screen](/zh-CN/docs/Learn/Server-side/Express_Nodejs/mongoose/mlab_new_deployment_select_region_v2.png)
    * 选择离你最近的地区，然后 **Continue**。
4. 将打开 Final Details（最后的细节）屏幕。

 ![New deployment database name](/zh-CN/docs/Learn/Server-side/Express_Nodejs/mongoose/mlab_new_deployment_final_details.png)
    * 输入新数据库的名称 `local_library`，然后 **Continue**。
5. 将打开 *Order Confirmation*（订单确认）屏幕。

 ![Order confirmation screen](/zh-CN/docs/Learn/Server-side/Express_Nodejs/mongoose/mlab_new_deployment_order_confirmation.png)
    * 点击 \*\*Submit Order（提交订单）\*\*以创建数据库。
6. 将返回到主屏幕。点击刚创建的新数据库可以打开详细信息屏幕。当前数据库还没有任何数据。

 ![mLab - Database details screen](/zh-CN/docs/Learn/Server-side/Express_Nodejs/mongoose/mlab_new_deployment_database_details.png)
 表单显示了访问数据库的 URL（上图的红框）。此时可以创建一个用户，并在 URL 中指定用户名，就可以访问这个 URL 了。
7. 点击 **Users** 选项卡，点击 **Add database user** 按钮。
8. 输入用户名和密码（两次），然后按 **Create**。不要选择 Make *read-only*。

 ![](/zh-CN/docs/Learn/Server-side/Express_Nodejs/mongoose/mlab_database_users.png)

现在数据库已经创建好了，并且有一个可访问的 URL（带有用户名和密码）：`mongodb://<dbuser>:<dbpassword>@ds019038.mlab.com:19038/local_library`

安装 Mongoose
-----------

打开终端，并转到 LocalLibrary 站点骨架 的目录。通过以下命令安装 Mongoose（及其依赖项），并将其添加至 **package.json** 文件，若你在阅读 **Mongoose 入门** 时已经完成这一操作，请忽略本段。

```bash
npm install mongoose
```

连接到 MongoDB
-----------

打开 **/app.js**（位于项目根目录），并将以下代码复制到声明 Express 应用对象的位置（`var app = express();` 之后）。将数据库 URL 字符串（'在此插入数据库\_URL'）替换为真实的 URL（设置自 mLab）。

```js
// 设置 Mongoose 连接
const mongoose = require("mongoose");
const mongoDB = "在此插入数据库\_URL";
mongoose.connect(mongoDB, { useNewUrlParser: true, useUnifiedTopology: true });
mongoose.Promise = global.Promise;
const db = mongoose.connection;
db.on("error", console.error.bind(console, "MongoDB 连接错误："));
```

如上文 Mongoose 入门 所讲，以上代码创建了与数据库的默认连接，并绑定了错误事件（错误信息将及时打印到控制台）。

定义 LocalLibrary 模式
------------------

如上文所述，我们将为每个模型定义单独的模块。首先在项目根目录中创建一个文件夹用来保存模型（**/models**），然后为每个模型创建单独的文件：
```

/express-locallibrary-tutorial  // 项目根目录
  /models
    author.js
    book.js
    bookinstance.js
    genre.js
```

### 作者模型（Author）

将下方的 `Author` 模式代码复制粘贴至 **./models/author.js** 文件中。模式中定义了两个 `String` 模式类型来表示作者的姓氏和名字（这两个字段是必需的，且长度不能超过 100 字符），定义了两个 `Date` 字段做为作者的生卒日期。

```js
const mongoose = require("mongoose");

const Schema = mongoose.Schema;

const AuthorSchema = new Schema({
  first\_name: { type: String, required: true, max: 100 },
  family\_name: { type: String, required: true, max: 100 },
  date\_of\_birth: { type: Date },
  date\_of\_death: { type: Date },
});

// 虚拟属性'name'：表示作者全名
AuthorSchema.virtual("name").get(function () {
  return this.family_name + ", " + this.first_name;
});

// 虚拟属性'lifespan'：作者寿命
AuthorSchema.virtual("lifespan").get(function () {
  return (
    this.date_of_death.getYear() - this.date_of_birth.getYear()
  ).toString();
});

// 虚拟属性'url'：作者 URL
AuthorSchema.virtual("url").get(function () {
  return "/catalog/author/" + this._id;
});

// 导出 Author 模型
module.exports = mongoose.model("Author", AuthorSchema);
```

我们还为 `AuthorSchema` 声明了一个 "`url`" 虚拟属性，以返回模型特定实例的绝对 URL。在模板中需要获取特定作者的链接时可使用该属性。

**备注：** 有必要将 URL 声明为虚拟属性，因为这样，项目的 URL 就只需在一处进行更改。
 此时，使用此 URL 的链接还不能工作，因为目前还没有设置任何路由，无法处理特定模型实例的代码。这个问题下节再讲。

模块的最后对模型进行导出。

### 藏书模型（Book）

将下方的 `Book` 模式代码复制粘贴至 **./models/book.js** 文件中。大体结构与作者模型相似，有三个字符串字段，一个用于获取特定藏书记录 URL 的虚拟属性，代码最后对模型进行导出。

```js
const mongoose = require("mongoose");

const Schema = mongoose.Schema;

const BookSchema = new Schema({
  title: { type: String, required: true },
  author: { type: Schema.Types.ObjectId, ref: "Author", required: true },
  summary: { type: String, required: true },
  isbn: { type: String, required: true },
  genre: [{ type: Schema.Types.ObjectId, ref: "Genre" }],
});

// 虚拟属性'url'：藏书 URL
BookSchema.virtual("url").get(function () {
  return "/catalog/book/" + this._id;
});

// 导出 Book 模块
module.exports = mongoose.model("Book", BookSchema);
```

主要区别在于：此处有两个字段是对其他模型的引用（黑体字所示）：

* `author` 是对单一 `Author` 模型对象的引用，并且是必需的。
* `genre` 是对 `Genre` 模型（目前尚未声明）对象数组的引用。
### 藏书副本模型（BookInstance）

最后将 `BookInstance` 模式代码复制粘贴至 **./models/bookinstance.js** 文件中。 `BookInstance` 表示可供借阅的藏书的特定副本，其中包含该副本是否可用、还书期限，“出版批次”或版本详细信息。

```js
const mongoose = require("mongoose");

const Schema = mongoose.Schema;

const BookInstanceSchema = new Schema({
  // 指向相关藏书的引用
  book: { type: Schema.Types.ObjectId, ref: "Book", required: true },
  // 出版项
  imprint: { type: String, required: true },
  status: {
    type: String,
    required: true,
    enum: ["Available", "Maintenance", "Loaned", "Reserved"],
    default: "Maintenance",
  },
  due\_back: { type: Date, default: Date.now },
});

// 虚拟属性'url'：藏书副本 URL
BookInstanceSchema.virtual("url").get(function () {
  return "/catalog/bookinstance/" + this._id;
});

// 导出 BookInstance 模型
module.exports = mongoose.model("BookInstance", BookInstanceSchema);
```

以上代码有点儿新东西，即字段选项（黑体字）：

* `enum`：可以设置字符串允许的值。本例中可指定书籍的状态。（使用枚举可以避免状态中出现错误拼写或不允许的值）
* `default`：用默认值可以设定新 建藏书实例的默认状态（为'馆藏维护'），还可以将默认还书期限（`due_back`）日期设置为今天（`now`）。（设置日期时请注意 `Date` 函数的用法！）

其他内容和之前的模式大同小异。

### 图书种类模型（Genre）——挑战自我！

打开 **./models/genre.js** 文件，并创建一个模式来存储 `Genre`（书本的类别，例如它是小说类还是纪实类，是爱情题材还是军事史题材，等）。

与之前模型的定义方式相似：

* 该模型应该有一个 `String` 模式类型，命名为 `name` ，用来描述图书种类。
* `name` 字段应该是必需的，并且有 3 到 100 个字符。
* 声明一个 虚拟属性，命名为 `url`，返回图书类型 URL。
* 导出模型。
测试——添加项目
--------

好了，现在所有模型已准备完毕。

为了测试这些模型（并添加一些示例藏书和项目，以便后续使用），我们来运行一个单独的脚本来为每种类型创建一些项目：

1. 下载（或新建）文件 populatedb.js，保存在 express-locallibrary-tutorial 目录（`package.json` 所在位置） 。

**备注：** 无需深究 populatedb.js，它只是为数据库添加一些示例数据。

译注：针对 node.js3.0 及以后版本，mlab 使用“mongodb+srv://”链接而非“mongodb://”，请对populatedb.js源码酌情修改，否则会报错而添加数据失败。
2. 在项目根目录运行以下命令，以安装脚本所需的异步模块（后续教程再展开讲）

```bash
npm install async
```

3. 在命令提示符下用 node 运行此脚本，并以 MongoDB 数据库的 URL 作为参数（同 `app.js` 中替换 `insert_your_database_url_here` 占位符的 URL）：

```bash
node populatedb <mongodb url>
```

4. 该脚本应一路运行至完成，并在终端中记录所创建的项目。

**备注：** 打开 mLab 数据库主页面，现在藏书、作者、种类和藏书副本的集合应该都可以打开了，也可以查看单个文档。

小结
--

本节介绍了数据库和 ORM（Node/Express 环境）的一些知识，以及定义 Mongoose 模式与模型的方法。随后为 LocalLibrary 网站设计并实现了 `Book`、`BookInstance`、`Author`、`Genre` 模型。

本文最后（使用独立运行的脚本）创建了一些测试实例。下一节将关注如何创建页面以显示这些对象。

参见
--

* 数据库集成 (Express 文档)
* Mongoose 站点 (Mongoose 文档)
* Mongoose 指南 (Mongoose 文档)
* 验证 (Mongoose 文档)
* 模式类型 (Mongoose 文档)
* 模型 (Mongoose 文档)
* 查询 (Mongoose 文档)
* 填充 (Mongoose 文档)
* 上一页
* Overview: Express Nodejs
* 下一页


Express/Node 入门 - 学习 Web 开发

Express/Node 入门
===============

* Overview: Express Nodejs
* 下一页

本节将回答“什么是 Node”以及“什么是 Express”这两个问题，并通过主要特征和构成要件来简要介绍 Express 框架的与众不同之处。（只是目前尚不能用一个开发环境来测试它）

|  |  |
| --- | --- |
| 前提： |
 掌握计算机基础知识。了解
 服务器端编程，特别是
 客户 - 服务器交互机制。
  |
| 学习目标： | 熟悉 Express，以及它适配 Node 的方式、具体功能和构成要件。 |

什么是 Node?
---------

Node（正式名称 Node.js）是一个开源的、跨平台的运行时环境，有了它，开发人员可以使用 JavaScript 创建各种服务器端工具和应用程序。此运行时主要用于浏览器上下文之外（即可以直接运行于计算机或服务器操作系统上）。据此，该环境省略了一些浏览器专用的 JavaScript API，同时添加了对更传统的 OS API（比如 HTTP 库和文件系统库）的支持。

从 web 服务器开发的角度来看，Node 有很多好处：

* 卓越的性能表现！Node 为优化 web 应用的吞吐量和扩展度而生，对常见的 web 开发问题是一套绝佳方案（比如实时 web 应用）。
* 代码还是熟悉的老伙伴 JavaScript，这意味着在客户端和服务器端“上下文切换”的时间成本更低。
* 与传统的 web 服务器语言（例如 Python、PHP 等）相比，JavaScript 理念更新，语言设计的改进带来了诸多好处。许多其他新近流行的语言也可编译/转换成 JavaScript，所以 TypeScript、CoffeeScript、ClojureScript、Scala、LiveScript 等等也可以使用。
* Node 包管理工具（node package manager，NPM）提供了数十万个可重用的工具包。它还提供了一流的依赖解决方案，可实现自动化工具链构建。
* Node.js 是可移植的，可运行于 Microsoft Windows、macOS、Linux、Solaris、FreeBSD、OpenBSD、WebOS 和 NonStop OS。此外，许多 web 主机供应商对其提供了良好支持（包括专用的基础框架和构建 Node 站点的文档）。
* 它有一个非常活跃的第三方生态系统和开发者社区，很多人愿意提供帮助。

可以用 Node.js 的 HTTP 包来创建一个简单的 web 服务器。

### Hello Node.js

以下示例将创建一个 web 服务器，它将监听对 URL `http://127.0.0.1:8000/` 所有种类的 HTTP 请求，当接收到一个请求时，脚本将做出响应：返回一个字符串“Hello World”。如果已经安装了 Node，可以按照下面的步骤尝试一下：

1. 打开终端（Windows 中打开命令行工具）
2. 创建一个空文件夹用来存放项目，比如 `"test-node"`，然后在终端输入以下命令进入这个文件夹：

```bash
cd test-node
```

3. 用你最喜欢的文本编辑器创建一个名为 `"hello.js"` 的文件，把以下代码粘贴进来。

```js
// 调用 HTTP 模块
const http = require("http");

// 创建 HTTP 服务器并监听 8000 端口的所有请求
http
  .createServer((request, response) => {
    // 用 HTTP 状态码和内容类型来设定 HTTP 响应头
    response.writeHead(200, { "Content-Type": "text/plain" });

    // 发送响应体 "Hello World"
    response.end("Hello World\n");
  })
  .listen(8000);

// 在控制台打印访问服务器的 URL
console.log("服务器运行于 http://127.0.0.1:8000/");
```

4. 将其保存在刚才创建的文件夹。
5. 返回终端并输入以下命令：

```bash
node "hello.js"
```

最后，在浏览器地址栏中输入 `"http://localhost:8000"` 并按回车，可以看到一个大面积空白的网页，左上角有“Hello World" 字样。

Web 框架
------

Node 本身并不支持其他常见的 web 开发任务。如果需要进行一些具体的处理，比如运行其他 HTTP 动词（比如 `GET`、`POST`、`DELETE` 等）、分别处理不同 URL 路径的请求（“路由”）、托管静态文件，或用模板来动态创建响应，那么可能就要自己编写代码了，亦或使用 web 框架，以避免重新发明轮子。

什么是 Express?
------------

Express 是最流行的 Node 框架，是许多其他流行 Node 框架 的底层库。它提供了以下机制：

* 为不同 URL 路径中使用不同 HTTP 动词的请求（路由）编写处理程序。
* 集成了“视图”渲染引擎，以便通过将数据插入模板来生成响应。
* 设置常见 web 应用设置，比如用于连接的端口，以及渲染响应模板的位置。
* 在请求处理管道的任何位置添加额外的请求处理“中间件”。

虽然 Express 本身是极简风格的，但是开发人员通过创建各类兼容的中间件包解决了几乎所有的 web 开发问题。这些库可以实现 cookie、会话、用户登录、URL 参数、`POST` 数据、安全头等功能。可在 Express 中间件 网页中找到由 Express 团队维护的中间件软件包列表（还有一张流行的第三方软件包列表）。

**备注：** 这种灵活性是一把双刃剑。虽然有一些中间件包可以解决几乎所有问题或需求，但是挑选合适的包有时也会成为一个挑战。这里构建应用没有“不二法门”，Internet 上许多示例也不是最优的，或者只展示了开发 web 应用所需工作的冰山一角。

Node 和 Express 从哪儿来？
--------------------

Node 发布于 2009 年，最初版本仅支持 Linux。NPM 包管理器发布于 2010 年，并于 2012 年支持 Windows。目前（2019 年 1 月）的 LTS 版本是 Node 10.15.0，最新版本是 Node 11.8.0。这只是沧海一粟，更多历史信息请到 维基百科 探究。

Express 发布于 2010 年 11 月，目前 API 的版本为 4.16.4。可以查看 修改记录 来查看当前版本的更新信息，或者访问 GitHub 页面来查看更详细的历史发布记录。

Node 和 Express 有多流行？
--------------------

一个 web 框架是否流行是至关重要的，因为这预示着它是否会得到持续维护，是否会有更丰富的文档、插件库和技术支持。

服务器端框架的流行程度不容易量化（尽管有 Hot Frameworks 这样的网站试图通过计算 GitHub 项目和 StackOverflow 问题的数量等机制来评估框架的流行程度)。可以换个角度思考：Node 和 Express 是否“足够流行”、能够避免冷门平台带来的问题？它们还在持续更新吗？遇到问题时能得到帮助吗？学 Express 能挣钱吗？

基于使用 Express 的 知名企业 的数量、维护代码库的人数、以及提供免费或付费支持的人数来说，Express 是一个流行的框架！

Express 是固执的吗？
--------------

Web 框架通常自称“固执的（opinionated）”或“包容的（unopinionated）”。

固执的框架认为应该有一套“标准答案”来解决各类具体任务。通常支持特定领域的快速开发（解决特定类型的问题）。因为标准答案通常易于理解且文档丰富。然而在解决主领域之外的问题时，就会显得不那么灵活，可用的组件和方法也更少。

相比之下，那些包容的框架，对于用于实现目标的组件组合的最佳方式限制要少得多，甚至不怎么限定组件的选择。这使开发人员更容易使用最合适的工具来完成特定的任务，但是要付出亲自寻找组件的成本。

Express 是高度包容的。几乎可以将任何兼容的中间件以任意顺序插入到请求处理链中，只要你喜欢。可以用单一文件或多个文件构造应用，怎样的目录结构都可以。有时候你自己都会觉得眼花缭乱！

Express 代码是什么样子的？
-----------------

传统的数据驱动型网站中，web 应用是用于等待来自浏览器（或其他客户端）的 HTTP 请求的。当 web 应用收到一个请求时，会根据 URL 的模式，以及 `POST` 数据和 `GET` 数据可能包含的信息，来解析请求所需的功能。根据请求的内容，web 应用可能会从数据库读或写一些信息，等等操作来满足请求。随后，web 应用会返回给浏览器一个响应，通常是动态生成一页 HTML，在页面中用所取得的信息填充占位符。

使用 Express 可以调用特定 HTTP 动词（`GET`, `POST`, `SET`等）函数和 URL 模式（“路由”）函数，还可以指定模板（“视图”）引擎的种类、模板文件的位置以及渲染响应所使用的模板。可以使用 Express 中间件来添加对 cookie、会话、用户、获取 `POST`/`GET` 参数，等。可以使用 Node 支持的任何类型数据库（Express 本身没有定义任何数据库行为）。

下文将介绍 Express 和 Node 的一些常见知识点。

### Helloworld Express

首先来看 Express 的 Hello World 的示例（下文将逐行讨论）。

**备注：** 如果你已经安装了 Node 和 Express（或者你已经按照 下一节 中的说明安装好了），可以将此代码保存为 **app.js**，并通过在 bash 中这样运行它：

```bash
node ./app.js
```

```js
const express = require("express");
const app = express();

app.get("/", (req, res) => {
  res.send("Hello World!");
});

app.listen(3000, () => {
  console.log("示例应用正在监听 3000 端口 !");
});
```

前两行通过 `require()` 导入 Express 模块，并创建了一个 Express 应用。传统上把这个对象命名为 `app`，它可以进行路由 HTTP 请求、配置中间件、渲染 HTML 视图、注册模板引擎以及修改 应用程序设置 等操作，从而控制应用的行为（例如，环境模式，路由定义是否为区分大小写等）。

代码的中间部分（从 `app.get()` 开始共三行）是**路由定义**。`app.get()` 方法指定了一个回调（callback）函数，该函数在每监听到一个关于站点根目录路径（`'/'`）的 HTTP `GET` 请求时调用。此回调函数以一个请求和一个响应对象作为参数，并直接调用响应的 `send()` 来返回字符串“Hello World!”

最后一个代码块在“3000”端口上启动服务器，并在控制台打印日志。服务器运行时，可用浏览器访问 `localhost:3000`，看看响应返回了什么。

### 导入和创建模块

模块是 JavaScript 库或文件，可以用 Node 的 `require()` 函数将它们导入其他代码。Express 本身就是一个模块，Express 应用中使用的中间件和数据库也是。

下面的代码以 Express 框架为例展示了如何通过名字来导入模块。首先，调用 `require()` 函数，用字符串（`'express'`）指定模块的名字，然后调用返回的对象来创建 Express 应用。然后就可以访问应用对象的属性和函数了。

```js
const express = require("express");
const app = express();
```

还可以创建自定义模块，并用相同的方法导入。

**备注：** 你一定会有自建模块的**需求**，因为这可以让代码管理更有序。单文件应用是很难理解和维护的。使用模块还有助于管理名字空间，因为在使用模块时只会导入模块中显式导出的变量。

为了让对象暴露于模块之外，只需把它们设置为 `exports` 对象的附加属性即可。例如，下面的 **square.js** 模块就是一个导出了 `area()` 和 `perimeter()` 方法的文件：

```js
exports.area = (width) => {
  return width \* width;
};
exports.perimeter = (width) => {
  return 4 \* width;
};
```

可以用 `require()` 导入这个模块，然后调用导出的方法，用法如下：

```js
const square = require("./square");
// 这里 require() 了文件名，省略了 .js 扩展名（可选）
console.log("边长为 4 的正方形面积为 " + square.area(4));
```

**备注：** 为模块指定绝对路径（或模块的名字，见最初的示例）也是可行的。

一次赋值不仅能构建一个单一的属性，还能构建一个完整的对象，可以像下面这样把对象赋值给 `module.exports`（也可以让 `exports` 对象直接作为一个构造器或另一个函数）：

```js
module.exports = {
  area: (width) => {
    return width \* width;
  },
  perimeter: (width) => {
    return 4 \* width;
  },
};
```

**备注：** 在一个既定的模块内，可以把 `exports` 想象成 `module.exports` 的 快捷方式。`exports` 本质上就是在模块初始化前为 `module.exports` 的值进行初始化的一个变量。这个值是对一个对象（这里是空对象）的引用。这意味着 `exports` 与 `module.exports` 引用了同一个对象，也意味着如果为 `exports` 赋其他值不会影响到 `module.exports`。

更多信息请参阅 模块（Node API 文档）。

### 使用异步 API

JavaScript 代码在完成那些需要一段时间才能完成的操作时，经常会用异步 API 来取代同步 API。同步 API 下，每个操作完成后才可以进行下一个操作。例如，下列日志函数是同步的，将按顺序将文本打印到控制台（第一、第二）。

```js
console.log("第一");
console.log("第二");
```

而异步 API 下，一个操作开始后（在其完成之前）会立即返回。一旦操作完成，API 将使用某种机制来执行附加操作。例如，下面的代码将打印“第二、第一”。这是因为虽然先调用了 `setTimeout()` 方法并立即返回，但它的操作到 3 秒后才完成。

```js
setTimeout(() => {
  console.log("第一");
}, 3000);
console.log("第二");
```

在 Node 中使用无阻塞异步 API 甚至比在浏览器中更为重要，这是因为 Node 是一个单线程事件驱动的执行环境。“单线程”意味着对服务器的所有请求运行在同一个线程上，而不是分布在不同的进程上。这个模式在速度和管理服务器资源方面效率很高，但也意味着如果以同步方式调用的函数占用了很长时间，不仅会阻塞当前请求，还会阻塞当前 web 应用其他所有请求。

有多种方法可以让一个异步 API 通知当前应用它已执行完毕。最常用的是在调用异步 API 时注册一个回调函数，在 API 操作结束后将“回调”之。这也是上面的代码所使用的方法。

**备注：** 如果有一系列独立的异步操作必须按顺序执行，那么使用回调可能会非常“混乱”，因为这会导致多级嵌套回调。人们通常把这个问题叫做“回调地狱”。缓解这个问题有以下办法：良好的编码实践（参考 http://callbackhell.com/）、使用 async 等模块、迁移至 ES6 并使用 Promise 等特性。

**备注：** Node 和 Express 有一个一般性约定，即：使用“错误优先”回调。这个约定要求回调函数的第一个参数是错误值，而后续的参数包含成功数据。以下博文很好的解释了这个方法的有效性：以 Node.js 之名：理解错误优先回调（fredkschott.com 英文文章）

### 创建路由处理器（Route handler）

上文的 Hello World 示例中定义了一个（回调）路由处理函数来处理对站点根目录（`'/'`）的 HTTP `GET` 请求。

```js
app.get("/", (req, res) => {
  res.send("Hello World!");
});
```

回调函数将请求和响应对象作为参数。该函数直接调用响应的 `send()` 以返回字符串“Hello World!”。有 许多其他响应方法 可以结束请求/响应周期，例如，通过调用 `res.json()` 来发送 JSON 响应、调用 `res.sendFile()` 来发送文件。

**备注：** 虽然回调函数的参数命名没有限制，但是当调用回调时，第一个参数将始终是请求，第二个参数将始终是响应。合理的命名它们，在回调体中使用的对象将更容易识别。

**Express 应用**对象还提供了为其他所有 HTTP 动词定义路由处理器的方法，大多数处理器的使用方式完全一致：

`checkout()`, `copy()`, **`delete()`**, **`get()`**, `head()`, `lock()`, `merge()`, `mkactivity()`, `mkcol()`, `move()`, `m-search()`, `notify()`, `options()`, `patch()`, **`post()`**, `purge()`, **`put()`**, `report()`, `search()`, `subscribe()`, `trace()`, `unlock()`, `unsubscribe()`.

有一个特殊的路由方法 `app.all()`，它可以在响应任意 HTTP 方法时调用。用于在特定路径上为所有请求方法加载中间件函数。以下示例（来自 Express 文档）中的处理程序将在监听到针对 `/secret` 的任意 HTTP 动词（只要 HTTP 模块 支持）的请求后执行。

```js
app.all("/secret", (req, res, next) => {
  console.log("访问私有文件 ...");
  next(); // 控制权传递给下一个处理器
});
```

路由器可以匹配 URL 中特定的字符串模式，并从 URL 中提取一些值作为参数传递给路由处理程序（作为请求对象的属性）。

可以为站点的特定部分提供一组路由处理器（使用公共路由前缀进行组合）。（比如对于一个有 维基（Wiki）内容的站点，可以把所有 Wiki 相关的路由放在同一个文件里，使用路由前缀 \*`'/wiki/'` \*访问它们）。在 Express 中可以使用 express.Router 对象实现。例如，可以把所有维基相关的路由都放在一个 **wiki.js** 模块中，然后导出 `Router` 对象，如下：

```js
// wiki.js - 维基路由模块

const express = require("express");
const router = express.Router();

// 首页路由
router.get("/", (req, res) => {
  res.send("维基首页");
});

// “关于”页面路由
router.get("/about", (req, res) => {
  res.send("关于此维基");
});

module.exports = router;
```

**备注：** 向 `Router` 对象添加路由就像向之前为 `app` 对象添加路由一样。

首先 `require()` 路由模块（**wiki.js**），然后在 Express 应用中调用 `use()` 把 `Router` 添加到中间件处理路径中，就可以在主应用中使用这个模块中的路由处理器了。路由路径有两条：`/wiki` 和 `/wiki/about/`。

```js
const wiki = require("./wiki.js");
// ...
app.use("/wiki", wiki);
```

今后将介绍更多关于路由的信息，特别是关于 `Router` 的用法，请参见 路由和控制器 一节。

### 使用中间件（Middleware）

中间件在 Express 应用中得到了广泛使用，从提供错误处理静态文件、到压缩 HTTP 响应等等。路由函数可以通过向 HTTP 客户端返回一些响应来结束 HTTP“请求 - 响应”周期，而中间件函数*通常是*对请求或响应执行某些操作，然后调用“栈”里的下一个函数，可能是其他中间件或路由处理器。中间件的调用顺序由应用开发者决定。

**备注：** 中间件可以执行任何操作，运行任何代码，更改请求和响应对象，也可以**结束“请求 - 响应”周期**。如果它没有结束循环，则必须调用 `next()` 将控制传递给下一个中间件函数（否则请求将成为悬挂请求）。

大多数应用会使用**第三方**中间件来简化常见的 web 开发任务，比如 cookie、会话、用户身份验证、访问请求 `POST` 和 JSON 数据，日志记录等。参见 Express 团队维护的中间件包列表（包含受欢迎的第三方包）。NPM 有提供其他 Express 包。

要使用第三方中间件，首先需要使用 NPM 将其安装到当前应用中。比如，要安装 morgan HTTP 请求记录器中间件，可以这样做：

```bash
npm install morgan
```

然后，你可以对 *Express 应用对象*调用 `use()` 将该中间件添加到栈：

```js
const express = require('express');
const logger = require('morgan');
const app = express();
app.use(logger('dev'));
...
```

**备注：** 中间件和路由函数是按声明顺序调用的。一些中间件的引入顺序很重要（例如，如果会话中间件依赖于 cookie 中间件，则必须先添加 cookie 处理器）。绝大多数情况下要先调用中间件后设置路由，否则路由处理器将无法访问中间件的功能。

可以自己编写中间件函数，这是基本技能（仅仅为了创建错误处理代码也需要）。中间件函数和路由处理回调之间的**唯一**区别是：中间件函数有第三个参数 `next`，在中间件不会结束请求周期时应调用这个 `next`（它包含中间件函数调用后应调用的**下一个**函数）。

可以使用 `app.use()` 或 `app.add()` 将一个中间件函数添加至处理链中，这取决于中间件是应用于所有响应的，还是应用于特定 HTTP 动词（`GET`，`POST`等）响应的。可以为两种情况指定相同的路由，但在调用 `app.use()` 时路由可以省略。

下面的示例显示了如何使用这两种方法添加中间件功能，以及是否使用路由。

```js
const express = require("express");
const app = express();

// 示例中间件函数
const a\_middleware\_function = (req, res, next) => {
  // ... 进行一些操作
  next(); // 调用 next() ，Express 将调用处理链中下一个中间件函数。
};

// 用 use() 为所有的路由和动词添加该函数
app.use(a_middleware_function);

// 用 use() 为一个特定的路由添加该函数
app.use("/someroute", a_middleware_function);

// 为一个特定的 HTTP 动词和路由添加该函数
app.get("/", a_middleware_function);

app.listen(3000);
```

**备注：** 上面代码中单独声明了中间件函数，并把它设置为回调。之前是把路由处理函数在使用时声明为回调。在 JavaScript 中，两种方法都可行。

请参阅 Express 文档中关于 使用 和 开发 Express 中间件的内容。

### 托管静态文件

可以使用 express.static 中间件来托管静态文件，包括图片、CSS 以及 JavaScript 文件（其实 `static()` 是 Express 提供的**原生**中间件函数之一）。例如，可以通过下面一行来托管 'public' 文件夹（应位于 Node 调用的同一级）中的文件：

```js
app.use(express.static("public"));
```

现在 'public' 文件夹下的所有文件均可通过在根 URL 后直接添加文件名来访问了，比如：
```

http://localhost:3000/images/dog.jpg
http://localhost:3000/css/style.css
http://localhost:3000/js/app.js
http://localhost:3000/about.html
```

可以通过多次调用 `static()` 来托管多个文件夹。如果一个中间件函数找不到某个文件，将直接传递给下一个中间件（中间件的调用顺序取决于声明顺序）。

```js
app.use(express.static("public"));
app.use(express.static("media"));
```

还可以为静态 URL 创建一个虚拟的前缀，而不是直接把文件添加到根 URL 里。比如，这里 指定了一个装载路径，于是这些文件将通过 '/media' 前缀调用：

```js
app.use("/media", express.static("public"));
```

现在可以通过 '/media' 路径前缀来访问 'public' 文件夹中的文件。
```

http://localhost:3000/media/images/dog.jpg
http://localhost:3000/media/video/cat.mp4
http://localhost:3000/media/cry.mp3
```

更多信息请参阅 Express 文档 托管静态文件。

### 错误处理

用来处理错误的特殊中间件函数有四个参数`(err, req, res, next)`，而不是之前的三个。例如：

```js
app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(500).send("出错了！");
});
```

错误处理中间件可以任何所需内容，但是必须在所有其他 `app.use()` 和路由调用后才能调用，因此它们是需求处理过程中最后的中间件。

Express 内建了错误处理机制，可以协助处理 app 中没有被处理的错误。默认的错误处理中间件函数在中间件函数栈的末尾。如果一个错误传递给 `next()` 而没有用错误处理器来处理它，内建处理机制将启动，栈跟踪的错误将回写给客户端。

**备注：** 生产环境中不保留栈跟踪轨迹。可将环境变量 `NODE_ENV` 设置为 `'production'` 来运行所需的生产环境。

**备注：** HTTP 404 和其他“错误”状态码不作为错误处理。可使用中间件来自行处理这些状态。更多信息请参阅 Express 文档 FAQ。

更多信息请参阅 Express 文档 错误处理。

### 使用数据库

*Express* 应用可以使用 Node 支持的所有数据库（Express 本身并没有定义任何数据库管理的附加行为或需求）。其中包括：PostgreSQL、MySQL、Redis、SQLite、MongoDB，等等。

使用数据库前先要用 NPM 来安装驱动程序。比如，要安装流行的 NoSQL 数据库 MongoDB 的驱动程序，可运行以下命令：

```bash
npm install mongodb
```

数据库可以安装在本地或云端。在 Express 代码中 `require()` 驱动程序，连接，然后就可以执行增加、读取、更新、删除四种操作（CRUD）。以下示例展示了如何查找 MongoDB 表中 '哺乳动物' 的记录：

```js
// MongoDB 3.0 以上版本适用，老版本不适用。
const MongoClient = require("mongodb").MongoClient;

MongoClient.connect("mongodb://localhost:27017/animals", (err, client) => {
  if (err) {
    throw err;
  }

  let db = client.db("动物");
  db.collection("哺乳动物")
    .find()
    .toArray((err, result) => {
      if (err) throw err;
      console.log(result);
      client.close();
    });
});
```

还有一种通过“对象关系映射（Object Relational Mapper，简称 ORM）”间接访问数据库的方法。可以把数据定义为“对象”或“模型”，然后由 ORM 根据给定的数据库格式搞定所有映射关系。这种方法对于开发者有一个好处：可以用 JavaScript 对象的思维而无需直接使用数据库语法，同时传进的数据也有现成的检查工具。稍后详细讨论数据库问题。.

更多信息请参阅 Express 文档 数据库集成。

### 渲染数据（视图，view）

模板引擎可为输出文档的结构指定一个模板，在数据处先放置占位符，并于页面生成时填充。模板通常用于生成 HTML，也可以生成其他类型的文档。Express 支持 多个版本的模板引擎，可以参阅：JavaScript 模板引擎对比评测：Jade、Mustache、Dust 与其他。

在应用设置代码中声明了模板引擎的名称和位置后，Express 可以使用 `'views'` 和 `'view engines'` 设置来寻找模板，如下所示（必须事先安装包含模板库的包！）：

```js
const express = require("express");
const app = express();

// 设置包含模板的文件夹（'views'）
app.set("views", path.join(__dirname, "views"));

// 设置视图引擎，比如'some\_template\_engine\_name'
app.set("view engine", "some\_template\_engine\_name");
```

模板的外观取决于所使用的引擎。假设一个模板文件名为 "index.<template\_extension>"，其中包括数据变量 `'title'` 和 `'message'` 的两个占位符，可以在路由处理器函数中调用 `Response.render()` 来创建并发送 HTML 响应：

```js
app.get("/", (req, res) => {
  res.render("index", { title: "关于狗狗", message: "狗狗很牛！" });
});
```

更多信息请参见 Express 文档 使用模板引擎。

### 文件结构

Express 不对文件结构和组件的选用做任何约定。路由、视图、静态文件，以及其他应用具体逻辑均可按任意文件结构保存在任意数量的文件中。当然可以让整个 Express 应用保存在单一文件中，但是一般情况下，把应用按功能（比如账户管理、博客、论坛）和架构问题域（比如 MVC 架构 中的模型、视图、控制器）进行拆分是有意义的。

后文将使用 **Express 应用生成器** 来创建一个模块化的应用框架，从而可以更方便的扩展出新的 web 应用。

小结
--

恭喜，你迈出了 Express/Node 旅程的第一步！你现在已经了解了 Express 与 Node 的主要优势，并大致了解了 Express 应用的结构 (路由处理器、中间件、错误处理和模板代码)。你还了解到 Express 作为一个高度包容的框架，让你在组织应用结构和库时更自由，更开放！

诚然，Express 是一个非常轻量的 web 应用框架，这是有意为之的，它巨大的裨益和无尽的潜能都来自第三方的库和功能。今后的章节会详细讨论。下一节会讲如何设置 Node 开发环境，之后就能开始 Express 的实战了。

参见
--

* Venkat.R - 管理多版本 Node（英文页面）
* 模块 (Node API 中文文档)
* Express 主页 （Express 国内镜像）
* 路由入门（Express 英文文档）
* 路由指南 （Express 英文文档）
* 使用模板引擎 （Express 英文文档）
* 使用中间件（Express 英文文档）
* 开发中间件（Express 英文文档）
* 数据库集成（Express 英文文档）
* 托管静态文件 （Express 中文文档）
* 错误处理 （Express 英文文档）
* Overview: Express Nodejs
* 下一页


Express 教程 2：创建站点框架 - 学习 Web 开发

Express 教程 2：创建站点框架
===================

* 上一页
* Overview: Express Nodejs
* 下一页

本节将演示如何创建一个可添加路由、模板/视图、和数据库调用的“骨架”站点。

|  |  |
| --- | --- |
| 前提： | 配置 Node 开发环境。复习 Express 教程。 |
| 目标： | 掌握用 **Express 应用生成器** 创建站点的方法。 |

概览
--

本节演示了如何使用 Express 应用生成器 创建一个可添加路由、模板/视图和数据库调用的“骨架”网站。这里我们将使用该生成器为 本地图书馆网站 创建框架，以便在以后章节添加其他代码。过程非常简单，只需要在命令行运行“生成器 + 项目名称”即可，此外还可以指定站点的模板引擎和 CSS 生成器。

以下内容介绍了应用生成器的用法，以及视图 / CSS 的一些不同选项。还介绍了骨架站点的组织结构。最后，我们将介绍站点的运行方法，从而对其进行验证。

**备注：** Express 应用生成器并非唯一的 Express 应用生成工具，而且生成项目的结构也不是组织文件和目录的唯一可行方式。但生成项目具有易于扩展和理解的模块化结构。最简单的 Express 应用请参阅 Hello world 示例（Express 镜像站）。

使用应用生成器
-------

**备注：** 本教程中命令操作基于 Linux/macOS 的 bash 终端，Windows 的命令提示符 cmd/PowerShell 与 bash 的概念和用法略有不同，为在 Windows 上获得一致的体验，可以：

* 自己弄懂 cmd/PowerShell 与 bash 的区别。
* 使用 Git 或 MSYS2 为 Windows 提供的 bash。（推荐）
* 使用 Windows 的 Linux 子系统。（到 Microsoft Store 中搜索“Linux”，安装喜欢的版本（Ubuntu 18.04、openSUSE 42、Debian 等），仅限 Windows 10，使用前需要先安装 WSL）

**备注：** 你可能已经发现国内用 NPM 太慢了！这是由众所周知的不可抗力造成的。可用淘宝提供的 CNPM 代替之，功能和用法基本一致（只是不能上传自己的包）。

你应该已经安装好了生成器，它是 设置 Node 开发环境 的一部分。可以使用 NPM 来安装全局的生成器，如下所示：

```bash
sudo npm install express-generator -g
```

生成器有许多选项，可以使用 `--help`（或 `-h`）命令进行查看：

![express 生成器的帮助信息](/zh-CN/docs/Learn/Server-side/Express_Nodejs/skeleton_website/express-help.png)

大意如下：

```bash
$ express --help

  用法：express [选项] [目录]

  选项：

        --version        打印版本号
    -e, --ejs            添加 ejs 引擎支持
        --pug            添加 pug 引擎支持
        --hbs            添加 handlebars 引擎支持
    -H, --hogan          添加 hogan.js 引擎支持
    -v, --view <engine>  添加 <engine> 视图引擎支持 (ejs|hbs|hjs|jade|pug|twig|vash) (默认为 jade)
    -c, --css <engine>   添加 <engine> 样式表引擎支持 (less|stylus|compass|sass) (默认为纯 css)
        --git            添加 .gitignore
    -f, --force          对非空文件夹强制执行
    -h, --help           打印帮助信息
```

可以直接运行 express 命令，将使用 Jade 视图引擎和纯 CSS 在当前目录中创建项目。（如果指定目录名，则在子目录中创建项目）。

```bash
express
```

还可以使用 `--view` 选择视图（模板）引擎，并且/或者使用 `--css` 选择 CSS 生成引擎。

**备注：** 不推荐用 `--hogan`、`--ejs`、`--hbs` 等参数选用模板引擎。请使用 `--view`（或 `-v`）。

### 我应该用哪个视图引擎？

Express 应用生成器支持多款流行的视图/模板引擎，包括 EJS、Hbs、Pug (Jade)、Twig 和 Vash，缺省选项是 Jade。Express 本身也支持大量其他模板语言，开箱即用。

**备注：** 如果要使用生成器不支持的模板引擎，请参阅 在 Express 中使用模板引擎（Express 文档）和所选视图引擎的文档。

一般来说，你应该选择一个大而全的模板引擎，可以尽快进入生产状态。就像你选择其他组件一样！选用模板引擎需要考虑以下因素：

* 进入生产状态的时间——如果你的团队已经有某个模板语言的经验，那么用它可能更快进入生产状态。否则你应该考虑所选模板引擎的学习曲线。
* 流行度和活跃度——要评估所选引擎的流行程度，以及它是否拥有活跃的社区。在网站的生命周期中遇到问题时，是否能够获得相关支持非常重要。
* 风格——某些模板引擎使用特定标记，来标识插入“普通”HTML 中的内容，而另一些模板引擎使用不同的语法（例如使用缩进和块名称）构造 HTML。
* 性能/渲染时间。
* 功能——你应该考虑所选引擎是否具有以下功能：
    + 布局继承：可以定义基本模板，然后“继承”它的一部分，使不同页面可以有不同的呈现。这通常比包含大量所需组件，或每次从头开始构建模板更好。
    + “包含”支持：可以通过包含其他模板来构建新模板。
    + 简明的变量和循环控制语法。
    + 能够在模板级别过滤变量值（例如，将变量设置为大写，或格式化日期值）。
    + 能够生成 HTML 以外的输出格式（例如 JSON 或 XML）。
    + 支持异步操作和流。
    + 可以在同时在客户端和服务器上使用。如果一款模板引擎可以在客户端使用，那么就使在客户端托管数据并完成所有（或大多数）渲染成为可能。

**备注：** 互联网上有许多资源，可帮助你选择合适的视图/模板引擎。

本项目选用 Pug 模板引擎（Jade 是它不久前的曾用名），它是最流行的 Express / JavaScript 模板语言之一，且对 Express 生成器 开箱即用。

### 我应该用哪个 CSS 引擎？

Express 应用生成器支持最常见的 CSS 引擎：LESS, SASS, Compass, Stylus。

**备注：** CSS 的一些限制导致某些任务完成起来非常困难。CSS 引擎提供了更强大的语法来定义 CSS，然后将定义编译为纯 CSS 供浏览器使用。

与模板引擎一样，你也应该使用样式表引擎，这可以最大化团队生产力。本项目将使用原始 CSS（默认的），因为我们对 CSS 要求不复杂，没有必要使用引擎。

### 我应该用哪个数据库？

生成器生成的代码不使用、也不包含任何数据库。Express 应用可以使用 Node 支持的所有 数据库（Express 本身不提供数据库管理机制）。

我们后续讨论数据库集成问题。

创建项目
----

我们为本地图书馆应用创建一个名为 express-locallibrary-tutorial 的项目，使用 Pug 模板库，不使用 CSS 引擎。

首先，进入准备放置项目的目录，然后在命令提示符运行 Express 应用生成器，生成器将创建（并列出）项目的文件：

![用 express 生成器生成一个应用](/zh-CN/docs/Learn/Server-side/Express_Nodejs/skeleton_website/express.png)

生成器在最后还告诉你如何安装（**package.json** 中所列的）依赖，以及如何运行该应用。

运行骨架网站
------

现在我们已经拥有一个完整的项目骨架。虽然这个网站现在还做不了什么，但运行一下，展示一下工作原理也是值得的。

1. 首先，安装依赖项（`install` 命令将获取项目的 **package.json** 文件中列出的所有依赖项包）。

```bash
npm install
```

2. 然后运行该应用。

```bash
DEBUG=express-locallibrary-tutorial:* npm start
```

3. 最后在浏览器中导航至 http://localhost:3000/ ，就可以访问该应用。你应该可以看到：
 ![Express 应用生成器生成的应用启动成功](/zh-CN/docs/Learn/Server-side/Express_Nodejs/skeleton_website/success.png)

一个 Express 应用就配置成功了，它托管于 localhost:3000。

**备注：** 指定 DEBUG 变量可启用控制台日志记录/调试。例如，当你访问上面的页面时，你会看到像这样的调试输出：
 ![用 npm start 启动这个应用](/zh-CN/docs/Learn/Server-side/Express_Nodejs/skeleton_website/npm-start.png) 直接通过 `npm start` 命令启动应用也可以，但不会看到调试信息。

文件改动时重启服务器
----------

只有重启服务器才能看到 Express 网站所做的改动。每次改动后手动启停服务器实在太烦人了，有必要花点时间让这项工作自动化。

nodemon 是最简便的自动化工具之一。通常将其全局安装（因为它是一个“工具”）：

```bash
sudo npm install -g nodemon
```

这里还可以把它作为开发依赖将安装在本地，于是使用这个项目的开发人员只要安装这个应用就能自动获得。通过以下命令将其安装在骨架项目的根目录：

```bash
npm install --save-dev nodemon
```

项目的 **package.json** 文件将自动添加一个新的属性：

```json
  "devDependencies": {
    "nodemon": "^1.18.9"
  }
```

如果没有全局安装该工具，就无法从命令行启动它（除非我们将其添加到路径中），但是可以在 NPM 脚本中调用它，因为 NPM 掌握所有已安装包的信息。找到 package.json 的 `scripts` 部分。在 `"start"` 一行的末尾添加逗号，并在新的一行中添加 `"devstart"`，如下所示：

```json
  "scripts": {
    "start": "node ./bin/www",
    "devstart": "nodemon ./bin/www"
  },
```

现在可以用新建的 `devstart` 命令启动服务器：

```bash
DEBUG=express-locallibrary-tutorial:* npm run devstart
```

现在，如果编辑项目中的任何文件，服务器将自动重启（或者可以随时使用 rs 命令来重启）。查看更新后的页面需要点击浏览器的“刷新”按钮。

**备注：** 这里必须使用“`npm run <scriptname>`”命令，而不是 `npm start`，因为“start”本质上是映射到脚本的一条 NPM 命令。我们可以在 `start` 脚本中替换它，但我们只想在开发期间使用 nodemon，因此有必要创建一条新的脚本命令。

生成的项目
-----

我们来看看刚刚创建的项目。

### 目录结构

安装好依赖项的生成项目具有如下文件结构（**不**带“/”前缀的是文件）：
```

/express-locallibrary-tutorial
    app.js
    /bin
        www
    package.json
    /node_modules
        [约 4,500 个子文件夹和文件]
    /public
        /images
        /javascripts
        /stylesheets
            style.css
    /routes
        index.js
        users.js
    /views
        error.pug
        index.pug
        layout.pug
```

**package.json** 文件定义依赖项和其他信息，以及一个调用应用入口（**/bin/www**，一个 JavaScript 文件）的启动脚本，脚本中还设置了一些应用的错误处理，加载 **app.js** 来完成其余工作。**/routes** 目录中用不同模块保存应用路由。/**views** 目录保存模板。

下面来详细介绍这些文件。

### package.json

**package.json** 文件中定义了应用依赖和其他信息：

```json
{
  "name": "express-locallibrary-tutorial",
  "version": "0.0.0",
  "private": true,
  "scripts": {
    "start": "node ./bin/www"
    "devstart": "nodemon ./bin/www"
  },
  "dependencies": {
    "cookie-parser": "~1.4.3",
    "debug": "~2.6.9",
    "express": "~4.16.0",
    "http-errors": "~1.6.2",
    "morgan": "~1.9.0",
    "pug": "2.0.0-beta11"
  },
  "devDependencies": {
    "nodemon": "^1.18.9"
  }
}
```

依赖包括 *express* 包，和选用的视图引擎包（*pug*）。还有以下一些实用的包：

* cookie-parser：用于解析 cookie 头来填充 `req.cookies`（提供了访问 cookie 信息的便捷方法）。
* debug：一个小型 node 调试程序，仿照 node 核心的调试技术建立。
* http-errors：处理错误中间件。
* morgan：node 专用 HTTP 请求记录器中间件。

"`scripts`" 部分，定义了一个 "`start`" 脚本，当运行 `npm start` 时会调用它来启动服务器。在脚本定义中可以看到 `start` 实际上运行了 "node **./bin/www"**。还有一个 "`devstart`" 脚本，可以通过运行 `npm run devstart` 来运行 "nodemon **./bin/www**"。

```json
  "scripts": {
    "start": "node ./bin/www",
    "devstart": "nodemon ./bin/www"
  },
```

### www 文件

文件 **/bin/www** 是应用入口！它做的第一件事是 `require()` “真实”的应用入口（即项目根目录中的 **app.js** ），**app.js** 会设置并返回 `express()`应用对象。

```js
#!/usr/bin/env node

/\*\*
 \* Module dependencies.
 \* 模块依赖项。
 \*/

var app = require("../app");
```

**备注：** `require()` 是一个全局的 node 函数，可将模块导入当前文件。这里使用相对路径指定 **app.js** 模块，并省略了 .**js** 扩展名（可选）。

文件的其余部分先为 `app` 设置端口（环境变量中的预定义值或默认值 3000），再创建一个 HTTP 服务器，然后开始监听请求，报告服务器错误和连接信息。其他内容可暂时忽略（这里所有内容都是机器生成的模板），但感兴趣的话可以随时回来看看。

### app.js

此文件创建一个 `express` 应用对象（依照惯例命名为 `app`），通过各种设置选项和中间件来设置这个应用，然后从该模块中导出。以下代码只展示了文件中创建和导出应用对象的部分：

```js
var express = require('express');
var app = express();
...
module.exports = app;
```

上文的 **www** 入口文件中 `require()` 的 `app` 就是这里导出的。

我们来详细了解一下 **app.js** 文件。首先，它使用 `require()` 导入了一些实用 node 库，其中包括之前用 NPM 下载的 *`express`*、*`http-errors`*、*`morgan`* 和 *`cookie-parser`*，还有一个 *`path`* 库，它是用于解析文件和目录的核心 node 库。

```js
var express = require("express");
var createError = require("http-errors");
var logger = require("morgan");
var cookieParser = require("cookie-parser");
var path = require("path");
```

然后 `require()` 的是用户路由目录中的模块。这些模块/文件用于处理特定的“路由”（URL 路径）。可以通过添加新文件来扩展骨架应用，比如添加图书相关的路由来列出所有馆藏书目。

```js
var indexRouter = require("./routes/index");
var usersRouter = require("./routes/users");
```

**备注：** 此时我们刚刚导入了模块；还没有真正使用过其中的路由（稍后会使用）。

下面我们用导入的 `express` 模块来创建 `app` 对象，然后使用它来设置视图（模板）引擎。设置引擎分两步：首先设置 '`views`' 以指定模板的存储文件夹（此处设为子文件夹 **/views**）。然后设置 '`view engine`' 以指定模板库（本例中设为“pug” ）。

```js
var app = express();

// view engine setup
// 视图引擎设定
app.set("views", path.join(__dirname, "views"));
app.set("view engine", "pug");
```

下一组 `app.use()` 调用将中间件库添加进请求处理链。除了之前导入的第三方库之外，我们还使用 `express.static` 中间件将项目 **/public** 目录下所有静态文件托管至根目录。

```js
app.use(logger("dev"));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, "public")));
```

所有中间件都已设置完毕，现在把（之前导入的）路由处理器添加到请求处理链中。从而为网站的不同部分定义具体的路由：

```js
app.use("/", indexRouter);
app.use("/users", usersRouter);
```

**备注：** 这些路径（`'/'` 和 '`/users'`）将作为导入路由的前缀。如果导入的模块 `users` 在 `/profile` 定义了路由，则可以在 `/users/profile` 访问该路由。我们将在后面的文章中，详细讨论路由。

最后一个中间件为错误和 HTTP 404 响应添加处理方法。

```js
// catch 404 and forward to error handler
// 捕获 404 并抛给错误处理器
app.use(function (req, res, next) {
  next(createError(404));
});

// error handler
// 错误处理器
app.use(function (err, req, res, next) {
  // set locals, only providing error in development
  // 设置 locals，只在开发环境提供错误信息
  res.locals.message = err.message;
  res.locals.error = req.app.get("env") === "development" ? err : {};

  // render the error page
  // 渲染出错页面
  res.status(err.status || 500);
  res.render("error");
});
```

Express 应用对象（`app`）现已完成配置。最后一步是将其添加到 `exports` 模块（使它可以通过 **/bin/www** 导入）。

```js
module.exports = app;
```

### 路由

路由文档 **/routes/users.js** 如下所示（由于路由文件均使用类似结构，所以 **index.js** 略过不讲）。首先加载 `express` 模块 并获取 `express.Router` 对象（命名为 `router`）。然后为 `router` 指定路由，最后导出 `router`（就可以导入 **app.js** 了）。

```js
var express = require("express");
var router = express.Router();

/\* GET users listing. \*/
router.get("/", function (req, res, next) {
  res.send("respond with a resource");
});

module.exports = router;
```

该路由定义了一个回调，在检测到正确模式的 HTTP `GET` 请求时将调用该回调。正确模式即导入模块时指定的路由（'`/users`'）加该模块（'`/`'）中定义的任何内容。换句话说，在收到 `/users/` URL 时使用此路由。

**备注：** 用 node 启动该应用并访问 http://localhost:3000/users/，浏览器会返回一条消息：'respond with a resource'。

值得注意的是，上述回调函数有第三个参数 '`next`'，因此它是一个中间件函数，而不是简单的路由回调。`next` 参数暂时还用不到，在 `'/'` 路径中添加多个路由处理器时才会涉及。

### 视图（模板）

视图（模板）存保存在 **/views** 目录中（ **app.js** 中指定），使用 **.pug** 扩展名。 `Response.render()` 方法用某对象的某个变量值一同来渲染一个特定的模板，然后将结果作为响应发送。在 **/routes/index.js** 中可以看到，该路由使用 '`index`' 模板和一个模板变量 `title` 来渲染响应。

```js
/\* GET home page. \*/
router.get("/", function (req, res) {
  res.render("index", { title: "Express" });
});
```

以下是上文代码中涉及到的模板（**index.pug**）。pug 语法稍后再详细讨论。现在只需要知道：`title` 变量将以 `'Express'` 作为值插入模板的指定位置。
```

extends layout

block content
  h1= title
  p Welcome to #{title}
```

挑战自我
----

在 **/routes/users.js** 中添加一个新路由，在 URL `/users/cool/` 处显示文本“你好酷”。运行服务器，并在浏览器中访问 `http://localhost:3000/users/cool/`，测试一下是否成功。

小结
--

现在你已经为 本地图书馆 创建好了骨架，并且成功在 node 上运行起来。同时最重要的是，你了解了项目的结构，从而了解了如何为本地图书馆添加路由和视图。

下一节我们开始修改骨架，让它具备一些图书馆网站的功能。

参见
--

* Express 应用程序生成器（Express 文档）
* 将模板引擎用于 Express（Express 文档）
* 上一页
* Overview: Express Nodejs
* 下一页


Express 教程 4：路由和控制器 - 学习 Web 开发

Express 教程 4：路由和控制器
===================

* 上一页
* Overview: Express Nodejs
* 下一页

本节将为 LocalLibrary 站点中所需的资源端点（Endpoint）配置路由。先用空的处理函数搭建起路由处理的模块结构（下节会将它们扩充为真实的处理函数）。并详细介绍了 Express 路由模块的创建方法。

|  |  |
| --- | --- |
| 前提： |
 回顾
 Express/Node 入门。完成本教程之前小节（Express 教程 3：使用数据库 (Mongoose)
 等）。了解服务器端编程，了解正则表达式。
  |
| 学习目标： | 理解简单路由的创建方法。设置示例中所有 URL 端点。 |

概览
--

上节 定义了与数据库交互的 Mongoose 模型，使用一个（独立）脚本创建了一些初始记录。现在可以编写代码来向用户展示这些信息。首先要确定页面中应显示哪些信息，然后定义适当的 URL 来返回这些资源。随后应创建路由（URL 处理器）和视图（模板）来显示这些页面。

下图展示了 HTTP 请求/响应处理的主数据流和需要实现的行为。图中除视图（View）和路由（Route）外，还展示了控制器（Controller），它们是实际的请求处理函数，与路由请求代码是分开的。

模型已经创建，现在要创建的主要是：

* 路由：把需要支持的请求（以及请求 URL 中包含的任何信息）转发到适当的控制器函数。
* 控制器：从模型中获取请求的数据，创建一个 HTML 页面显示出数据，并将页面返回给用户，以便在浏览器中查看。
* 视图（模板）：供控制器用来渲染数据。

![Express HTTP 请求/响应 路径](/zh-CN/docs/Learn/Server-side/Express_Nodejs/routes/mvc_express.png)

因此我们需要页面来显示藏书、藏书种类、作者、藏书副本的列表和详细信息，还需要页面来创建、更新和删除记录。这些内容对于本节来说不算少，因此本节将主要集中在路由和控制器设置。本节编写的这些函数都只有框架，后续章节再扩展控制器方法，以使用模型数据。

第一段提供了 Express 的 Router 中间件的“入门”知识。后续设置 LocalLibrary 路由时将用到这些知识。

路由入门
----

路由是一段 Express 代码，它将 HTTP 动词（`GET`、`POST`、`PUT`、`DELETE` 等）、URL 路径/模式和处理函数三者关联起来。

创建路由有几种方法。本教程将使 `express.Router` 中间件，因为使用它可以将站点特定部分的路由处理程序打包，并使用通用路由前缀访问它们。我们会将所有与图书馆有关的路由保存在 `catalog` 模块中，在添加处理帐户或其他功能的路由时，可以分开保存。

**备注：** Express 简介 > 创建路由处理程序 简要讨论了 Express 应用的路由机制。使用 `Router` 可以保证更好的模块化（下文所述），且用法与直接在 Express 应用对象定义路由非常类似。

本段以下内容介绍使用 `Router` 定义路由的方法。

### 定义和使用单独的路由模块

以下代码举例说明了如何创建路由模块，以及如何在 Express 应用中使用它。

首先，在 **wiki.js** 模块中创建一个维基路由。代码一开始导入 Express 应用对象，用它取得一个 `Router` 对象，然后用 `get()` 方法向其添加两个具体的路由。模块的最后导出该 `Router` 对象。

```js
// wiki.js - 维基路由模块

const express = require("express");
const router = express.Router();

// 主页路由
router.get("/", (req, res) => {
  res.send("维基主页");
});

// “关于页面”路由
router.get("/about", (req, res) => {
  res.send("关于此维基");
});

module.exports = router;
```

**备注：** 上面的路由处理回调直接定义在了路由函数中。LocalLibrary 的回调将定义在单独的控制器模块中。

要在主应用中使用该路由模块，首先应 `require` 它（**wiki.js**），然后对 Express 应用对象调用 `use()`（指定路径‘/wiki’），即可将其添加到中间件处理路径。

```js
const wiki = require("./wiki.js");
// ...
app.use("/wiki", wiki);
```

这时 `wiki` 模块中定义的两个路由就可以从 `/wiki/` 和 `/wiki/about/` 访问了。

### 路由函数

上述模块定义了两个典型的路由函数。`Router.get()` 方法定义的“about”路由（下方重现的）仅响应 HTTP GET 请求。第一个参数是 URL 路径，第二个参数是一个回调，在收到带有路径的 HTTP GET 请求将调用之。

```js
router.get("/about", (req, res) => {
  res.send("关于此维基");
});
```

该回调有三个参数（通常命名为：`req`、`res`、`next`），分别是：HTTP 请求对象、HTTP 响应、中间件链中的下一个函数。

**备注：** 路由函数就是 Express 中间件，这意味着它们必须（通过响应）结束请求，否则必须调用链中的 `next` 函数。上述示例使用`send()` 完成了请求，所以没有使用 `next` 参数（参数表中将其省略）。

上述路由函数只需要一个回调，可以根据需要指定任意数量的回调参数，或一个回调函数数组。每个函数都将加入中间件链，并且将按添加顺序调用（若有回调完成请求则中止当前周期）。

此处的回调对响应对象调用 `send()`，当收到带有路径（'`/about'`）的 GET 请求时将返回字符串“关于此维基”。还有许多其他可以结束请求/响应周期 响应方法，例如，可调用 `res.json()` 来发送 JSON 响应，或调用 `res.sendFile()` 来发送文件。构建 LocalLibrary 最常使用的响应方法是 `render()`，它使用模板和数据创建并返回 HTML 文件。后续章节进一步讨论。

### HTTP 动词

上面的示例使用 `Router.get()` 方法来响应特定路径的 HTTP GET 请求。

`Router` 还为所有其他 HTTP 动词提供路由方法，大都用法相同：`post()`, `put()`, `delete()`, `options()`, `trace()`, `copy()`, `lock()`, `mkcol()`, `move()`, `purge()`, `propfind()`, `proppatch()`, `unlock()`, `report()`, `mkactivity()`, `checkout()`, `merge()`, `m-search()`, `notify()`, `subscribe()`, `unsubscribe()`, `patch()`, `search()`, 和 `connect()`。

比如下方代码与上方 `/about` 路由行为一致，但只响应 HTTP POST 请求。

```js
router.post("/about", (req, res) => {
  res.send("关于此维基");
});
```

### 路由路径

路由路径用于定义可请求的端点。之前示例中路径都是字符串，并且必须精确写为：'/'、'/ about'、'/ book'，等等。

路由路径也可以是字符串模式（String Pattern）。可用部分正则表达式语法来定义端点的模式。以下是所涉及的正则表达式（注意，连字符（ `-`）和点（`.`）在字符串路径中解释为字面量，不能做为正则表达式）：

* `?`：问号之前的一个字符只能出现零次或一次。例如，路由路径 `'/ab?cd'` 路径匹配端点 `acd` 或 `abcd`。
* `+`：加号之前的一个字符至少出现一次。例如，路径路径 `'/ab+cd'` 匹配端点 `abcd`、`abbcd`、`abbbcd`，等。
* `*`：星号可以替换为任意字符串。例如，路由路径 `'/ab*cd'` 匹配端点 `abcd`、`abXcd`、`abSOMErandomTEXTcd`，等。
* `()`：将一个字符串视为一体以执行 `?`、`+`、`*` 操作。例如。 `'/ab(cd)?e'` 将对 `(cd)` 进行匹配，将匹配到 `abe` 和 `abcde`。

路由路径也可以是 JavaScript 正则表达式。例如，下面的路由路径将匹配 `catfish` 和 `dogfish`，但不会匹配 `catflap`、`catfishhead` 等。注意，正则表达式路径不再用引号 `"..."` 括起来，而是正则表达式语法 `/.../`。

```js
app.get(/.\*fish$/, (req, res) => {
  ...
});
```

**备注：** LocalLibrary 的大部分路由都只使用字符串，很少用字符串模式和正则表达式。接下来将讨论“路由参数”。

### 路由参数

路径参数是命名的 URL 段，用于捕获在 URL 中的位置指定的值。命名段以冒号为前缀，然后是名称（例如 `/:your_parameter_name/`。捕获的值保存在 `req.params` 对象中，键即参数名（例如 `req.params.your_parameter_name`）。

举例说，一个包含用户和藏书信息的 URL：`http://localhost:3000/users/34/books/8989`，可以这样提取信息（使用 `userId` 和 `bookId` 路径参数）：

```js
app.get("/users/:userId/books/:bookId", (req, res) => {
  // 通过 req.params.userId 访问 userId
  // 通过 req.params.bookId 访问 bookId
  res.send(req.params);
});
```

路由参数名必须由“单词字符”（/`[A-Za-z0-9_]`/）组成。

**备注：** URL `/book/create` 会匹配 `/book/:bookId` 这样的路由（将提取值为'`create`' 的 '`bookId`'）。第一个与传入 URL 相匹配的路由会被使用，因此 `/book/create` 的路由处理器必须定义在 `/book/:bookId` 路由之前，才能妥善处理。

以上就是使用路由所有的预备知识。Express 文档中还能找到更多信息：基础路由 和 路由指南。以下是 LocalLibrary 路由和控制器的设置过程。

LocalLibrary 所需路由
-----------------

以下是站点页面的完整 URL 列表。`<object>` 是模型名称（`book`、`bookinstance`、`genre`、`author`），`<objects>` 是一组模型，`<id>` 是每个 Mongoose 模型实例默认的标识字段（`_id`）。

* `catalog/`：主页。
* `catalog/<objects>/`：模型（藏书、藏书副本、藏书种类、作者）的完整列表（例如 `/catalog/books/`、`/catalog/genres/` 等）
* `catalog/<object>/<id>`*：具有* `_id` 字段值的特定模型的详细信息页面（例如 `/catalog/book/584493c1f4887f06c0e67d37`）。
* `catalog/<object>/create`：添加新模型的表单（例如 `/catalog/book/create`）。
* `catalog/<object>/<id>/update`：更新具有 `_id` 字段值的特定模型的表单（例如 `/catalog/book/584493c1f4887f06c0e67d37/update`）。
* `catalog/<object>/<id>/delete`：删除具有 `_id` 字段值的特定模型的表单（例如 `/catalog/book/584493c1f4887f06c0e67d37/delete`）。

首页和列表页面没有包含任何额外信息。因此它们返回的结果只取决于模型类型和数据库内容，获取信息的查询操作是恒定不变的（类似地，创建对象的代码也没有较大改动）。

与之相反，其他 URL 是用于处理特定文档/模型实例的，它们会将项目的标识嵌入 URL（上文的 `<id>`）。可以用路径参数来提取嵌入的信息，并传递给路由处理器（后续章节中用于动态获取数据库中的信息）。通过在 URL 中嵌入信息，使得每种类型的所有资源只需要一个路由（例如，所有藏书副本的显示操作只需要一个路由）。

**备注：** Express 可以通过任何方式构造 URL，可以在 URL 正文中嵌入信息（如上文），或使用 URL `GET` 参数（例如 `/book/?id=6`）。无论哪种方法，URL 都应保持整洁、合理且易读（另请参阅 W3C 相关建议）。

下面我们为上述所有 URL 创建路由处理器回调函数和路由代码。

创建路由处理器回调函数
-----------

定义路由之前应先使用单独的“控制器”模块创建回调的结构骨架。（文件/模块结构没有限制，但以下结构很适合当前项目的规模）。

首先在项目根目录新建一个存放控制器的文件夹（**/controllers**），然后为每个模型创建单独的控制器文件（模块）：
```

/express-locallibrary-tutorial  // 项目根目录
  /controllers
    authorController.js
    bookController.js
    bookinstanceController.js
    genreController.js
```

**备注：** 上述四个文件可到 GitHub 下载 dummyControllers.zip。（链接已失效，请移步英文版查看具体代码）

### `Author` 控制器

以下是 **/controllers/authorController.js** 文件的内容：

```js
const Author = require("../models/author");

// 显示完整的作者列表
exports.author\_list = (req, res) => {
  res.send("未实现：作者列表");
};

// 为每位作者显示详细信息的页面
exports.author\_detail = (req, res) => {
  res.send("未实现：作者详细信息：" + req.params.id);
};

// 由 GET 显示创建作者的表单
exports.author\_create\_get = (req, res) => {
  res.send("未实现：作者创建表单的 GET");
};

// 由 POST 处理作者创建操作
exports.author\_create\_post = (req, res) => {
  res.send("未实现：创建作者的 POST");
};

// 由 GET 显示删除作者的表单
exports.author\_delete\_get = (req, res) => {
  res.send("未实现：作者删除表单的 GET");
};

// 由 POST 处理作者删除操作
exports.author\_delete\_post = (req, res) => {
  res.send("未实现：删除作者的 POST");
};

// 由 GET 显示更新作者的表单
exports.author\_update\_get = (req, res) => {
  res.send("未实现：作者更新表单的 GET");
};

// 由 POST 处理作者更新操作
exports.author\_update\_post = (req, res) => {
  res.send("未实现：更新作者的 POST");
};
```

该模块首先导入了用于访问和更新数据的模型，然后为每个需要处理（添加、更新和删除表单，以及相应的 POST 请求，稍后在 使用表单 一节中讲解）的 URL 导出一个函数。

所有函数都遵循 Express 中间件函数的标准形式，三个参数依次为：请求 `req`、响应 `res`、当前方法无法完成请求循环时（这里不存在这种情况）调用的 `next` 函数。上述方法只返回一个字符串，显示相关页面尚未创建。接收路径参数的控制器函数可将参数输出到消息字符串中（代码中出现的 `req.params.id` ）。

### `BookInstance`、`Genre`、`Book` 控制器

这三个控制器与 `Author` 的模式完全相同，只是 `Book` 有一个用于显示站点欢迎页面的 `index()` 函数：

```js
// /controllers/bookController.js

const Book = require('../models/book');

exports.index = (req, res) => { res.send('未实现：站点首页'); };

...
```

创建藏书编目 `catalog` 路由模组
---------------------

定义好控制器后，我们来为 LocalLibrary 网站创建完整的 URL 路由。

站点骨架中有一个 **./routes** 文件夹，其中包含两个路由文件：index 和 user，在这里新建一个 **catalog.js** 路由文件，如下所示：
```

/express-locallibrary-tutorial   // 项目根目录
  /routes
    index.js
    users.js
    catalog.js
```

**备注：** 可到 GitHub 下载完整的 catalog.js。

**/routes/catalog.js** 中有以下代码：

```js
const express = require("express");
const router = express.Router();

// 导入控制器模块
const book_controller = require("../controllers/bookController");
const author_controller = require("../controllers/authorController");
const genre_controller = require("../controllers/genreController");
const book_instance_controller = require("../controllers/bookinstanceController");

/// 藏书路由 ///

// GET 获取藏书编目主页
router.get("/", book_controller.index);

// GET 请求添加新的藏书。注意此项必须位于显示藏书的路由（使用了 id）之前。
router.get("/book/create", book_controller.book_create_get);

// POST 请求添加新的藏书
router.post("/book/create", book_controller.book_create_post);

// GET 请求删除藏书
router.get("/book/:id/delete", book_controller.book_delete_get);

// POST 请求删除藏书
router.post("/book/:id/delete", book_controller.book_delete_post);

// GET 请求更新藏书
router.get("/book/:id/update", book_controller.book_update_get);

// POST 请求更新藏书
router.post("/book/:id/update", book_controller.book_update_post);

// GET 请求藏书
router.get("/book/:id", book_controller.book_detail);

// GET 请求完整藏书列表
router.get("/books", book_controller.book_list);

/// 藏书副本、藏书种类、作者的路由与藏书路由结构基本一致，只是无需获取主页 ///

module.exports = router;
```

该模块导入了 `express` 并创建了一个 `Router` 对象 `router`。所有路由都设置在 `router` 上，最后将其导出。

对 `router` 对象调用 `.get()` 或`.post()` 函数即可定义路由。这里所有路径都使用字符串定义（而不用字符串模式或正则表达式）。某些特定资源（如藏书）的路由使用路径参数从 URL 中获取对象标识。

处理函数均导入自上文的控制器模块。

### 更新 index 路由模块

新路由已经设置完毕，还需要设置一下 `index` 模块。我们将网站的首页重定向（`redirect`）至刚创建的地址 '`/catalog`'。

将 **/routes/index.js** 中的中间件修改一下：

```js
// GET 请求主页
router.get("/", (req, res) => {
  res.redirect("/catalog");
});
```

**备注：** 这是我们第一次使用 redirect() 响应方法。它会使路由重定向到指定的页面，默认发送 HTTP 状态代码“302 Found”。可以根据需要更改返回的状态代码、设置绝对或相对路径。

### 更新 app.js

最后一步是在 `app.js` 中将路由添加到中间件链。

打开 **app.js**，在 `index` 和 `users` 路由下方添加 `catalog` 路由：

```js
const indexRouter = require("./routes/index");
const usersRouter = require("./routes/users");
const catalogRouter = require("./routes/catalog"); // 导入 catalog 路由
```

然后在已定义的路由下方将目录路由添加进中间件栈：

```js
app.use("/", indexRouter);
app.use("/users", usersRouter);
app.use("/catalog", catalogRouter); // 将 catalog 路由添加进中间件链
```

**备注：** 我们将图书编目模块添加到了 `'/catalog'` 路径，该路径是 catalog 模块中所有路径的前缀。例如，访问藏书列表 的 URL 为：`/catalog/books/`。

大功告成。LocalLibrary 网站所需的所有 URL 的路由和框架函数都已准备完毕。

### 测试路由

要测试路由，先要启动网站

* 默认方法

```bash
DEBUG=express-locallibrary-tutorial:* npm start
```

* 如果设置过 nodemon，则可以使用：

```bash
DEBUG=express-locallibrary-tutorial:* npm run devstart
```

**备注：** 以上命令只对 bash 有效，要在 Windows 上使用 bash，最简单的方法就是安装 Git。（详情参见教程 2 相关译注）

接下来浏览上面一些 URL，确保不会收到错误页面（HTTP 404）。可以尝试以下示例：

* `http://localhost:3000/`
* `http://localhost:3000/catalog`
* `http://localhost:3000/catalog/books`
* `http://localhost:3000/catalog/bookinstances/`
* `http://localhost:3000/catalog/authors/`
* `http://localhost:3000/catalog/genres/`
* `http://localhost:3000/catalog/book/5846437593935e2f8c2aa226`
* `http://localhost:3000/catalog/book/create`
小结
--

网站的路由已创建完毕，接下来的教程将把完整的实现填入控制器框架中。藉此来学习 Express 路由的基础知识，以及组织路由和控制器的一些方式。

下一节将使用视图（模板）和模型里的信息创建一个欢迎页面。

参见
--

* 基本路由（Express 文档）
* 路由指南（Express 文档）
* 上一页
* Overview: Express Nodejs
* 下一页


本地图书馆基础模板 - 学习 Web 开发

本地图书馆基础模板
=========

现在我们了解如何使用 Pug 拓展模板，让我们开始项目，创建一个基础模板。这个模板会有一个侧边栏，连结到本教程中将要创建的各个页面（例如，呈现并创建书本、种类、作者等等），以及一个主要内容区域，我们将在每个页面中进行覆写。

开启 **/views/layout.pug** ，并以下列代码置换其内容。

```pug
doctype html
html(lang='en')
  head
    title= title
    meta(charset='utf-8')
    meta(name='viewport', content='width=device-width, initial-scale=1')
    link(rel='stylesheet', href='https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css')
    script(src='https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js')
    script(src='https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js')
    link(rel='stylesheet', href='/stylesheets/style.css')
  body
    div(class='container-fluid')
      div(class='row')
        div(class='col-sm-2')
          block sidebar
            ul(class='sidebar-nav')
              li
                a(href='/catalog') Home
              li
                a(href='/catalog/books') All books
              li
                a(href='/catalog/authors') All authors
              li
                a(href='/catalog/genres') All genres
              li
                a(href='/catalog/bookinstances') All book-instances
              li
                hr
              li
                a(href='/catalog/author/create') Create new author
              li
                a(href='/catalog/genre/create') Create new genre
              li
                a(href='/catalog/book/create') Create new book
              li
                a(href='/catalog/bookinstance/create') Create new book instance (copy)

        div(class='col-sm-10')
          block content
```

此模板使用（并包含）来自 Bootstrap 的 JavaScript 和 CSS，以改进 HTML 页面的布局和呈现方式。使用 Bootstrap 或其他客户端网页框架，是一种快速的方式，可以创建吸引人的网页，能够良好地适应不同的浏览器尺寸，并且允许我们处理页面的呈现，而不需要纠缠于任何不同尺寸的细节—此处我们只想专注于伺服端代码！

布局的安排应该相当明白，假如你已经阅读了之前的 模板入门。注意，使用 `block content` 当做定位符号，放到页面内容将要放置的地方。

基础模板也参考了一个本地 css 档 (**style.css**) ，此档提供了一些额外的样式。打开 **/public/stylesheets/style.css** ，并用底下的 CSS 代码，取代它的内容：

```css
.sidebar-nav {
  margin-top: 20px;
  padding: 0;
  list-style: none;
}
```

当我们开始运行网站时，我们应该看到侧边栏出现！在本教程的下个部分，我们将使用以上的布局，来定义各个页面。

下一步
---

* 回到 Express 教程 5: 呈现图书馆数据
* 继续教程 5 的下個部分：主页


模板入门 - 学习 Web 开发

模板入门
====

模板是一个文字档，定义了一个输出档的结构或者排版，使用定位符号表示，当模板被绘制时，资料将插入到何处（在 Express，模板被称为视图）。

Express 模板选择
------------

Express 可以与许多不同的模板渲染引擎一起使用。在本教程中，我们使用Pug（以前称为 Jade）作为模板。这是最流行的 Node 模板语言，并且官方将自身描述为“用于编写 HTML，语法干净且空格敏感，受 Haml影响很大”。

不同的模板语言使用不同的方法，来定义布局和标记数据的占位符 — 一些使用 HTML 来定义布局，而另一些则使用可以编译为 HTML 的不同标记格式。Pug 是第二种类型；它使用 HTML 的表示形式，其中任何行中的第一个单词，通常表示 HTML 元素，后续行中的缩进，用于表示嵌套在这些元素中的任何内容。结果是一个页面定义直接转换为 HTML，但可以说更简洁，更容易阅读。

**备注：** 使用 Pug 的缺点，是它对缩进和空格敏感（如果在错误的位置添加额外的空格，可能会得到没什么帮助的错误代码）。但是，一旦你的模板到位，它们就很容易阅读和维护。

模板组态
----

在我们创建骨架网站时，LocalLibrary 配置为使用 Pug。你应该看到 Pug 模块作为依赖项，包含在网站的 **package.json**文件中，以及 **app.js**文件中的以下配置设置。设置告诉我们，使用 Pug 作为视图引擎，Express 应该在 **/views**子目录中搜索模板。

```js
// View engine setup.
app.set("views", path.join(__dirname, "views"));
app.set("view engine", "pug");
```

如果查看 views 目录，你将看到项目默认视图的 .pug 文件。这包括需要用自己的内容替换的主页（**index.pug**）和基本模板（**layout.pug**）的视图。
```

/express-locallibrary-tutorial  //the project root
  /views
    error.pug
    index.pug
    layout.pug
```

模板语法
----

下面的示例模板文件，展示了许多 Pug 最有用的功能。

首先要注意的是，该文件映射典型 HTML 文件的结构，其中（几乎）每一行中的第一个单词是 HTML 元素，并且缩进用于指示嵌套元素。因此，例如，`body` 本文元素位于 `html` 元素内，而段落元素（`p`）位于 `body` 元素内等。非嵌套元素（例如，各个段落）位于不同的行上。

```pug
doctype html
html(lang="en")
  head
    title= title
    script(type='text/javascript').
  body
    h1= title

    p This is a line with #[em some emphasis] and #[strong strong text] markup.
    p This line has un-escaped data: !{'<em> is emphasised</em>'} and escaped data: #{'<em> is not emphasised</em>'}.
      | This line follows on.
    p= 'Evaluated and <em>escaped expression</em>:' + title

    <!-- You can add HTML comments directly -->
    // You can add single line JavaScript comments and they are generated to HTML comments
    //- Introducing a single line JavaScript comment with "//-" ensures the comment isn't rendered to HTML

    p A line with a link
      a(href='/catalog/authors') Some link text
      |  and some extra text.

    #container.col
      if title
        p A variable named "title" exists.
      else
        p A variable named "title" does not exist.
      p.
 Pug is a terse and simple template language with a
 strong focus on performance and powerful features.

    h2 Generate a list

    ul
      each val in [1, 2, 3, 4, 5]
        li= val
```

元素属性被定义在其关联元素之后的括号中。在括号内，属性定义在以逗号或空格分隔的属性名称和属性值对的列表中，例如：

* `script(type='text/javascript')`, `link(rel='stylesheet', href='/stylesheets/style.css')`
* `meta(name='viewport' content='width=device-width initial-scale=1')`

所有属性的值都被转义（例如“`>`”等字符转换为 HTML 代码等效项，如“`>`”），以防止注入 JavaScript 或跨站点脚本攻击。

如果标记后跟着等号，则以下文本将被视为 JavaScript 表达式。因此，打个比方，在下面的第一行中，`h1`标记的内容将是标题变量`title`（在文件中定义，或从 Express 传递到模板中）。在第二行中，段落内容是与标题变量`title`连接的文本字符串。在这两种情况下，默认行为是转义该行。

```pug
h1= title
p= 'Evaluated and <em>escaped expression</em>:' + title
```

如果标记后面没有等号，则将内容视为纯文本。在纯文本中，你可以使用`#{}` 和`!{}`语法，插入转义和非转义数据，如下所示。你还可以在纯文本中添加原始 HTML。

```pug
p This is a line with #[em some emphasis] and #[strong strong text] markup.
p This line has an un-escaped string: !{'<em> is emphasised</em>'}, an escaped string: #{'<em> is not emphasised</em>'}, and escaped variables: #{title}.
```

**备注：** 你几乎总是希望转义来自用户的数据（通过 **`#{}`** 语法）。可信任的数据（例如，生成的记录计数等）可以不先转义就显示。

你可以在行的开头使用管道（“**|**”）字符来表示“纯文本”。例如，下面显示的附加文本，将显示在与前一个锚点相同的行上，但不会链接。

```pug
a(href='http://someurl/') Link text
| Plain text
```

Pug 允许你使用`if`, `else` , `else if` 和 `unless`执行条件操作 - 例如：

```pug
if title
  p A variable named "title" exists
else
  p A variable named "title" does not exist
```

以使用`each-in` 或 `while`语法执行循环/迭代操作。在下面的代码片段中，我们循环遍历数组，以显示变量列表（注意，使用 'li =' 来评估“val” ，以作为下面的变量。）迭代的值也可以传递给模板作为变量！

```pug
ul
  each val in [1, 2, 3, 4, 5]
    li= val
```

语法还支持注释（可以在输出中呈现 - 或者不是 - 可自行选择），支持 mixins 创建可重用的代码块，case 语句和许多其他功能。有关更多详细信息，请参阅Pug文档。

扩展模板
----

在一个站点中，通常所有页面都有一个共同的结构，包括页首，页脚，导航等的标准 HTML 标记。比起强迫开发人员在每个页面中复制这个“样板”的做法，Pug 允许你声明一个基本模板，然后扩展它，只替换每个特定页面不同的地方。

例如，在我们的骨架项目中，创建的基本模板 **layout.pug**，如下所示：

```pug
doctype html
html
  head
    title= title
    link(rel='stylesheet', href='/stylesheets/style.css')
  body
    block content
```

块标记`block`用于标记“可在派生模板中替换的内容部分“（如果未重新定义块，则使用其在基类中的实现）。

默认的 **index.pug**（为我们的骨架项目所创建），显示了我们如何覆盖基本模板。`extends`标记，标识要使用的基本模板，然后我们使用 `block section_name` ，来指示我们将覆盖的部分的新内容。

```pug
extends layout

block content
  h1= title
  p Welcome to #{title}
```

下一步
---

* 回到 Express 教程 5: 呈现图书馆数据
* 继续教程 5 下一个部分：图书馆基本模板


书本列表页面 - 学习 Web 开发

书本列表页面
======

接下做我们将实作书本列表页面。这个页面需要呈现数据库中所有书本的列表，包含每本书的作者、标题，标题将成为一个超连结，连到书本详细内容页面。

控制器
---

书本列表控制器函数，需要获取数据库中所有`Book`对象的列表，然后将这些对象传给模板进行呈现。

打开 **/controllers/bookController.js**. 找到导出的 `book_list()` 控制器方法，并替换為下面的代码。

```js
// Display list of all Books.
exports.book\_list = function (req, res, next) {
  Book.find({}, "title author")
    .populate("author")
    .exec(function (err, list\_books) {
      if (err) {
        return next(err);
      }
      //Successful, so render
      res.render("book\_list", { title: "Book List", book\_list: list_books });
    });
};
```

该方法使用模型的`find()`函数返回所有`Book`对象，选择仅返回标题`title`和作者`author`，因为我们不需要其他字段（它也会返回`_id`和虚拟字段）。这里我们还调用`Book`上的`populate()` ，指定作者`author`字段 — 这将用完整的作者信息，替换存储的书本作者 id。

成功时，传递给查询的回调，将呈现 **book\_list**(.pug) 模板，将标题`title`和`book_list`（包含作者的書本列表）作为变量传递。

视图
--

创建 **/views/book\_list.pug** 并复制底下的文字。

```js
extends layout

block content
  h1= title

  ul
  each book in book_list
    li
      a(href=book.url) #{book.title}
      |  (#{book.author.name})

  else
    li There are no books.
```

這个视图扩展了 **layout.pug** 基本模板，并覆盖了名为 '**content**' 的區块 `block` 。它显示我们从控制器传入的标题`title`（通过`render()`方法），然后使用`each`-`in`-`else`语法，遍历`book_list`变量。为每本图书创建一个列表项，以显示书名，并作为书的详细信息页面的链接，后面跟着作者姓名。如果`book_list`中没有书，则执行`else`子句，并显示文字“没有书” 'There are no books.'。

**备注：** 我们使用 `book.url` ，为每本书提供详细记录链接（我们已经实现了此路由，但尚未实现此页面）。这是 `Book` 模型的一个虚拟属性，它使用模型实例的 `_id` 字段，生成唯一的 URL 路径。

在这里，我們感兴趣的是，每本书被定义为两行，第二行使用管道（上面高亮显示）。这种方法是必要的，因为如果作者姓名位于上一行，那么它将成为超链接的一部分。

它看起來像是？
-------

运行本应用 (参见 测试路由 有相关的命令) ，并打开你的浏览器，访问 http://localhost:3000/。然后选择 所有书本 连结。如果每样东西都设定正确了，你的网站看起来应该像底下的截图。

![Book List Page - Express Local Library site](/zh-CN/docs/Learn/Server-side/Express_Nodejs/Displaying_data/Book_list_page/new_book_list.png)

下一步
---

* 回到 Express 教程 5: 呈现图书馆数据
* 继续教程 5 下个部分：书本实例列表页面


主页 - 学习 Web 开发

主页
==

我们创建的第一个页面，是网站的主页面，可以从网站的根目录 (`'/'`) ，或者 catalog 的根目录 (`catalog/`) 访问。这将呈现一些网站的静态文字描述，以及动态计算数据库中不同记录类型的“计数”。

我们已经为主页创建了一个路由。为了完成页面，我们需要更新控制器函数，以从数据库中提取记录的“计数”，并创建一个可用于呈现页面的视图（模板）。

路由
--

在前面的教程中，我们创建了索引页的路由。此处要提醒的是，所有的路由函数都定义在 **/routes/catalog.js** 中：

```js
// GET 分类主页
router.get("/", book_controller.index); // 实际上，它映射到 /catalog/，因为我们导入路由时使用了 /catalog 前缀
```

在 **/controllers/bookController.js** 中，定义回调函数参数 (`book_controller.index`) ：

```js
exports.index = asyncHandler(async (req, res, next) => {
  res.send("NOT IMPLEMENTED: Site Home Page");
});
```

我们扩展这个控制器函数，以从我们的模型获取信息，然后使用模板（视图）渲染它。

控制器
---

索引控制器函数需要获取以下有关信息，即数据库中有多少`Book`，`BookInstance`，可用的`BookInstance`，`Author`和`Genre`记录，将这些数据渲染到模板中，以创建 HTML 页面，然后将其返回到 HTTP 响应中。

打开 **/controllers/bookController.js**。在文件的顶部，你应该可以看到导出的 `index()` 函数。

```js
const Book = require("../models/book");
const asyncHandler = require("express-async-handler");

exports.index = asyncHandler(async (req, res, next) => {
  res.send("NOT IMPLEMENTED: Site Home Page");
});
```

用以下代码片段替换上面的所有代码。这要做的第一件事，是导入（`require()`）所有模型。我们需要这样做，因为我们将使用它们来获取记录的计数。这部分代码也同样需要“express-async-handler”，它可以捕获路由处理器函数抛出的异常。

```js
const Book = require("../models/book");
const Author = require("../models/author");
const Genre = require("../models/genre");
const BookInstance = require("../models/bookinstance");

const asyncHandler = require("express-async-handler");

exports.index = asyncHandler(async (req, res, next) => {
  // 并行获取书的详细信息、书实例、作者和体裁的数量
  const [
    numBooks,
    numBookInstances,
    numAvailableBookInstances,
    numAuthors,
    numGenres,
  ] = await Promise.all([
    Book.countDocuments({}).exec(),
    BookInstance.countDocuments({}).exec(),
    BookInstance.countDocuments({ status: "Available" }).exec(),
    Author.countDocuments({}).exec(),
    Genre.countDocuments({}).exec(),
  ]);

  res.render("index", {
    title: "Local Library Home",
    book\_count: numBooks,
    book\_instance\_count: numBookInstances,
    book\_instance\_available\_count: numAvailableBookInstances,
    author\_count: numAuthors,
    genre\_count: numGenres,
  });
});
```

成功时，回调函数调用 `res.render()`，指定名为 '**index**' 的视图（模板），以及一个对象包含了要插入其中的数据（这包括我们模型计数的结果对象）。数据以键值对的形式提供，可以使用键在模板中访问。

视图
--

打开 **/views/index.pug**，并用底下文字取代它的内容。

```pug
extends layout

block content
  h1= title
  p Welcome to #[em LocalLibrary], a very basic Express website developed as a tutorial example on the Mozilla Developer Network.

  h1 Dynamic content

  p The library has the following record counts:

  ul
    li #[strong Books:] !{book\_count}
    li #[strong Copies:] !{book\_instance\_count}
    li #[strong Copies available:] !{book\_instance\_available\_count}
    li #[strong Authors:] !{author\_count}
    li #[strong Genres:] !{genre\_count}
```

这个视图很简单。我们扩展了 **layout.pug** 基本模板，覆盖了名为 '**content**' 的模块 `block`。第一个`h1`标题，将是传递给`render()`函数的`title` 变量的转义文本 — 请注意 '`h1=`' 的使用方式，将使得接下來的文本，被视为 JavaScript 表达式。然后我们放入一个介绍本地图书馆的段落。

在动态内容标题下，我们检查从`render()`函数传入的错误变量，是否已定义。如果是这样，我们列出这个错误。如果不是，我们从`data`变量中，获取并列出每个模型的副本数量。

**备注：** 我们没有转义计数值 (i.e. 我们使用 `!{}` 语法) ，因为计数值已经被计算过了。如果信息是由终端用户提供的，那么我们就会转义該变量，以用于显示。

它看起来像是？
-------

此处，我们应该已经创建了呈现 index 页面，所需要的每样东西。运行本地图书馆应用，并打开浏览器访问 `http://localhost:3000/`。如果每样东西都设定正确了，你的网站看起来应该像底下的截图。

![Home page - Express Local Library site](/zh-CN/docs/Learn/Server-side/Express_Nodejs/Displaying_data/Home_page/locallibary_express_home.png)

**备注：** 你将无法使用侧边栏链接，因为这些网页的网址，视图和模板尚未定义。例如，如果你尝试，取决于你点击的链接，你将获取“NOT IMPLEMENTED: Book list”等错误。在“控制器”文件中的不同控制器中，会指定这些字符串文字（将被合适的数据替换）。

下一步
---

* 回到 Express 教程 5：呈现图书馆数据
* 继续阅读教程 5 下个子章节：书本列表页面


使用 moment 做日期格式化 - 学习 Web 开发

使用 moment 做日期格式化
================

我们模型的日期预设呈现很难看：*Tue Dec 06 2016 15:49:58 GMT+1100 (AUS Eastern Daylight Time)*。在本节中，我们将展示如何更新上一节中的 書本實例 BookInstance 列表页面，以更友好的格式显示`due_date`字段：December 6th, 2016。

我们将使用的方法，是在我们的`BookInstance`模型中，创建一个返回格式化日期的虚拟屬性。我们将使用moment 来做实际的格式化，这是一个轻量级 JavaScript 日期库，用于解析，验证，操作和格式化日期。

**备注：** 我们可以直接在 Pug 模板中，使用 *moment* 格式化字符串，或者可以在许多其他地方格式化字符串。使用虚拟属性，可以使我们获得格式化的日期，這与我们当前获取 `due_date` 的方式完全相同。

安装 moment
---------

在项目的根目录，输入下列命令

```bash
npm install moment
```

创建虚拟属性
------

1. 打开 **./models/bookinstance.js**.
2. 在此页面最上方，引用 moment

```js
var moment = require("moment");
```

在 url 属性后面，加入虚拟属性 `due_back_formatted` 。

```js
BookInstanceSchema.virtual("due\_back\_formatted").get(function () {
  return moment(this.due_back).format("MMMM Do, YYYY");
});
```

**备注：** 格式化方法可以使用几乎任何模式显示日期。moment 文档中，可以找到表示不同日期组件的语法。

更新视图
----

打开 **/views/bookinstance\_list.pug** ，然后用 `due_back_formatted` 取代 `due_back` 。

```js
      if val.status!='Available'
        //span (Due: #{val.due\_back} )
        span  (Due: #{val.due_back_formatted} )
```

这就是本章节的全部了。如果你访问侧边栏的 All book-instances，你应该会看到所有的归还日期都更吸引人了！

下一步
---

* 回到 Express 教程 5: 呈现图书馆数据
* 继续教程 5 下一個部分：作者列表页面、种类列表页面、与自我挑战


书籍详细信息页面 - 学习 Web 开发

书籍详细信息页面
========

*书籍详细信息页面*需要呈现指定书籍（`Book`，使用自动生成的 `_id` 字段值进行标识）的信息，以及图书馆中每一个关联的副本（`BookInstance`）的信息。无论我们在哪里呈现一个作者、种类，或书籍实例，都应该链接到关联的详细信息页面。

控制器
---

打开 **/controllers/bookController.js**，找到导出的 `book_detail()` 控制器方法，并将其替换为以下代码。

```js
// 显示特定书籍的详细信息页面。
exports.book_detail = asyncHandler(async (req, res, next) => {
  // 获取书籍的详细信息，以及特定书籍的实例
  const [book, bookInstances] = await Promise.all([
    Book.findById(req.params.id).populate("author").populate("genre").exec(),
    BookInstance.find({ book: req.params.id }).exec(),
  ]);

  if (book === null) {
    // 没有结果。
    const err = new Error("Book not found");
    err.status = 404;
    return next(err);
  }

  res.render("book\_detail", {
    title: book.title,
    book: book,
    book\_instances: bookInstances,
  });
});
```

**备注：** 我们不需要用 require 导入任何额外的模块，因为我们在实现主页控制器时已经导入了依赖项。

此方法与类别详细信息页面描述的方法完全相同。路由控制器函数使用 `Promise.all()` 并行查询指定的书籍（`Book`）以及它的相关副本（`BookInstance`）。如果没有找到匹配的书籍，就会返回一个带有“404: Not Found”错误的 Error 对象。如果找到了书籍，那么就会使用“book\_detail”模板呈现检索到的数据库信息。由于“title”键用于给网页命名（如“layout.pug”中定义的标题），所以这次我们在渲染网页时传递了 `results.book.title`。

视图
--

创建 **/views/book\_detail.pug** 并添加以下文本。

```pug
extends layout

block content
  h1 Title: #{book.title}

  p #[strong Author:]
    a(href=book.author.url) #{book.author.name}
  p #[strong Summary:] #{book.summary}
  p #[strong ISBN:] #{book.isbn}
  p #[strong Genre:]
    each val, index in book.genre
      a(href=val.url) #{val.name}
      if index < book.genre.length - 1
        |,

  div(style='margin-left:20px;margin-top:20px')
    h4 Copies

    each val in book\_instances
      hr
      if val.status=='Available'
        p.text-success #{val.status}
      else if val.status=='Maintenance'
        p.text-danger #{val.status}
      else
        p.text-warning #{val.status}
      p #[strong Imprint:] #{val.imprint}
      if val.status!='Available'
        p #[strong Due back:] #{val.due\_back}
      p #[strong Id:]
        a(href=val.url) #{val.\_id}

    else
      p There are no copies of this book in the library.
```

在这个模板中，几乎所有内容都在先前的章节演示过了。

**备注：** 与本书相关的类别列表在模板中的实现如下。除了最后一个之外，这会在与本书相关的每个类别后面添加一个逗号。

```pug
  p #[strong Genre:]
    each val, index in book.genre
      a(href=val.url) #{val.name}
      if index < book.genre.length - 1
        |,
```

它看起来像是？
-------

运行本应用，并打开浏览器访问 `http://localhost:3000/`。选择 *All books* 链接，然后选择其中某一书籍。如果所有内容都设置正确了，你的页面应类似于以下屏幕截图。

![书籍详细信息页面——Express 本地图书馆网站](/zh-CN/docs/Learn/Server-side/Express_Nodejs/Displaying_data/Book_detail_page/locallibary_express_book_detail.png)

下一步
---

* 回到 Express 教程 5：呈现图书馆数据
* 继续教程 5 的下一个部分：作者详细信息页面


作者细节页面 - 学习 Web 开发

作者细节页面
======

作者细节页面需要呈现指定作者`Author`的信息，使用 `_id` 字段的值（自动产生）识别，接着是这个作者的所有书本物件`Book`的列表。

控制器
---

打开 **/controllers/authorController.js**。

在档案最上方，加入底下几行，引入 async 和 Book 模组 (作者细节页面需要它们)。

```js
var async = require("async");
var Book = require("../models/book");
```

找到 exported `author_detail()` 控制器方法，并用底下代码置换。

```js
// Display detail page for a specific Author.
exports.author\_detail = function (req, res, next) {
  async.parallel(
    {
      author: function (callback) {
        Author.findById(req.params.id).exec(callback);
      },
      authors\_books: function (callback) {
        Book.find({ author: req.params.id }, "title summary").exec(callback);
      },
    },
    function (err, results) {
      if (err) {
        return next(err);
      } // Error in API usage.
      if (results.author == null) {
        // No results.
        var err = new Error("Author not found");
        err.status = 404;
        return next(err);
      }
      // Successful, so render.
      res.render("author\_detail", {
        title: "Author Detail",
        author: results.author,
        author\_books: results.authors_books,
      });
    },
  );
};
```

此处的控制器方法使用 `async.parallel()`，用平行的方式，查询作者 `Author`和相应的书本实例，并附加上绘制本页面的回调，如果 2 个要求都成功完成，就运行回调。这个方式，就跟前面的种类细节页面所说明的完全相同。

视图
--

创建 **/views/author\_detail.pug** ，並複制貼上底下的文字。

```js
extends layout

block content

  h1 Author: #{author.name}
  p #{author.date_of_birth} - #{author.date_of_death}

  div(style='margin-left:20px;margin-top:20px')

    h4 Books

    dl
      each book in author_books
        dt
          a(href=book.url) #{book.title}
        dd #{book.summary}

      else
        p This author has no books.
```

本模板里的所有事物，都在先前的章节演示过了。

它看起來像是？
-------

运行本应用，并打开浏览器访问 http://localhost:3000/。选择 All Authors 连结，然后选择一个作者。如果每个东西都设定正确了，你的网站看起来应该会像底下的截图。

![Author Detail Page - Express Local Library site](/zh-CN/docs/Learn/Server-side/Express_Nodejs/Displaying_data/Author_detail_page/locallibary_express_author_detail.png)

**备注：** 作者的出生与死亡日期的外观很丑！我们将在本文最后的自我挑战处理它。

下一步
---

* 回到 Express 教程 5: 呈现图书馆数据
* 继续教程 5 的下一个部分 : 书本实例细节页面和自我挑战


书本实例列表页面 - 学习 Web 开发

书本实例列表页面
========

接下来，我们将实作图书馆中所有书本实例 (`BookInstance`) 的列表页面。这个页面需要包含与每个 `BookInstance` (链接到其详细信息页面) 关联的书本 `Book` 标题，以及`BookInstance`模型中的其他信息，包含每个副本的状态，印记和唯一 ID。唯一 ID 的文字，应该链接到 `BookInstance` 详细信息页面。

控制器
---

`BookInstance`列表控制器函数，需要获取所有书本实例的列表，填充关联的书本信息，然后将列表传递给模板以进行呈现。

打开 **/controllers/bookinstanceController.js**。找到导出的 `bookinstance_list()` 控制器方法，并用以下代码替换它（更改后的代码以粗体显示）。

```js
// Display list of all BookInstances.
exports.bookinstance\_list = function (req, res, next) {
  BookInstance.find()
    .populate("book")
    .exec(function (err, list\_bookinstances) {
      if (err) {
        return next(err);
      }
      // Successful, so render
      res.render("bookinstance\_list", {
        title: "Book Instance List",
        bookinstance\_list: list_bookinstances,
      });
    });
};
```

此方法使用模型的`find()`函数，返回所有`BookInstance`对象。然后它将一个调用，以菊花链方式连接到`populate()`，附加书本`book`字段，这将使用完整的`Book`文档，替换每个`BookInstance`存储的书本 ID。

成功时，传递给查询的回调，会呈现 **bookinstance\_list** (.pug) 模板，并将标题`title`和书籍实例列表`bookinstance_list`作为变量传递。

视图
--

创建 **/views/bookinstance\_list.pug**，并复制粘贴下面的文本。

```pug
extends layout

block content
  h1= title

  ul
  each val in bookinstance\_list
    li
      a(href=val.url) #{val.book.title} : #{val.imprint} -
      if val.status=='Available'
        span.text-success #{val.status}
      else if val.status=='Maintenance'
        span.text-danger #{val.status}
      else
        span.text-warning #{val.status}
      if val.status!='Available'
        span  (Due: #{val.due\_back} )

  else
    li There are no book copies in this library.
```

这个视图与其他视图非常相似。它扩展了布局，替换*内容*区块，显示从控制器传入的标题 `title`，并遍历 `bookinstance_list` 中的所有书籍副本。对于每个副本，我们都会显示它的状态（用颜色编码），如果书本不可用，则显示其预期返回日期。这里引入了一个新功能——我们可以在标签之后使用点符号表示法，来指定一个类别。因此，`span.text-success` 将被编译为 `<span class="text-success">`（也可以用 Pug 编写为 `span(class="text-success")`）。

它看起来像是？
-------

运行本应用，打开浏览器访问 `http://localhost:3000/`，然后选择 *All book-instances* 链接。假如每个东西都设置正确了，你的网站看起来应该像是底下的截图。

![BookInstance List Page - Express Local Library site](/zh-CN/docs/Learn/Server-side/Express_Nodejs/Displaying_data/BookInstance_list_page/locallibary_express_bookinstance_list.png)

下一步
---

* 回到 Express 教程 5: 呈现图书馆数据
* 继续教程 5 下个部分：日期格式化与使用 moment


作者清单面页、分类清单页面挑战 - 学习 Web 开发

作者清单面页、分类清单页面挑战
===============

作者列表页面，需要呈现数据库中所有作者的列表，有每位作者的名字，并连结到作者详细内容页面。出生与死亡日期应该在名字后面，并且在同一列。

控制器
---

作者列表控制器函数，需要获取所有作者实例的列表，然后将这些实例传递给模板进行渲染。

打开 **/controllers/authorController.js**。在文件顶部附近，找到导出的 `author_list()` 控制器方法，并将其替换为以下代码（更改后的代码以粗体显示）。

```js
// Display list of all Authors.
exports.author\_list = function (req, res, next) {
  Author.find()
    .sort([["family\_name", "ascending"]])
    .exec(function (err, list\_authors) {
      if (err) {
        return next(err);
      }
      //Successful, so render
      res.render("author\_list", {
        title: "Author List",
        author\_list: list_authors,
      });
    });
};
```

该方法使用模型的 `find()`, `sort()` 和 `exec()` 函数，以返回所有 `Author` 对象，并按 `family_name` 的字母顺排列。传递给 `exec()` 方法的回调被调用，并将传入任何错误（或 `null`）作为第一个参数，或者成功时，传入所有作者列表。如果出现错误，则调用带有错误值的下一个中间件函数，如果没有错误，则呈现 **author\_list**（.pug）模板，传递页面标题 `title`，和作者列表（`author_list`）。

视图
--

打开 **/views/author\_list.pug** ，用底下文字取代它的内容。

```js
extends layout

block content
  h1= title

  ul
  each author in author_list
    li
      a(href=author.url) #{author.name}
      | (#{author.date_of_birth} - #{author.date_of_death})

  else
    li There are no authors.
```

如同我们其他的模板，上面视图也依照着同样的模式。

它看起來像是？
-------

运行本应用，并打开浏览器访问 http://localhost:3000/ 。然后选择所有作者 All authors 连结。如果每个东西都设定正确了，页面看起来应该像底下的截图。

![Author List Page - Express Local Library site](/zh-CN/docs/Learn/Server-side/Express_Nodejs/Displaying_data/Author_list_page/locallibary_express_author_list.png)

**备注：** 作者生命日期的外观是丑陋的！你可以使用我们用于`BookInstance` 列表的相同方法（将生命周期的虚拟属性，添加到 `Author` 模型），来改进此方法。

但是，这次缺少日期，除非严格模式生效，否则将忽略对不存在的属性的引用。`moment()`返回当前时间，并且你不希望将缺少的日期格式化为就像今天一样。

解决此问题的一种方法，是定义返回格式化日期的函数内容，以便返回空字符串，除非日期实际存在。例如：

`return this.date_of_birth ? moment(this.date_of_birth).format('YYYY-MM-DD') : '';`

种类列表页面—自我挑战！
------------

在这个部分，你应该实作你自己的种类列表页面。该页面应显示数据库中所有种类的列表，每个种类都链接到其关联的详细信息页面。预期结果的屏幕截图如下所示。

![Genre List - Express Local Library site](/zh-CN/docs/Learn/Server-side/Express_Nodejs/Displaying_data/Author_list_page/locallibary_express_genre_list.png)

种类列表控制器功能，需要获取所有种类实例的列表，然后将这些实例传递给模板进行渲染。

1. 你需要在 **/controllers/genreController.js** 中编辑`genre_list()`。
2. 实现方式几乎与`author_list()`控制器完全相同。
    * 按名称以上升顺序，对结果进行排序。
3. 要呈现的模板，应命名为 **genre\_list.pug**。
4. 要呈现的模板应该传递变量`title`（'Genre List'）和种类列表`genre_list`（从`Genre.find()`回调返回）。
5. 该视图应与上面的屏幕截图/要求相匹配（这应该与作者列表视图具有非常相似的结构/格式，除了种类没有日期）。
下一步
---

回到 Express 教程 5: 呈现图书馆数据

继续教程 5 下一個部分：种类细节页面


书本实例细节页面、与自我挑战 - 学习 Web 开发

书本实例细节页面、与自我挑战
==============

书本实例细节页面
--------

`BookInstance`细节页面，需要呈现每一个`BookInstance`的信息，用 `_id` 字段值（自动产生）做识别。它包含了 `Book` 名称 (也是一个连结，连到 书本细节页面)，接着是纪录中的其他的信息。

### 控制器

打开 **/controllers/bookinstanceController.js**，找到 exported `bookinstance_detail()` 控制器方法，并替换以下代码。

```js
// Display detail page for a specific BookInstance.
exports.bookinstance\_detail = function (req, res, next) {
  BookInstance.findById(req.params.id)
    .populate("book")
    .exec(function (err, bookinstance) {
      if (err) {
        return next(err);
      }
      if (bookinstance == null) {
        // No results.
        var err = new Error("Book copy not found");
        err.status = 404;
        return next(err);
      }
      // Successful, so render.
      res.render("bookinstance\_detail", {
        title: "Book:",
        bookinstance: bookinstance,
      });
    });
};
```

该方法使用从 URL（使用路由）中提取的特定书本实例的 ID，调用`BookInstance.findById()`，并通过请求参数（`req.params.id`），在控制器中访问。然后调用`populate()`来获取相关`Book`的详细信息。

### 视图

创建 **/views/bookinstance\_detail.pug**，并复制到下面的内容中。

```js
extends layout

block content

  h1 ID: #{bookinstance._id}

  p #[strong Title:]
    a(href=bookinstance.book.url) #{bookinstance.book.title}
  p #[strong Imprint:] #{bookinstance.imprint}

  p #[strong Status:]
    if bookinstance.status=='Available'
      span.text-success #{bookinstance.status}
    else if bookinstance.status=='Maintenance'
      span.text-danger #{bookinstance.status}
    else
      span.text-warning #{bookinstance.status}

  if bookinstance.status!='Available'
    p #[strong Due back:] #{bookinstance.due_back}
```

本模组中的所有东西，都在先前的章节演示过了。

### 它看起來像是？

运行本应用，并打开浏览器访问 http://localhost:3000/。选择 All book-instances 连结，然后选择其中一本。如果每个东西都设定正确了，你的网站看起来应该像是底下的截图。

![BookInstance Detail Page - Express Local Library site](/zh-CN/docs/Learn/Server-side/Express_Nodejs/Displaying_data/BookInstance_detail_page_and_challenge/locallibary_express_bookinstance_detail.png)

自我挑战
----

目前，我们网站上显示的大多数日期，都使用默认的 JavaScript 格式（例如 *Tue Dec 06 2016 15:49:58 GMT+1100*（AUS 东部夏令时间）。本文的挑战，是改善作者`Author`生命周期日期显示的外观信息（死亡/出生日期）和 BookInstance 详细信息页面，使用格式：December 6th, 2016。

**备注：** 你可以使用与我们用于 Book Instance List 的相同方法（将生命周期的虚拟属性，添加到`Author`模型，并使用moment来设置日期字符串的格式）。

这一挑战的要求：

1. 用 BookInstance 详细信息页面中的 `due_back_formatted` 替换 `due_back`。
2. 更新作者模块以添加寿命虚拟属性。寿命应該有两个值： *date\_of\_birth - date\_of\_death，這*两个值的格式与 `BookInstance.due_back_formatted`的日期格式相同。
3. 在当前使用`date_of_birth` 和 `date_of_death`的所有视图中，使用 `Author.lifespan` 。
下一步
---

* 回到 Express 教程 5: 呈现图书馆数据


种类细节页面 - 学习 Web 开发

种类细节页面
======

种类细节页面，需要利用`_id` 字段值 (自动生成) ，以呈现特定种类实例的信息。此页面应该呈现种类名称，各个种类的所有书本列表 (每本书都连结到书本的细节页面)。

控制器
---

打开 **/controllers/genreController.js** ，并在档案最上方引用 async 和 Book 模组。

```js
var Book = require("../models/book");
var async = require("async");
```

找到导出的 `genre_detail()` 控制器方法，并将其替换为以下代码。

```js
// Display detail page for a specific Genre.
exports.genre\_detail = function (req, res, next) {
  async.parallel(
    {
      genre: function (callback) {
        Genre.findById(req.params.id).exec(callback);
      },

      genre\_books: function (callback) {
        Book.find({ genre: req.params.id }).exec(callback);
      },
    },
    function (err, results) {
      if (err) {
        return next(err);
      }
      if (results.genre == null) {
        // No results.
        var err = new Error("Genre not found");
        err.status = 404;
        return next(err);
      }
      // Successful, so render
      res.render("genre\_detail", {
        title: "Genre Detail",
        genre: results.genre,
        genre\_books: results.genre_books,
      });
    },
  );
};
```

该方法使用`async.parallel()`，并行查询类型名称及其相关联的书本，并在（如果）两个请求成功完成时，回调呈现页面。

所需种类记录的 ID，在 URL 的末尾编码，并根据路由定义（**/genre/:id**）自动提取。通过请求参数（`req.params.id`）在控制器内访问 ID。它在 `Genre.findById()` 中用于获取当前种类。它还用于获取符合当前种类的所有`Book`对象，就是在种类字段中具有种类 ID 的那些 `Book.find({ 'genre': req.params.id })`。

**备注：** 如果数据库中不存在该类型（即它可能已被删除），则`findById()`将成功返回，但没有结果。在这种情况下，我们想要显示一个“未找到”页面，因此我们创建一个`Error`对象，并将其传递给链中的下一个中间件函数`next`。

```js
if (results.genre == null) {
  // No results.
  var err = new Error("Genre not found");
  err.status = 404;
  return next(err);
}
```

然后，此消息将传播给我们的错误处理代码（这是在我们生成应用程序框架时设置的 - 有关更多信息，请参阅处理错误）。

渲染的视图是 **genre\_detail**，它传递了该类型的标题`title`，种类`genre`和书本列表的变量（`genre_books`）。

视图
--

创建 **/views/genre\_detail.pug** ，并填写底下文字：

```js
extends layout

block content

  h1 Genre: #{genre.name}

  div(style='margin-left:20px;margin-top:20px')

    h4 Books

    dl
    each book in genre_books
      dt
        a(href=book.url) #{book.title}
      dd #{book.summary}

    else
      p This genre has no books
```

这个视图跟我们其他的模板非常相似。主要的差别在于，我们不使用 `title` 传送第一个标题 (虽然它还是用在底层的 **layout.pug** 模板，设定页面的标题)。

它看起來像是？
-------

运行本应用，并打开浏览器访问 http://localhost:3000/。选择 All genres 连结，然后选择其中一个种类 (例如，"Fantasy")。如果每样东西都设定正确了，你的页面看起来应该像底下的截图。

![Genre Detail Page - Express Local Library site](/zh-CN/docs/Learn/Server-side/Express_Nodejs/Displaying_data/Genre_detail_page/locallibary_express_genre_detail.png)

**备注：** 你可能会收到与此类似的错误：

```bash
Cast to ObjectId failed for value " 59347139895ea23f9430ecbb" at path "\_id" for model "Genre"
```

这是来自 **req.params.id** 的 mongoose 错误。要解决这个问题，首先需要在 **genreController.js** 页面上要求 mongoose，如下所示：

```js
var mongoose = require("mongoose");
```

然后使用 \*\*mongoose.Types.ObjectId()\*\*将 id 转换为可以使用的。例如：

```js
exports.genre\_detail = function(req, res, next) {
    var id = mongoose.Types.ObjectId(req.params.id);
    ...
```

下一步
---

* 回到 Express 教程 5: 呈现图书馆数据
* 继续教程 5 下一个部分：书本细节页面


创建书本实例表单 - 学习 Web 开发

创建书本实例表单
========

本章节演示如何定义一个页面/表单，以创建`BookInstance` 物件。这很像我们用来创建书本 `Book` 物件的表单。

导入验证和清理方法
---------

打开 **/controllers/bookinstanceController.js**，并在档案最上方加入以下几行：

```js
const { body, validationResult } = require("express-validator/check");
const { sanitizeBody } = require("express-validator/filter");
```

控制器—get 路由
----------

在档案最上方，用 require 导入书本模型 (因为每个`BookInstance` 都有关连的 `Book`)。

```js
var Book = require("../models/book");
```

找到导出的 `bookinstance_create_get()` 控制器方法，并替换为底下代码。

```js
// Display BookInstance create form on GET.
exports.bookinstance\_create\_get = function (req, res, next) {
  Book.find({}, "title").exec(function (err, books) {
    if (err) {
      return next(err);
    }
    // Successful, so render.
    res.render("bookinstance\_form", {
      title: "Create BookInstance",
      book\_list: books,
    });
  });
};
```

控制器取得所有书本的列表 (`book_list`) 并将它传送到视图 **`bookinstance_form.pug`** (里面附加上 `title`)。

控制器—post 路由
-----------

找到导出的 `bookinstance_create_post()` 控制器方法，并替换为底下代码。

```js
// Handle BookInstance create on POST.
exports.bookinstance_create_post = [
  // Validate fields.
  body("book", "Book must be specified").isLength({ min: 1 }).trim(),
  body("imprint", "Imprint must be specified").isLength({ min: 1 }).trim(),
  body("due\_back", "Invalid date").optional({ checkFalsy: true }).isISO8601(),

  // Sanitize fields.
  sanitizeBody("book").trim().escape(),
  sanitizeBody("imprint").trim().escape(),
  sanitizeBody("status").trim().escape(),
  sanitizeBody("due\_back").toDate(),

  // Process request after validation and sanitization.
  (req, res, next) => {
    // Extract the validation errors from a request.
    const errors = validationResult(req);

    // Create a BookInstance object with escaped and trimmed data.
    var bookinstance = new BookInstance({
      book: req.body.book,
      imprint: req.body.imprint,
      status: req.body.status,
      due\_back: req.body.due_back,
    });

    if (!errors.isEmpty()) {
      // There are errors. Render form again with sanitized values and error messages.
      Book.find({}, "title").exec(function (err, books) {
        if (err) {
          return next(err);
        }
        // Successful, so render.
        res.render("bookinstance\_form", {
          title: "Create BookInstance",
          book\_list: books,
          selected\_book: bookinstance.book._id,
          errors: errors.array(),
          bookinstance: bookinstance,
        });
      });
      return;
    } else {
      // Data from form is valid.
      bookinstance.save(function (err) {
        if (err) {
          return next(err);
        }
        // Successful - redirect to new record.
        res.redirect(bookinstance.url);
      });
    }
  },
];
```

此代码的结构和行为，与创建其他对象的结构和行为相同。首先，我们验证数据，并為数据做無害化處理。如果数据无效，我们会重新显示表單，以及用户最初输入的数据，還有错误消息列表。如果数据有效，我们保存新的`BookInstance`记录，并将用户重定向到详细信息页面。

视图
--

创建 **/views/bookinstance\_form.pug** ，并复制贴上以下代码。
```

extends layout

block content
  h1=title

  form(method='POST' action='')
    div.form-group
      label(for='book') Book:
      select#book.form-control(type='select' placeholder='Select book' name='book' required='true')
        for book in book_list
          if bookinstance
            option(value=book._id selected=(bookinstance.book.toString()==book._id.toString() ? 'selected' : false)) #{book.title}
          else
            option(value=book._id) #{book.title}

    div.form-group
      label(for='imprint') Imprint:
      input#imprint.form-control(type='text' placeholder='Publisher and date information' name='imprint' required='true' value=(undefined===bookinstance ? '' : bookinstance.imprint))
    div.form-group
      label(for='due_back') Date when book available:
      input#due_back.form-control(type='date' name='due_back' value=(undefined===bookinstance ? '' : bookinstance.due_back))

    div.form-group
      label(for='status') Status:
      select#status.form-control(type='select' placeholder='Select status' name='status' required='true')
        option(value='Maintenance') Maintenance
        option(value='Available') Available
        option(value='Loaned') Loaned
        option(value='Reserved') Reserved

    button.btn.btn-primary(type='submit') Submit

  if errors
    ul
      for error in errors
        li!= error.msg
```

这个视图的结构和行为，几乎等同于 **book\_form.pug** 模板，因此我们就不再重覆说明一次了。

**备注：** 以上的模板将状态值 (Maintenance, Available, 等等) 写死在代码里，而且不能 "记忆" 使用者的输入值。如果你愿意的话，考虑重新实作此列表，当表单被重新呈现时，从控制器传入选项数据，并设定选中的值。

它看起來像是？
-------

运行本应用，打开浏览器访问网址 http://localhost:3000/。然后点击创建新书本实例 Create new book instance (copy) 连结。如果每个东西都设定正确了，你的网站看起应该像底下的截图。在你提交一个有效的 `BookInstance` 之后，它应该会被储存，并且你将被带到详细信息页面。

![](/zh-CN/docs/Learn/Server-side/Express_Nodejs/forms/Create_BookInstance_form/locallibary_express_bookinstance_create_empty.png)

下一步
---

* 回到 Express 教程 6: 使用表单
* 继续教程 6 的下一个部分：删除作者表单


删除作者表单 - 学习 Web 开发

删除作者表单
======

此子文档显示，如何定义页面以删除`Author`对象。

正如表单设计部分所讨论的那样，我们的策略是，只允许删除“未被其他对象引用”的对象（在这种情况下，这意味着如果作者`Author`被一本书`Book`引用，我们将不允许删除作者）。在实现方面，这意味着，表单需要在删除作者之前，先确认没有关联的书籍。如果存在关联的书籍，则应显示它们，并说明在删除`Author`对象之前，必须删除它们。

控制器—get 路由
----------

打开 **/controllers/authorController.js**。找到导出的`author_delete_get()` 控制器方法，并将其替换为以下代码。

```js
// Display Author delete form on GET.
exports.author\_delete\_get = function (req, res, next) {
  async.parallel(
    {
      author: function (callback) {
        Author.findById(req.params.id).exec(callback);
      },
      authors\_books: function (callback) {
        Book.find({ author: req.params.id }).exec(callback);
      },
    },
    function (err, results) {
      if (err) {
        return next(err);
      }
      if (results.author == null) {
        // No results.
        res.redirect("/catalog/authors");
      }
      // Successful, so render.
      res.render("author\_delete", {
        title: "Delete Author",
        author: results.author,
        author\_books: results.authors_books,
      });
    },
  );
};
```

控制器从 URL 参数（`req.params.id`）中，获取要删除的`Author`实例的 id。它使用`async.parallel()` 方法，并行获取作者记录和所有相关书本。当两个操作都完成后，它将呈现 **author\_delete.pug** 视图，为 `title`、`author` 和 `author_books` 传递变量。

**备注：** 如果`findById()`返回“没有结果”，则作者不在数据库中。在这种情况下，没有什么可以删除，所以我们立即呈现所有作者的列表。

```js
}, function(err, results) {
    if (err) { return next(err); }
    if (results.author==null) { // No results.
        res.redirect('/catalog/authors')
    }
```

控制器—post 路由
-----------

找到导出的 `author_delete_post()` 控制器方法，并将其替换为以下代码。

```js
// Handle Author delete on POST.
exports.author\_delete\_post = function (req, res, next) {
  async.parallel(
    {
      author: function (callback) {
        Author.findById(req.body.authorid).exec(callback);
      },
      authors\_books: function (callback) {
        Book.find({ author: req.body.authorid }).exec(callback);
      },
    },
    function (err, results) {
      if (err) {
        return next(err);
      }
      // Success
      if (results.authors_books.length > 0) {
        // Author has books. Render in same way as for GET route.
        res.render("author\_delete", {
          title: "Delete Author",
          author: results.author,
          author\_books: results.authors_books,
        });
        return;
      } else {
        // Author has no books. Delete object and redirect to the list of authors.
        Author.findByIdAndRemove(req.body.authorid, function deleteAuthor(err) {
          if (err) {
            return next(err);
          }
          // Success - go to author list
          res.redirect("/catalog/authors");
        });
      }
    },
  );
};
```

首先，我们验证是否已提供 id（这是通过表单主体参数发送的，而不是使用 URL 中的版本）。然后我们以与`GET`路由相同的方式，获得作者及其相关书本。如果没有书本，那么我们删除作者对象，并重定向到所有作者的列表。如果还有书本，那么我们只需重新呈现表格，传递作者和要删除的书本列表。

**备注：** 我们可以检查对 `findById()` 的调用，是否返回任何结果，如果没有，则立即呈现所有作者的列表。为简洁起见，我们将代码保留在上面（如果找不到 id，它仍会返回作者列表，但这将在`findByIdAndRemove()`之后发生）。

视图
--

创建 **/views/author\_delete.pug** 并复制贴上底下文字。
```

extends layout

block content
  h1 #{title}: #{author.name}
  p= author.lifespan

  if author_books.length

    p #[strong Delete the following books before attempting to delete this author.]

    div(style='margin-left:20px;margin-top:20px')

      h4 Books

      dl
      each book in author_books
        dt
          a(href=book.url) #{book.title}
        dd #{book.summary}

  else
    p Do you really want to delete this Author?

    form(method='POST' action='')
      div.form-group
        input#authorid.form-control(type='hidden',name='authorid', required='true', value=author._id )

      button.btn.btn-primary(type='submit') Delete
```

视图扩展了布局模板，覆盖了名为 `content` 的区块。在顶部显示作者详细信息。然后它包含一个，基于\*\*`author_books`\*\*（`if`和`else`子句）数量的条件语句。

* 如果存在与作者相关联的书本，则该页面列出书本，并说明在删除该作者 `Author` 之前，必须删除这些书籍。
* 如果没有书本，则页面会显示确认提示。如果单击“删除”**Delete**按钮，则会在`POST`请求中，将作者 ID 发送到服务器，并且将删除该作者的记录。
加入一个删除控制器
---------

接下来，我们将向 Author 详细视图添加 **Delete** 控件（详细信息页面是删除记录的好地方）。

**备注：** 在完整实现中，控件将仅对授权用户可见。但是在这个时间点上，我们还没有一个授权系统！

打开 **author\_detail.pug** 视图，并在底部添加以下行。

```pug
hr
p
  a(href=author.url+'/delete') Delete author
```

控件现在应显示为链接，如下面的作者详细信息页面所示。

![](/zh-CN/docs/Learn/Server-side/Express_Nodejs/forms/Delete_author_form/locallibary_express_author_detail_delete.png)

它看起來像是？
-------

运行应用程序，并将浏览器打开，到http://localhost:3000/。然后选择所有作者链接 *All authors*，然后选择一个特定作者。最后选择删除作者链接 *Delete* author。

如果作者没有书本，你将看到这样的页面。按删除后，服务器将删除作者并重定向到作者列表。

![](/zh-CN/docs/Learn/Server-side/Express_Nodejs/forms/Delete_author_form/locallibary_express_author_delete_nobooks.png)

如果作者确实有书本，那么你将看到如下视图。然后，你可以从其详细信息页面中，删除这些书本（一旦该代码被实现！）。

![](/zh-CN/docs/Learn/Server-side/Express_Nodejs/forms/Delete_author_form/locallibary_express_author_delete_withbooks.png)

**备注：** 其他删除对象的页面，可以用相同的方式实现。我们把它留下，作为挑战。

下一步
---

* 回到 Express 教程 6: 使用表单
* 继续教程 6 子文档的下一步：更新书本表单