Express 教程第四部分:路由和控制器

在本教程中,我们将为 LocalLibrary 网站中最终需要的所有资源端点设置路由(URL 处理代码)和“虚拟”处理函数。完成后,我们将为路由处理代码提供一个模块化结构,我们可以在后续文章中用实际的处理函数来扩展它。我们还将对如何使用 Express 创建模块化路由有一个很好的理解!

预备知识 阅读 Express/Node 简介。完成之前的教程主题(包括 Express 教程第三部分:使用数据库(Mongoose))。
目标 了解如何创建简单路由。设置我们所有的 URL 端点。

概述

上一篇教程文章中,我们定义了 Mongoose 模型来与数据库交互,并使用一个(独立的)脚本来创建一些初始的库记录。现在我们可以编写代码来向用户展示这些信息。我们需要做的第一件事是确定我们希望能够在页面中显示哪些信息,然后定义用于返回这些资源的相应 URL。然后我们需要创建路由(URL 处理程序)和视图(模板)来显示这些页面。

下图提供了 HTTP 请求/响应处理时需要实现的数据流和主要事项的提醒。除了视图和路由之外,该图还显示了“控制器”——将路由请求的代码与实际处理请求的代码分离开来的函数。

由于我们已经创建了模型,因此我们主要需要创建的是

  • “路由”将支持的请求(以及请求 URL 中编码的任何信息)转发到适当的控制器函数。
  • 控制器函数,用于从模型中获取请求的数据,创建显示数据的 HTML 页面,并将其返回给用户在浏览器中查看。
  • 控制器用于渲染数据的视图(模板)。

Main data flow diagram of an MVC express server: 'Routes' receive the HTTP requests sent to the Express server and forward them to the appropriate 'controller' function. The controller reads and writes data from the models. Models are connected to the database to provide data access to the server. Controllers use 'views', also called templates, to render the data. The Controller sends the HTML HTTP response back to the client as an HTTP response.

最终我们可能会有页面来显示图书、类型、作者和图书实例的列表和详细信息,以及创建、更新和删除记录的页面。在一篇文章中记录这么多内容是很困难的。因此,本文大部分内容将集中在设置我们的路由和控制器以返回“虚拟”内容上。我们将在后续文章中扩展控制器方法以处理模型数据。

下面的第一部分简要介绍了如何使用 Express 的 Router 中间件。然后我们将在以下部分设置 LocalLibrary 路由时使用这些知识。

路由入门

路由是 Express 代码的一个部分,它将 HTTP 动词(GETPOSTPUTDELETE 等)、URL 路径/模式以及一个用于处理该模式的函数关联起来。

有多种方法可以创建路由。在本教程中,我们将使用 express.Router 中间件,因为它允许我们将网站特定部分的路由处理程序分组在一起,并使用一个通用的路由前缀来访问它们。我们将所有与库相关的路由保存在“目录”模块中,如果添加处理用户帐户或其他功能的路由,我们可以将它们单独分组。

注意:我们在Express 简介 > 创建路由处理程序中简要讨论了 Express 应用程序路由。除了为模块化提供更好的支持(如下面第一小节所述)之外,使用 Router 与直接在 Express 应用程序对象上定义路由非常相似。

本节的其余部分概述了如何使用 Router 定义路由。

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

以下代码提供了一个具体的示例,说明我们如何创建路由模块,然后将其用于 Express 应用程序。

首先,我们创建了一个名为 wiki.js 的模块中的 wiki 路由。代码首先导入 Express 应用程序对象,然后使用它获取一个 Router 对象,并使用 get() 方法向其添加了几个路由。最后,该模块导出了 Router 对象。

js
// wiki.js - Wiki route module.

const express = require("express");

const router = express.Router();

// Home page route.
router.get("/", (req, res) => {
  res.send("Wiki home page");
});

// About page route.
router.get("/about", (req, res) => {
  res.send("About this wiki");
});

module.exports = router;

注意:上面我们直接在路由函数中定义了路由处理程序回调。在 LocalLibrary 中,我们将在一个单独的控制器模块中定义这些回调。

要在我们的主应用程序文件中使用路由模块,我们首先 require() 路由模块(wiki.js)。然后我们调用 Express 应用程序上的 use() 来将 Router 添加到中间件处理路径,指定 URL 路径为 'wiki'。

js
const wiki = require("./wiki.js");

// …
app.use("/wiki", wiki);

