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

在本教程中,我们将为最终在LocalLibrary网站中需要的所有资源端点设置路由(URL 处理代码)以及“虚拟”处理程序函数。完成后,我们将拥有一个模块化的路由处理代码结构,我们可以在后续文章中使用真实的处理程序函数对其进行扩展。我们还将深入了解如何使用 Express 创建模块化路由!

先决条件 阅读Express/Node 简介。完成之前的教程主题(包括Express 教程第 3 部分:使用数据库(使用 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中间件,因为它允许我们将特定站点部分的路由处理程序组合在一起,并使用公共路由前缀访问它们。我们将把所有与库相关的路由保存在“catalog”模块中,如果我们添加用于处理用户帐户或其他功能的路由,我们可以将它们分别分组。

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

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

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

以下代码提供了一个具体的示例,说明我们如何创建路由模块并在Express应用程序中使用它。

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

js
// wiki.js - Wiki route module.

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

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

// About page route.
router.get("/about", function (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/about/访问在我们的 wiki 路由模块中定义的两个路由。

路由函数

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

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

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

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

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

此处的回调函数在响应上调用send(),当我们收到路径为('/about')的 GET 请求时,返回字符串“关于此维基”。有其他一些响应方法用于结束请求/响应周期。例如,您可以调用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'。

路由路径也可以是字符串模式。字符串模式使用正则表达式语法的一种形式来定义将匹配的端点模式。语法如下所示(请注意,连字符(-)和点(.)在基于字符串的路径中按字面意思解释)

  • ?:端点必须有 0 个或 1 个前面的字符(或组),例如,路由路径'/ab?cd'将匹配端点acdabcd
  • +:端点必须有 1 个或多个前面的字符(或组),例如,路由路径'/ab+cd'将匹配端点abcdabbcdabbbcd等。
  • *:端点可以在放置*字符的位置具有任意字符串。例如,路由路径'/ab*cd'将匹配端点abcdabXcdabSOMErandomTEXTcd等。
  • ():对一组字符进行分组匹配以对其执行其他操作,例如,'/ab(cd)?e'将对组(cd)执行?匹配——它将匹配abeabcde

路由路径也可以是 JavaScript 正则表达式。例如,下面的路由路径将匹配catfishdogfish,但不会匹配catflapcatfishhead等。请注意,正则表达式的路径使用正则表达式语法(它不是像以前的情况那样是带引号的字符串)。

js
app.get(/.*fish$/, function (req, res) {
  // …
});

注意:LocalLibrary 的大多数路由将使用字符串而不是正则表达式。我们还将使用下一节中讨论的路由参数。

路由参数

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

例如,考虑一个 URL,其编码包含有关用户和书籍的信息:https://127.0.0.1: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);
});

路由参数的名称必须由“单词字符”(A-Z、a-z、0-9 和 _)组成。

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

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

处理路由函数中的错误

前面显示的路由函数都具有reqres参数,它们分别表示请求和响应。路由函数也使用第三个参数next调用,该参数可用于将错误传递给 Express 中间件链。

以下代码展示了它是如何工作的,使用数据库查询的示例,该查询采用回调函数,并返回错误err或一些结果。如果返回err,则使用err作为其第一个参数的值调用next(最终错误会传播到我们的全局错误处理代码)。成功时,返回所需数据,然后在响应中使用。

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

处理路由函数中的异常

上一节展示了 Express 如何期望路由函数返回错误。该框架设计用于与异步函数一起使用,这些函数接收一个回调函数(带有错误和结果参数),并在操作完成后调用该函数。这是一个问题,因为稍后我们将进行使用Promise 基于 API 的 Mongoose 数据库查询,这些查询可能会在我们的路由函数中抛出异常(而不是在回调中返回错误)。

为了使框架能够正确处理异常,必须捕获这些异常,然后将其作为错误转发,如上一节所示。

注意:目前处于测试阶段的 Express 5 预计将原生处理 JavaScript 异常。

重新构思上一节中使用 About.find().exec() 作为返回 Promise 的数据库查询的简单示例,我们可能会在try...catch 代码块中编写路由函数,如下所示

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

为每个函数添加如此多的样板代码。相反,在本教程中,我们将使用express-async-handler 模块。这定义了一个包装函数,它隐藏了 try...catch 代码块和转发错误的代码。同一个示例现在非常简单,因为我们只需要编写假设成功情况下的代码即可

js
// Import the module
const asyncHandler = require("express-async-handler");

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

LocalLibrary 需要哪些路由

我们最终需要的页面 URL 列在下面,其中 object 被每个模型的名称(book、bookinstance、genre、author)替换,objects 是 object 的复数形式,id 是默认情况下每个 Mongoose 模型实例赋予的唯一实例字段 (_id)。

  • catalog/ — 首页/索引页。
  • catalog/<objects>/ — 所有书籍、bookinstance、类型或作者的列表(例如 /catalog/books/、/catalog/genres/ 等)。
  • catalog/<object>/<id> — 具有给定 _id 字段值的特定书籍、bookinstance、类型或作者的详细信息页面(例如 /catalog/book/584493c1f4887f06c0e67d37)
  • catalog/<object>/create — 创建新书籍、bookinstance、类型或作者的表单(例如 /catalog/book/create)
  • catalog/<object>/<id>/update — 更新具有给定 _id 字段值的特定书籍、bookinstance、类型或作者的表单(例如 /catalog/book/584493c1f4887f06c0e67d37/update)
  • catalog/<object>/<id>/delete — 删除具有给定 _id 字段值的特定书籍、bookinstance、类型或作者的表单(例如 /catalog/book/584493c1f4887f06c0e67d37/delete)

第一个主页和列表页不编码任何其他信息。虽然返回的结果将取决于模型类型和数据库中的内容,但获取信息的查询始终相同(类似地,对象创建运行的代码也将始终类似)。

相反,其他 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

控制器将使用 express-async-handler 模块,因此在继续之前,请使用 npm 将其安装到库中

bash
npm install express-async-handler

作者控制器

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

js
const Author = require("../models/author");
const asyncHandler = require("express-async-handler");

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

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

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

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

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

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

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

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

该模块首先需要我们稍后将用于访问和更新数据的 Author 模型,以及我们将用于捕获路由处理程序函数中抛出的任何异常的 asyncHandler 包装器。然后它为我们希望处理的每个 URL 导出函数。请注意,创建、更新和删除操作使用表单,因此还具有处理表单发布请求的其他方法——我们将在以后的“表单文章”中讨论这些方法。

所有函数都使用上面在路由函数中处理异常中描述的包装函数,并带有请求、响应和 next 的参数。这些函数响应一个字符串,指示关联的页面尚未创建。如果预期控制器函数接收路径参数,则这些参数会输出在消息字符串中(请参见上面的 req.params.id)。

请注意,一旦实现,某些路由函数可能不包含任何可能抛出异常的代码。当我们到达这些函数时,我们可以将它们更改回“普通”路由处理程序函数。

BookInstance 控制器

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

js
const BookInstance = require("../models/bookinstance");
const asyncHandler = require("express-async-handler");

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

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

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

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

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

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

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

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

类型控制器

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

js
const Genre = require("../models/genre");
const asyncHandler = require("express-async-handler");

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

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

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

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

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

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

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

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

书籍控制器

打开 /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");
});

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

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

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

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

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

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

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

// Handle book update on POST.
exports.book_update_post = asyncHandler(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");
const router = express.Router();

// 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");

/// 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("/", function (req, res) {
  res.redirect("/catalog");
});

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

更新 app.js

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

打开 app.js 并在其他路由下方需要 catalog 路由(添加下面显示的第三行,在文件中原有的另外两行下方)

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

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

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

注意:我们在路径 '/catalog' 下添加了我们的 catalog 模块。这将预先添加到 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://127.0.0.1:3000/
  • https://127.0.0.1:3000/catalog
  • https://127.0.0.1:3000/catalog/books
  • https://127.0.0.1:3000/catalog/bookinstances/
  • https://127.0.0.1:3000/catalog/authors/
  • https://127.0.0.1:3000/catalog/genres/
  • https://127.0.0.1:3000/catalog/book/5846437593935e2f8c2aa226
  • https://127.0.0.1:3000/catalog/book/create

总结

我们现在为我们的网站创建了所有路由,以及我们可以用完整实现填充的虚拟控制器函数,这些函数将在以后的文章中介绍。在此过程中,我们了解了许多关于 Express 路由、处理异常以及构建路由和控制器的某些方法的基本信息。

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

另请参阅