Express 教程第四部分:路由和控制器
在本教程中,我们将为 LocalLibrary 网站中最终需要的所有资源端点设置路由(URL 处理代码)和“虚拟”处理函数。完成后,我们将为路由处理代码提供一个模块化结构,我们可以在后续文章中用实际的处理函数来扩展它。我们还将对如何使用 Express 创建模块化路由有一个很好的理解!
预备知识 | 阅读 Express/Node 简介。完成之前的教程主题(包括 Express 教程第三部分:使用数据库(Mongoose))。 |
---|---|
目标 | 了解如何创建简单路由。设置我们所有的 URL 端点。 |
概述
在上一篇教程文章中,我们定义了 Mongoose 模型来与数据库交互,并使用一个(独立的)脚本来创建一些初始的库记录。现在我们可以编写代码来向用户展示这些信息。我们需要做的第一件事是确定我们希望能够在页面中显示哪些信息,然后定义用于返回这些资源的相应 URL。然后我们需要创建路由(URL 处理程序)和视图(模板)来显示这些页面。
下图提供了 HTTP 请求/响应处理时需要实现的数据流和主要事项的提醒。除了视图和路由之外,该图还显示了“控制器”——将路由请求的代码与实际处理请求的代码分离开来的函数。
由于我们已经创建了模型,因此我们主要需要创建的是
- “路由”将支持的请求(以及请求 URL 中编码的任何信息)转发到适当的控制器函数。
- 控制器函数,用于从模型中获取请求的数据,创建显示数据的 HTML 页面,并将其返回给用户在浏览器中查看。
- 控制器用于渲染数据的视图(模板)。
最终我们可能会有页面来显示图书、类型、作者和图书实例的列表和详细信息,以及创建、更新和删除记录的页面。在一篇文章中记录这么多内容是很困难的。因此,本文大部分内容将集中在设置我们的路由和控制器以返回“虚拟”内容上。我们将在后续文章中扩展控制器方法以处理模型数据。
下面的第一部分简要介绍了如何使用 Express 的 Router 中间件。然后我们将在以下部分设置 LocalLibrary 路由时使用这些知识。
路由入门
路由是 Express 代码的一个部分,它将 HTTP 动词(GET
、POST
、PUT
、DELETE
等)、URL 路径/模式以及一个用于处理该模式的函数关联起来。
有多种方法可以创建路由。在本教程中,我们将使用 express.Router
中间件,因为它允许我们将网站特定部分的路由处理程序分组在一起,并使用一个通用的路由前缀来访问它们。我们将所有与库相关的路由保存在“目录”模块中,如果添加处理用户帐户或其他功能的路由,我们可以将它们单独分组。
注意:我们在Express 简介 > 创建路由处理程序中简要讨论了 Express 应用程序路由。除了为模块化提供更好的支持(如下面第一小节所述)之外,使用 Router 与直接在 Express 应用程序对象上定义路由非常相似。
本节的其余部分概述了如何使用 Router
定义路由。
定义和使用单独的路由模块
以下代码提供了一个具体的示例,说明我们如何创建路由模块,然后将其用于 Express 应用程序。
首先,我们创建了一个名为 wiki.js 的模块中的 wiki 路由。代码首先导入 Express 应用程序对象,然后使用它获取一个 Router
对象,并使用 get()
方法向其添加了几个路由。最后,该模块导出了 Router
对象。
// 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'。
const wiki = require("./wiki.js");
// …
app.use("/wiki", wiki);
然后,我们 wiki 路由模块中定义的两个路由将可通过 /wiki/
和 /wiki/about/
访问。
路由函数
我们上面的模块定义了几个典型的路由函数。“about”路由(复制如下)是使用 Router.get()
方法定义的,该方法仅响应 HTTP GET 请求。此方法的第一个参数是 URL 路径,第二个参数是接收到带有该路径的 HTTP GET 请求时将调用的回调函数。
router.get("/about", (req, res) => {
res.send("About this wiki");
});
回调函数接受三个参数(通常按所示命名:req
、res
、next
),它们将包含 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 请求。
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
。我们可以如下所示提取此信息,其中包含 userId
和 bookId
路径参数
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 标识符的名称,包括空格、连字符、表情符号或任何其他字符,但您需要使用带引号的字符串定义它们,并使用括号表示法访问它们。例如
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/
之后的所有信息
app.get("/users/*example", (req, res) => {
// req.params would contain { "example": ["34", "books", "8989"]}
res.send(req.params);
});
可选部分
花括号可用于定义路径中的可选部分。例如,下面我们匹配带有任何扩展名(或没有扩展名)的文件名。
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 设置路由和控制器。
处理路由函数中的错误和异常
前面显示的路由函数都带有 req
和 res
参数,分别代表请求和响应。路由函数还会传递第三个参数 next
,其中包含一个回调函数,可以调用该函数将任何错误或异常传递给 Express 中间件链,最终它们将传播到您的全局错误处理代码。
从 Express 5 开始,如果路由处理程序返回的 Promise 随后被拒绝,则会自动使用拒绝值调用 next
;因此,在使用 Promise 时,路由函数中不需要错误处理代码。在使用异步基于 Promise 的 API 时,这会导致非常紧凑的代码,尤其是在使用 async
和 await
时。
例如,以下代码使用 find()
方法查询数据库,然后渲染结果。
exports.get("/about", async (req, res, next) => {
const successfulResult = await About.find({}).exec();
res.render("about_view", { title: "About", list: successfulResult });
});
以下代码展示了使用 Promise 链的相同示例。请注意,如果您愿意,您可以 catch()
错误并实现自己的自定义处理。
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 自动捕获并转发在同步代码中抛出的异常
app.get("/", (req, res) => {
// Express will catch this
throw new Error("SynchronousException");
});
但是,您必须 catch()
路由处理程序或中间件调用的异步代码中发生的异常。这些将不会被默认代码捕获
app.get("/", (req, res, next) => {
setTimeout(() => {
try {
// You must catch and propagate this error yourself
throw new Error("AsynchronousException");
} catch (err) {
next(err);
}
}, 100);
});
最后,如果您使用的是旧式的异步方法,这些方法在回调函数中返回错误或结果,那么您需要自己传播错误。以下示例展示了如何操作。
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 替换为每个模型(书、图书实例、类型、作者)的名称,objects 是 object 的复数形式,而 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 创建路由处理程序回调函数和路由代码。
创建路由处理回调函数
在定义路由之前,我们首先创建所有将调用的虚拟/骨架回调函数。这些回调将存储在 Book
、BookInstance
、Genre
和 Author
的独立“控制器”模块中(您可以使用任何文件/模块结构,但这似乎是本项目适当的粒度)。
首先,在项目根目录(/controllers)中为控制器创建一个文件夹,然后为每个模型创建单独的控制器文件/模块
/express-locallibrary-tutorial # the project root /controllers authorController.js bookController.js bookinstanceController.js genreController.js
作者控制器
打开 /controllers/authorController.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
控制器模块遵循相同的模式)
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 文件并复制以下文本(这与 Author
和 BookInstance
文件遵循相同的模式)
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()
函数
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 文件夹,其中包含 index 和 users 的路由。在此文件夹内创建另一个路由文件 — catalog.js — 如下图所示。
/express-locallibrary-tutorial # the project root /routes index.js users.js catalog.js
打开 /routes/catalog.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 并用下面的函数替换现有路由。
// GET home page.
router.get("/", (req, res) => {
res.redirect("/catalog");
});
注意:这是我们第一次使用 redirect() 响应方法。此方法重定向到指定的页面,默认情况下发送 HTTP 状态码“302 Found”。如果需要,您可以更改返回的状态码,并提供绝对或相对路径。
更新 app.js
最后一步是将路由添加到中间件链中。我们在 app.js
中完成此操作。
打开 app.js 并在其他路由下方引入目录路由(添加下面显示的第三行,在文件中已有的另外两行下方)
const indexRouter = require("./routes/index");
const usersRouter = require("./routes/users");
const catalogRouter = require("./routes/catalog"); // Import routes for "catalog" area of site
接下来,将目录路由添加到其他路由下方的中间件堆栈中(添加下面显示的第三行,在文件中已有的另外两行下方)
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,则可以使用
bashnpm 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 路由、异常处理以及构建路由和控制器的一些方法的基本信息。
在下一篇文章中,我们将使用视图(模板)和模型中存储的信息为网站创建一个合适的欢迎页面。