然后,我们 wiki 路由模块中定义的两个路由将可通过 /wiki//wiki/about/ 访问。

路由函数

我们上面的模块定义了几个典型的路由函数。“about”路由(复制如下)是使用 Router.get() 方法定义的,该方法仅响应 HTTP GET 请求。此方法的第一个参数是 URL 路径,第二个参数是接收到带有该路径的 HTTP GET 请求时将调用的回调函数。

js
router.get("/about", (req, res) => {
  res.send("About this wiki");
});

回调函数接受三个参数(通常按所示命名:reqresnext),它们将包含 HTTP 请求对象、HTTP 响应以及中间件链中的 next 函数。

注意:路由函数是Express 中间件,这意味着它们必须完成(响应)请求或调用链中的 next 函数。在上面的情况下,我们使用 send() 完成请求,因此 next 参数未使用(我们选择不指定它)。

上面的路由函数只接受一个回调,但您可以指定任意数量的回调参数,或者一个回调函数数组。每个函数都是中间件链的一部分,并将按照添加到链中的顺序被调用(除非前面的函数完成了请求)。

此处的 callback 函数在响应上调用 send(),以在接收到路径为 (/about) 的 GET 请求时返回字符串“About this wiki”。还有许多其他响应方法用于结束请求/响应周期。例如,您可以调用 res.json() 发送 JSON 响应,或调用 res.sendFile() 发送文件。在构建库时,我们将最常使用的响应方法是 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 this wiki");
});

路由路径

路由路径定义了可以发出请求的端点。我们目前看到的示例只是字符串,并按原样使用:'/'、'/about'、'/book'、'/any-random.path'。

路由路径也可以是字符串模式。字符串模式使用正则表达式语法的一种形式来定义将匹配的端点“模式”。LocalLibrary 的大多数路由将使用字符串而不是正则表达式。我们还将使用下一节中讨论的路由参数。

路由参数

路由参数是命名的 URL 段,用于捕获 URL 中特定位置的值。命名的段以冒号开头,后跟名称(例如,/:your_parameter_name/)。捕获的值存储在 req.params 对象中,使用参数名称作为键(例如,req.params.your_parameter_name)。

因此,例如,考虑一个编码包含用户和图书信息的 URL:https://:3000/users/34/books/8989。我们可以如下所示提取此信息,其中包含 userIdbookId 路径参数

js
app.get("/users/:userId/books/:bookId", (req, res) => {
  // Access userId via: req.params.userId
  // Access bookId via: req.params.bookId
  res.send(req.params);
});

注意:URL /book/create 将由诸如 /book/:bookId 的路由匹配(因为 :bookId任何 字符串的占位符,因此 create 匹配)。将使用与传入 URL 匹配的第一个路由,因此如果您想专门处理 /book/create URL,则其路由处理程序必须在您的 /book/:bookId 路由之前定义。

路由参数名称(例如上面的 bookId)可以是任何以字母、_$ 开头的有效 JavaScript 标识符。您可以在第一个字符之后包含数字,但不能包含连字符和空格。您还可以使用不是有效 JavaScript 标识符的名称,包括空格、连字符、表情符号或任何其他字符,但您需要使用带引号的字符串定义它们,并使用括号表示法访问它们。例如

js
app.get('/users/:"user id"/books/:"book-id"', (req, res) => {
  // Access quoted param using bracket notation
  const user = req.params["user id"];
  const book = req.params["book-id"];
  res.send({ user, book });
});

通配符

通配符参数匹配多个段中的一个或多个字符,将每个段作为数组中的一个值返回。它们的定义方式与常规参数相同,但以星号为前缀。

因此,例如,考虑 URL https://:3000/users/34/books/8989,我们可以使用 example 通配符提取 users/ 之后的所有信息

js
app.get("/users/*example", (req, res) => {
  // req.params would contain { "example": ["34", "books", "8989"]}
  res.send(req.params);
});

可选部分

花括号可用于定义路径中的可选部分。例如,下面我们匹配带有任何扩展名(或没有扩展名)的文件名。

js
app.get("/file/:filename{.:ext}", (req, res) => {
  // Given URL: https://:3000/file/somefile.md`
  // req.params would contain { "filename": "somefile", "ext": "md"}
  res.send(req.params);
});

保留字符

以下字符是保留的:(()[]?+!)。如果您想使用它们,必须用反斜杠 (\) 对它们进行转义。

您也不能在正则表达式中使用管道符 (|)。

