在本教程中,我们将为最终在 本地图书馆 网站中需要的所有资源端点,搭配 "空壳" 处理函式来配置路由 (URL handling code) 。完成后,我们的路由处理源码将会有模组化结构,在接下来的文章中,我们可以用真实的处理函式加以扩充。我们也会对如何使用Express创建模组化路由,有更好的理解。

前置条件: 阅读 Express/Node 介紹。 完成先前教程主题 (包含 Express 教程 3: 使用数据库 (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.

var express = require('express');
var 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(),将路由器添加到中间件处理路径,并指定一个 'wiki' 的 URL 路径。

var wiki = require('./wiki.js');
// ...
app.use('/wiki', wiki);

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

路由函数

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

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

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

注意: 路由器功能是Express中间件,这意味着它们必须完成(响应)请求或调用链中的下一个功能next。在上面的例子中,我们使用send()完成了请求,所以下一个参数next没有被使用(我们选择不指定它)。

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

这里的回调函数,在响应中调用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', function (req, res) {
  res.send('About this wiki');
})

路由路径

路由路径定义可以进行请求的端点。我们到目前为止看到的例子,都是字符串,并且完全按照字符串的写法使用:'/','/ about','/ book','/any-random.path'。

路由路径也可以是字符串模式。字符串模式使用正则表达式语法的子集,来定义将匹配的端点模式。下面列出了子集(请注意,连字符( -)和点(.)由字符串路径字面解释):

  • ? : 端点在 ? 号前面的那个字符,必须为0个或1个。例如。 '/ab?cd'的路径路径将匹配端点 acd abcd
  • + : 端点在+号前面的那个字符,必须为1个或多个。例如,'/ab+cd'的路径路径将与端点abcdabbcdabbbcd等匹配。
  • * :  端点在放置*字符的地方,可以代换为任意字符串。例如。 'ab\*cd' 的路由路径,将匹配端点abcd, abXcd, abSOMErandomTEXTcd等。
  • () : 将一组字符进行匹配,以执行上面三个操作。例如。 '/ab(cd)?e',表示以?号对(cd)进行匹配 - 它会匹配 abeabcde。(译注:即(cd)必须为0个或1个。若为0,匹配 abe。若为1,匹配 abcde

路由路径也可以是 JavaScript 正则表达式。例如,下面的路由路径将与鲶鱼catfish 和角鲨鱼dogfish相匹配,但不包括鲶鱼catflap、鲶鱼头catfishhead等。请注意,正则表达式的路径使用正则表达式语法(它不像以前那样,是带引号的字符串)。

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

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

路由参数

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

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

app.get('/users/:userId/books/:bookId', function (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 之类的路由匹配(它将提取要创建'create' 的 “bookId” 值)。将使用与传入URL匹配的第一个路由,因此,如果要单独处理/book/createURL,则必须在/book/:bookId路由之前,先定义其路由处理程序。

这就是您开始使用路由所需的全部内容 - 如果需要,您可以在Express文档中找到更多信息:基本路由路由指南。以下部分显示了我们如何为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,创建路由处理程序回调函数和路由代码。

创建路由-handler回调函式

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

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

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

作者控制器

打开/controllers/authorController.js 文件,并复制以下代码:

var Author = require('../models/author');

// Display list of all Authors.
exports.author_list = function(req, res) {
    res.send('NOT IMPLEMENTED: Author list');
};

// Display detail page for a specific Author.
exports.author_detail = function(req, res) {
    res.send('NOT IMPLEMENTED: Author detail: ' + req.params.id);
};

// Display Author create form on GET.
exports.author_create_get = function(req, res) {
    res.send('NOT IMPLEMENTED: Author create GET');
};

// Handle Author create on POST.
exports.author_create_post = function(req, res) {
    res.send('NOT IMPLEMENTED: Author create POST');
};

// Display Author delete form on GET.
exports.author_delete_get = function(req, res) {
    res.send('NOT IMPLEMENTED: Author delete GET');
};

// Handle Author delete on POST.
exports.author_delete_post = function(req, res) {
    res.send('NOT IMPLEMENTED: Author delete POST');
};

// Display Author update form on GET.
exports.author_update_get = function(req, res) {
    res.send('NOT IMPLEMENTED: Author update GET');
};

// Handle Author update on POST.
exports.author_update_post = function(req, res) {
    res.send('NOT IMPLEMENTED: Author update POST');
};

该模块首先导入我们稍后将使用的模型,来访问和更新我们的数据。然后它为我们希望处理的每个URL,导出函数(创建,更新和删除操作使用表单,因此还有其他方法,来处理表单发布请求 - 我们将在稍后的 “表单文章” 中讨论这些方法)。

所有函数都具有 Express 中间件函数的标准形式,如果方法没有完成请求周期,则会调用请求,响应和next 下一个函数的参数(在所有这些情况下,它都会执行!)。这些方法只返回一个字符串,表明尚未创建关联的页面。如果期望控制器函数接收路径参数,则在消息字符串中,输出这些参数(参见上面的 req.params.id )。

书本实例控制器

打开 /controllers/bookinstanceController.js 文件,并将其复制到以下代码中(它遵循与Author 控制器模块相同的模式):

var BookInstance = require('../models/bookinstance');

// Display list of all BookInstances.
exports.bookinstance_list = function(req, res) {
    res.send('NOT IMPLEMENTED: BookInstance list');
};

// Display detail page for a specific BookInstance.
exports.bookinstance_detail = function(req, res) {
    res.send('NOT IMPLEMENTED: BookInstance detail: ' + req.params.id);
};

// Display BookInstance create form on GET.
exports.bookinstance_create_get = function(req, res) {
    res.send('NOT IMPLEMENTED: BookInstance create GET');
};

// Handle BookInstance create on POST.
exports.bookinstance_create_post = function(req, res) {
    res.send('NOT IMPLEMENTED: BookInstance create POST');
};

// Display BookInstance delete form on GET.
exports.bookinstance_delete_get = function(req, res) {
    res.send('NOT IMPLEMENTED: BookInstance delete GET');
};

// Handle BookInstance delete on POST.
exports.bookinstance_delete_post = function(req, res) {
    res.send('NOT IMPLEMENTED: BookInstance delete POST');
};

// Display BookInstance update form on GET.
exports.bookinstance_update_get = function(req, res) {
    res.send('NOT IMPLEMENTED: BookInstance update GET');
};

// Handle bookinstance update on POST.
exports.bookinstance_update_post = function(req, res) {
    res.send('NOT IMPLEMENTED: BookInstance update POST');
};

种类控制器

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

var Genre = require('../models/genre');

// Display list of all Genre.
exports.genre_list = function(req, res) {
    res.send('NOT IMPLEMENTED: Genre list');
};

// Display detail page for a specific Genre.
exports.genre_detail = function(req, res) {
    res.send('NOT IMPLEMENTED: Genre detail: ' + req.params.id);
};

// Display Genre create form on GET.
exports.genre_create_get = function(req, res) {
    res.send('NOT IMPLEMENTED: Genre create GET');
};

// Handle Genre create on POST.
exports.genre_create_post = function(req, res) {
    res.send('NOT IMPLEMENTED: Genre create POST');
};

// Display Genre delete form on GET.
exports.genre_delete_get = function(req, res) {
    res.send('NOT IMPLEMENTED: Genre delete GET');
};

// Handle Genre delete on POST.
exports.genre_delete_post = function(req, res) {
    res.send('NOT IMPLEMENTED: Genre delete POST');
};

// Display Genre update form on GET.
exports.genre_update_get = function(req, res) {
    res.send('NOT IMPLEMENTED: Genre update GET');
};

// Handle Genre update on POST.
exports.genre_update_post = function(req, res) {
    res.send('NOT IMPLEMENTED: Genre update POST');
};

书本控制器

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

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

exports.index = function(req, res) {
    res.send('NOT IMPLEMENTED: Site Home Page');
};

// Display list of all books.
exports.book_list = function(req, res) {
    res.send('NOT IMPLEMENTED: Book list');
};

// Display detail page for a specific book.
exports.book_detail = function(req, res) {
    res.send('NOT IMPLEMENTED: Book detail: ' + req.params.id);
};

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

// Handle book create on POST.
exports.book_create_post = function(req, res) {
    res.send('NOT IMPLEMENTED: Book create POST');
};

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

// Handle book delete on POST.
exports.book_delete_post = function(req, res) {
    res.send('NOT IMPLEMENTED: Book delete POST');
};

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

// Handle book update on POST.
exports.book_update_post = function(req, res) {
    res.send('NOT IMPLEMENTED: Book update POST');
};

创建目录路由模组

接下来,我们为 LocalLibrary 网站,创建所需全部 URL 的路由,这将调用我们在上一节中定义的控制器功能。

骨架网站已经有一个 ./routes文件夹,其中包含索引和用户的路由。在此文件夹中,创建另一个路径文件 — catalog.js — 如下图所示。

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

打开 /routes/catalog.js ,复制下面的代码:

var express = require('express');
var router = express.Router();

// Require controller modules.
var book_controller = require('../controllers/bookController');
var author_controller = require('../controllers/authorController');
var genre_controller = require('../controllers/genreController');
var 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。

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

更新 index 路由模組

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

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

// GET home page.
router.get('/', function(req, res) {
  res.redirect('/catalog');
});

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

更新 app.js

最后一步,是将路由,添加到中间件链。我们在app.js这样做。

打开 app.js,并要求其他路由下方的目录路由(添加下面显示的第三行,在其他两个路由下面):

var indexRouter = require('./routes/index');
var usersRouter = require('./routes/users');
var 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.

Note:  我们已在路径'/catalog'中添加了目录模块。它预先添加到目录模块中定义的所有路径。例如,要访问书本列表,URL将为:/catalog/books/

就是这样。现在应该为我们最终在 LocalLibrary 网站上支持的所有URL,启用路由和框架功能。

测试路由

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

  • 默认方法
    // Windows
    SET DEBUG=express-locallibrary-tutorial:* & npm start
    
    // macOS or Linux
    DEBUG=express-locallibrary-tutorial:* npm start
    
  • 如果您以前设置了nodemon,,则可以使用:
    // Windows
    SET DEBUG=express-locallibrary-tutorial:* & npm run devstart
    
    // macOS or Linux
    DEBUG=express-locallibrary-tutorial:* npm run devstart
    

然后浏览一些上面的 LocalLibrary URL,并验证您没有收到错误页面(HTTP 404)。为方便起见,下面列出了一小组网址:

总结

我们现在为网站创建了所有的路由,在稍后的教程,我们可以将实作完成的代码,填入到空壳控制器函式。以这样的方式,我们学到了许多关于 Express 路由的基本信息,以及一些组织路由和控制器的方式。

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

参见

 

本系列教程

 

文档标签和贡献者

此页面的贡献者: edgar-chen
最后编辑者: edgar-chen,