这就是开始使用路由所需的全部内容。如果需要,您可以在 Express 文档中找到更多信息:基本路由路由指南。以下部分将展示我们如何为 LocalLibrary 设置路由和控制器。

处理路由函数中的错误和异常

前面显示的路由函数都带有 reqres 参数,分别代表请求和响应。路由函数还会传递第三个参数 next,其中包含一个回调函数,可以调用该函数将任何错误或异常传递给 Express 中间件链,最终它们将传播到您的全局错误处理代码。

从 Express 5 开始,如果路由处理程序返回的 Promise 随后被拒绝,则会自动使用拒绝值调用 next;因此,在使用 Promise 时,路由函数中不需要错误处理代码。在使用异步基于 Promise 的 API 时,这会导致非常紧凑的代码,尤其是在使用 asyncawait 时。

例如,以下代码使用 find() 方法查询数据库,然后渲染结果。

js
exports.get("/about", async (req, res, next) => {
  const successfulResult = await About.find({}).exec();
  res.render("about_view", { title: "About", list: successfulResult });
});

以下代码展示了使用 Promise 链的相同示例。请注意,如果您愿意,您可以 catch() 错误并实现自己的自定义处理。

js
exports.get(
  "/about",
  // Removed 'async'
  (req, res, next) =>
    About.find({})
      .exec()
      .then((successfulResult) => {
        res.render("about_view", { title: "About", list: successfulResult });
      })
      .catch((err) => {
        next(err);
      }),
);

注意:大多数现代 API 都是异步且基于 Promise 的,因此错误处理通常就是这么简单。当然,这只是您在本教程中真正需要了解的错误处理!

Express 5 自动捕获并转发在同步代码中抛出的异常

js
app.get("/", (req, res) => {
  // Express will catch this
  throw new Error("SynchronousException");
});

但是,您必须 catch() 路由处理程序或中间件调用的异步代码中发生的异常。这些将不会被默认代码捕获

js
app.get("/", (req, res, next) => {
  setTimeout(() => {
    try {
      // You must catch and propagate this error yourself
      throw new Error("AsynchronousException");
    } catch (err) {
      next(err);
    }
  }, 100);
});

最后,如果您使用的是旧式的异步方法,这些方法在回调函数中返回错误或结果,那么您需要自己传播错误。以下示例展示了如何操作。

js
router.get("/about", (req, res, next) => {
  About.find({}).exec((err, queryResults) => {
    if (err) {
      // Propagate the error
      return next(err);
    }
    // Successful, so render
    res.render("about_view", { title: "About", list: queryResults });
  });
});

更多信息请参见错误处理

LocalLibrary 所需的路由

我们将最终用于页面的 URL 列在下面,其中 object 替换为每个模型(书、图书实例、类型、作者)的名称,objectsobject 的复数形式,而 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 将项目的标识编码在 URL 中(如上所示为 <id>)。我们将使用路径参数提取编码信息并将其传递给路由处理程序(在后面的文章中,我们将使用它来动态确定从数据库中获取哪些信息)。通过将信息编码在 URL 中,我们只需要一个路由来处理特定类型资源的每个实例(例如,一个路由来处理每个图书项目的显示)。

注意:Express 允许您以任何方式构建 URL — 您可以将信息编码在 URL 的主体中,如上所示,或使用 URL GET 参数(例如,/book/?id=6)。无论您使用哪种方法,URL 都应保持简洁、逻辑和可读性(请在此处查看 W3C 的建议)。

接下来,我们将为上述所有 URL 创建路由处理程序回调函数和路由代码。

创建路由处理回调函数

在定义路由之前,我们首先创建所有将调用的虚拟/骨架回调函数。这些回调将存储在 BookBookInstanceGenreAuthor 的独立“控制器”模块中(您可以使用任何文件/模块结构,但这似乎是本项目适当的粒度)。

首先,在项目根目录(/controllers)中为控制器创建一个文件夹,然后为每个模型创建单独的控制器文件/模块

/express-locallibrary-tutorial  # the project root
  /controllers
    authorController.js
    bookController.js
    bookinstanceController.js
    genreController.js

作者控制器

打开 /controllers/authorController.js 文件并输入以下代码

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

// Display list of all Authors.
exports.author_list = async (req, res, next) => {
  res.send("NOT IMPLEMENTED: Author list");
};

// Display detail page for a specific Author.
exports.author_detail = async (req, res, next) => {
  res.send(`NOT IMPLEMENTED: Author detail: ${req.params.id}`);
};

// Display Author create form on GET.
exports.author_create_get = async (req, res, next) => {
  res.send("NOT IMPLEMENTED: Author create GET");
};

// Handle Author create on POST.
exports.author_create_post = async (req, res, next) => {
  res.send("NOT IMPLEMENTED: Author create POST");
};

// Display Author delete form on GET.
exports.author_delete_get = async (req, res, next) => {
  res.send("NOT IMPLEMENTED: Author delete GET");
};

// Handle Author delete on POST.
exports.author_delete_post = async (req, res, next) => {
  res.send("NOT IMPLEMENTED: Author delete POST");
};

// Display Author update form on GET.
exports.author_update_get = async (req, res, next) => {
  res.send("NOT IMPLEMENTED: Author update GET");
};

// Handle Author update on POST.
exports.author_update_post = async (req, res, next) => {
  res.send("NOT IMPLEMENTED: Author update POST");
};

该模块首先需要我们将用于访问和更新数据的 Author 模型。然后它导出了我们将要处理的每个 URL 的函数。请注意,创建、更新和删除操作使用表单,因此也有用于处理表单 POST 请求的附加方法——我们将在后面的“表单文章”中讨论这些方法。

这些函数返回一个字符串,指示相关页面尚未创建。如果控制器函数预计会接收路径参数,这些参数将输出到消息字符串中(参见上方的 req.params.id)。

图书实例控制器

打开 /controllers/bookinstanceController.js 文件并复制以下代码(这与 Author 控制器模块遵循相同的模式)

js
const BookInstance = require("../models/bookinstance");

// Display list of all BookInstances.
exports.bookinstance_list = async (req, res, next) => {
  res.send("NOT IMPLEMENTED: BookInstance list");
};

// Display detail page for a specific BookInstance.
exports.bookinstance_detail = async (req, res, next) => {
  res.send(`NOT IMPLEMENTED: BookInstance detail: ${req.params.id}`);
};

// Display BookInstance create form on GET.
exports.bookinstance_create_get = async (req, res, next) => {
  res.send("NOT IMPLEMENTED: BookInstance create GET");
};

// Handle BookInstance create on POST.
exports.bookinstance_create_post = async (req, res, next) => {
  res.send("NOT IMPLEMENTED: BookInstance create POST");
};

// Display BookInstance delete form on GET.
exports.bookinstance_delete_get = async (req, res, next) => {
  res.send("NOT IMPLEMENTED: BookInstance delete GET");
};

// Handle BookInstance delete on POST.
exports.bookinstance_delete_post = async (req, res, next) => {
  res.send("NOT IMPLEMENTED: BookInstance delete POST");
};

// Display BookInstance update form on GET.
exports.bookinstance_update_get = async (req, res, next) => {
  res.send("NOT IMPLEMENTED: BookInstance update GET");
};

// Handle bookinstance update on POST.
exports.bookinstance_update_post = async (req, res, next) => {
  res.send("NOT IMPLEMENTED: BookInstance update POST");
};

类型控制器

打开 /controllers/genreController.js 文件并复制以下文本(这与 AuthorBookInstance 文件遵循相同的模式)

js
const Genre = require("../models/genre");

// Display list of all Genre.
exports.genre_list = async (req, res, next) => {
  res.send("NOT IMPLEMENTED: Genre list");
};

// Display detail page for a specific Genre.
exports.genre_detail = async (req, res, next) => {
  res.send(`NOT IMPLEMENTED: Genre detail: ${req.params.id}`);
};

// Display Genre create form on GET.
exports.genre_create_get = async (req, res, next) => {
  res.send("NOT IMPLEMENTED: Genre create GET");
};

// Handle Genre create on POST.
exports.genre_create_post = async (req, res, next) => {
  res.send("NOT IMPLEMENTED: Genre create POST");
};

// Display Genre delete form on GET.
exports.genre_delete_get = async (req, res, next) => {
  res.send("NOT IMPLEMENTED: Genre delete GET");
};

// Handle Genre delete on POST.
exports.genre_delete_post = async (req, res, next) => {
  res.send("NOT IMPLEMENTED: Genre delete POST");
};

// Display Genre update form on GET.
exports.genre_update_get = async (req, res, next) => {
  res.send("NOT IMPLEMENTED: Genre update GET");
};

// Handle Genre update on POST.
exports.genre_update_post = async (req, res, next) => {
  res.send("NOT IMPLEMENTED: Genre update POST");
};

图书控制器

打开 /controllers/bookController.js 文件并复制以下代码。这与其他的控制器模块遵循相同的模式,但额外有一个用于显示站点欢迎页面的 index() 函数

js
const Book = require("../models/book");

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

// Display list of all books.
exports.book_list = async (req, res, next) => {
  res.send("NOT IMPLEMENTED: Book list");
};

// Display detail page for a specific book.
exports.book_detail = async (req, res, next) => {
  res.send(`NOT IMPLEMENTED: Book detail: ${req.params.id}`);
};

// Display book create form on GET.
exports.book_create_get = async (req, res, next) => {
  res.send("NOT IMPLEMENTED: Book create GET");
};

// Handle book create on POST.
exports.book_create_post = async (req, res, next) => {
  res.send("NOT IMPLEMENTED: Book create POST");
};

// Display book delete form on GET.
exports.book_delete_get = async (req, res, next) => {
  res.send("NOT IMPLEMENTED: Book delete GET");
};

// Handle book delete on POST.
exports.book_delete_post = async (req, res, next) => {
  res.send("NOT IMPLEMENTED: Book delete POST");
};

// Display book update form on GET.
exports.book_update_get = async (req, res, next) => {
  res.send("NOT IMPLEMENTED: Book update GET");
};

// Handle book update on POST.
exports.book_update_post = async (req, res, next) => {
  res.send("NOT IMPLEMENTED: Book update POST");
};

创建目录路由模块

接下来,我们为 LocalLibrary 网站所需的所有 URL 创建路由,这些路由将调用我们在前几节中定义的控制器函数。

骨架已经有一个 ./routes 文件夹,其中包含 indexusers 的路由。在此文件夹内创建另一个路由文件 — catalog.js — 如下图所示。

/express-locallibrary-tutorial # the project root
  /routes
    index.js
    users.js
    catalog.js

打开 /routes/catalog.js 并复制以下代码

js
const express = require("express");

// Require controller modules.
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");

const router = express.Router();

/// BOOK ROUTES ///

// GET catalog home page.
router.get("/", book_controller.index);

// GET request for creating a Book. NOTE This must come before routes that display Book (uses id).
router.get("/book/create", book_controller.book_create_get);

// POST request for creating Book.
router.post("/book/create", book_controller.book_create_post);

// GET request to delete Book.
router.get("/book/:id/delete", book_controller.book_delete_get);

// POST request to delete Book.
router.post("/book/:id/delete", book_controller.book_delete_post);

// GET request to update Book.
router.get("/book/:id/update", book_controller.book_update_get);

// POST request to update Book.
router.post("/book/:id/update", book_controller.book_update_post);

// GET request for one Book.
router.get("/book/:id", book_controller.book_detail);

// GET request for list of all Book items.
router.get("/books", book_controller.book_list);

/// AUTHOR ROUTES ///

// GET request for creating Author. NOTE This must come before route for id (i.e. display author).
router.get("/author/create", author_controller.author_create_get);

// POST request for creating Author.
router.post("/author/create", author_controller.author_create_post);

// GET request to delete Author.
router.get("/author/:id/delete", author_controller.author_delete_get);

// POST request to delete Author.
router.post("/author/:id/delete", author_controller.author_delete_post);

// GET request to update Author.
router.get("/author/:id/update", author_controller.author_update_get);

// POST request to update Author.
router.post("/author/:id/update", author_controller.author_update_post);

// GET request for one Author.
router.get("/author/:id", author_controller.author_detail);

// GET request for list of all Authors.
router.get("/authors", author_controller.author_list);

/// GENRE ROUTES ///

// GET request for creating a Genre. NOTE This must come before route that displays Genre (uses id).
router.get("/genre/create", genre_controller.genre_create_get);

// POST request for creating Genre.
router.post("/genre/create", genre_controller.genre_create_post);

// GET request to delete Genre.
router.get("/genre/:id/delete", genre_controller.genre_delete_get);

// POST request to delete Genre.
router.post("/genre/:id/delete", genre_controller.genre_delete_post);

// GET request to update Genre.
router.get("/genre/:id/update", genre_controller.genre_update_get);

// POST request to update Genre.
router.post("/genre/:id/update", genre_controller.genre_update_post);

// GET request for one Genre.
router.get("/genre/:id", genre_controller.genre_detail);

// GET request for list of all Genre.
router.get("/genres", genre_controller.genre_list);

/// BOOKINSTANCE ROUTES ///

// GET request for creating a BookInstance. NOTE This must come before route that displays BookInstance (uses id).
router.get(
  "/bookinstance/create",
  book_instance_controller.bookinstance_create_get,
);

// POST request for creating BookInstance.
router.post(
  "/bookinstance/create",
  book_instance_controller.bookinstance_create_post,
);

// GET request to delete BookInstance.
router.get(
  "/bookinstance/:id/delete",
  book_instance_controller.bookinstance_delete_get,
);

// POST request to delete BookInstance.
router.post(
  "/bookinstance/:id/delete",
  book_instance_controller.bookinstance_delete_post,
);

// GET request to update BookInstance.
router.get(
  "/bookinstance/:id/update",
  book_instance_controller.bookinstance_update_get,
);

// POST request to update BookInstance.
router.post(
  "/bookinstance/:id/update",
  book_instance_controller.bookinstance_update_post,
);

// GET request for one BookInstance.
router.get("/bookinstance/:id", book_instance_controller.bookinstance_detail);

// GET request for list of all BookInstance.
router.get("/bookinstances", book_instance_controller.bookinstance_list);

module.exports = router;

该模块需要 Express,然后使用它来创建一个 Router 对象。所有路由都在路由器上设置,然后导出。

路由使用路由器对象的 .get().post() 方法定义。所有路径都使用字符串定义(我们不使用字符串模式或正则表达式)。处理特定资源(例如书籍)的路由使用路径参数从 URL 获取对象 ID。

处理函数都从我们在上一节中创建的控制器模块中导入。

更新索引路由模块

我们已经设置了所有新路由,但仍然有一个指向原始页面的路由。现在,让我们将它重定向到我们在路径 /catalog 创建的新索引页面。

打开 /routes/index.js 并用下面的函数替换现有路由。

js
// GET home page.
router.get("/", (req, res) => {
  res.redirect("/catalog");
});

注意:这是我们第一次使用 redirect() 响应方法。此方法重定向到指定的页面,默认情况下发送 HTTP 状态码“302 Found”。如果需要,您可以更改返回的状态码,并提供绝对或相对路径。

更新 app.js

最后一步是将路由添加到中间件链中。我们在 app.js 中完成此操作。

打开 app.js 并在其他路由下方引入目录路由(添加下面显示的第三行,在文件中已有的另外两行下方)

js
const indexRouter = require("./routes/index");
const usersRouter = require("./routes/users");
const catalogRouter = require("./routes/catalog"); // Import routes for "catalog" area of site

接下来,将目录路由添加到其他路由下方的中间件堆栈中(添加下面显示的第三行,在文件中已有的另外两行下方)

js
app.use("/", indexRouter);
app.use("/users", usersRouter);
app.use("/catalog", catalogRouter); // Add catalog routes to middleware chain.

注意:我们已在路径 /catalog 处添加了我们的目录模块。此路径将添加到目录模块中定义的所有路径的前面。因此,例如,要访问图书列表,URL 将是:/catalog/books/

就这样。现在,我们应该已经为 LocalLibrary 网站最终将支持的所有 URL 启用了路由和骨架函数。

测试路由

要测试路由,首先使用您通常的方法启动网站

  • 默认方法

    bash
    # Windows
    SET DEBUG=express-locallibrary-tutorial:* & npm start
    
    # macOS or Linux
    DEBUG=express-locallibrary-tutorial:* npm start
    
  • 如果您之前设置了 nodemon,则可以使用

    bash
    npm run serverstart
    

然后导航到一些 LocalLibrary URL,并验证您没有收到错误页面 (HTTP 404)。下面列出了一些 URL,供您方便使用

  • https://:3000/
  • https://:3000/catalog
  • https://:3000/catalog/books
  • https://:3000/catalog/bookinstances/
  • https://:3000/catalog/authors/
  • https://:3000/catalog/genres/
  • https://:3000/catalog/book/5846437593935e2f8c2aa226
  • https://:3000/catalog/book/create

总结

现在我们已经为我们的网站创建了所有路由,以及可在后续文章中填充完整实现的虚拟控制器函数。在此过程中,我们学习了大量关于 Express 路由、异常处理以及构建路由和控制器的一些方法的​​基本信息。

在下一篇文章中,我们将使用视图(模板)和模型中存储的信息为网站创建一个合适的欢迎页面。

另见