Express Tutorial Part 4: Routes and controllers
先决条件: | 阅读 Express /节点简介。 完成上一篇教程主题(包括快速教程第3部分:使用数据库(使用Mongoose))。 |
---|---|
目的: | 了解如何创建简单的路由。 设置所有的URL端点。 |
概述
在上一篇教程文章中,我们定义了 Mongoose 模型以与数据库进行交互,并使用(独立)脚本创建一些初始 图书馆记录。 我们现在可以编写代码以向用户呈现该信息。 我们需要做的第一件事是确定我们希望在我们的页面中显示哪些信息,然后定义返回这些资源的适当的URL。 然后,我们需要创建路由(URL处理程序)和视图(模板)来显示这些页面。
下面的图提供了处理HTTP请求/响应时需要实现的数据和事物的主要流程的提示。 除了视图和路由之外,图中还显示了"controllers" - 分离出代码以从实际处理请求的代码中路由请求的函数。
由于我们已经创建了模型,我们需要创建的主要内容是:
- "Routes" to forward the supported requests (and any information encoded in request URLs) to the appropriate controller functions.
- Controller functions to get the requested data from the models, create an HTML page displaying the data, and return it to the user to view in the browser.
- Views (templates) used by the controllers to render the data.
最终,我们可能有页面来显示书籍,流派,作者和bookinstances的列表和详细信息,以及创建,更新和删除记录的页面。 这是一篇文章在一篇文章。 因此,本文大部分将集中在设置我们的路由和控制器返回"虚拟"内容。 我们将在后续文章中扩展控制器方法以使用模型数据。
下面的第一部分提供了有关如何使用Express"路由器"中间件的简要"初级"。 然后,当我们设置LocalLibrary路由时,我们将在以下部分使用该知识。
路由引物
路由是Express代码的一部分,其将HTTP动词( GET
, POST
, PUT
, DELETE
等等),URL路径/模式,以及被调用以处理该模式的函数。
有几种方法来创建路由。 在本教程中,我们将使用 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, next) {
res.send('Wiki home page')
})
// About page route
router.get('/about', function (req, res, next) {
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()
方法定义"约"路由(如下所述),该方法仅响应HTTP GET请求。 此方法的第一个参数是URL路径,而第二个是回调函数,如果接收到具有路径的HTTP GET请求,将调用该函数。
router.get('/about', function (req, res, next) {
res.send('About this wiki')
})
回调需要三个参数(通常如下所示: req
, res
, next
),它将包含HTTP请求对象,HTTP响应, 和中间件链中的下一个功能。
此处的回调函数调用 send()
当我们收到带有路径(\' / about\'
)的GET请求时,返回字符串"About this wiki"的响应。 有用于结束请求/响应周期的其他响应方法数 。 例如,您可以调用 res.json() / code>发送JSON响应或
发送文件。 我们在构建库时最常使用的响应方法是 render (),它使用模板和数据创建和返回HTML文件 - 我们将在后面的文章中进一步讨论这一点! res.sendFile()
a>
HTTP动词
上面的示例路由使用 Router.get()
方法来响应具有某个路径的HTTP GET请求。
Router
还为所有其他HTTP动词提供路由方法,大多数使用方式完全相同: post()
, put
>, delete()
, options()
, trace()
(), mkcol()
, move()
, proppatch()
, unlock()
, checkout()
, merge()
, m-
/ code>, subscribe()
, unsubscribe()
code> connect()。
例如,下面的代码与前面的 / about
路由一样,但只响应HTTP POST请求。
router.post('/about', function (req, res, next) {
res.send('About this wiki')
})
路由路径
路由路径定义可以进行请求的端点。 到目前为止,我们看到的例子只是字符串,并且使用的是完全一样的:\'/\',\'/ about\',\'/ book\',\'/any-random.path\'。
路由路径也可以是字符串模式。 字符串模式使用正则表达式语法的子集来定义要匹配的端点的模式。 下面列出了子集(注意,连字符( -
)和点(。
)由字符串路径解释):
- ? : The endpoint must have 0 or more of the preceding character. E.g. a route path of
'/ab?cd'
will match endpointsacd
,abcd
,abbcd
etc. - + : The endpoint must have 1 or more of the preceding character. E.g. a route path of
'/ab+cd'
will match endpointsabcd
,abbcd
,abbbcd
, and so on. - * : The endpoint may have an arbitrary string where the * character is placed. E.g. a route path of
'ab*cd'
will match endpointsabcd
,abXcd
,abSOMErandomTEXTcd
, and so on. - () : Grouping match on a set of characters to perform another operation on. E.g.
'/ab(cd)?e'
will peform a ? match on (cd) —it will matchabe
,abcde
,abcdcde
, and so on.
路径路径也可以是JavaScript 正则表达式。 例如,下面的路由路径将匹配 catfish
和 dogfish
匹配,但不匹配 catflap
, catfishhead
上。 请注意,正则表达式的路径使用正则表达式语法(它不是以前情况下的带引号的字符串)。
app.get(/.*fish$/, function (req, res) {
...
})
注意:LocalLibrary的大多数路由只使用字符串,而不是字符串模式和正则表达式。 我们还将使用下一节中讨论的路由参数。
路由参数
路线参数是用于捕获在网址中在其位置处指定的值的命名的网址片段。 命名段的前缀为冒号,然后是名称(例如 / : your_parameter_name /
。)捕获的值存储在 req.params
对象使用参数名作为键(例如 req.params.your_parameter_name
)。
例如,假设一个编码为包含用户和书籍信息的URL: http:// localhost:3000 / users / 34 / books / 8989
。 我们可以使用 userId
和 bookId
路径参数来提取此信息:
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和_)组成。
注意: / book / create 的网址将通过 / book /:bookId
(将提取"bookId" of\' create
\')。 将使用与传入URL匹配的第一个路由,因此如果您要分别处理 URL,则必须在您的
/ book /:bookId 代码>路由。
这就是开始使用路由所需要的 - 如果需要,您可以在Express文档中找到更多信息: >基本路由和路由指南。 以下部分显示了如何为LocalLibrary设置路由和控制器。
LocalLibrary所需的路径
下面列出了我们最终需要用于我们网页的网址,其中对象由每个模型的名称(book,bookinstance,genre,author), 是对象的复数, 是默认情况下给每个Mongoose模型实例赋予的唯一实例字段( _id
)。
-
catalog/
— The home/index page. -
catalog/<objects>/
— The list of all books, bookinstances, genres, or authors (e.g. /catalog/books/
, /catalog/genres/
, etc.) -
catalog/<object>/<id>
— The detail page for a specific book, bookinstance, genre, or author with the given_id
field value (e.g./catalog/book/584493c1f4887f06c0e67d37)
. -
catalog/<object>/create
— The form to create a new book, bookinstance, genre, or author (e.g./catalog/book/create)
. -
catalog/<object>/<id>/update
— The form to update a specific book, bookinstance, genre, or author with the given_id
field value (e.g./catalog/book/584493c1f4887f06c0e67d37/update)
. -
catalog/<object>/<id>/delete
— The form to delete a specific book, bookinstance, genre, author with the given_id
field value (e.g./catalog/book/584493c1f4887f06c0e67d37/delete)
.
第一主页和列表页不编码任何附加信息。 虽然返回的结果将取决于模型类型和数据库中的内容,但运行以获取信息的查询将始终相同(类似地,用于对象创建的代码运行总是类似的)。
相反,其他URL用于对特定文档/模型实例进行操作 - 这些URL对URL中的项目的标识进行编码(如上所示的
。 我们将使用路径参数提取编码的信息,并将其传递给路由处理程序(在后面的文章中,我们将使用它来动态确定从数据库获取什么信息)。 通过在我们的URL中编码信息,我们只需要一个特定类型的每个资源的一个路由(例如一个路由来处理每个单个书项目的显示)。
注意:Express允许您以任何方式构建网址 - 您可以按上面所示对网址正文中的信息进行编码,或使用URL GET
参数(例如 > / book /?id = 6
)。 无论您使用哪种方法,网址都应保持干净,逻辑和可读(此处查看W3C建议 / a>)。
接下来,我们为所有上述URL创建我们的路由处理程序回调函数和路由代码。
创建路由处理程序回调函数
在我们定义我们的路由之前,我们首先创建他们将调用的所有dummy / skeleton回调函数。 回调将存储在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, next) { res.send('NOT IMPLEMENTED: Author list'); }; // Display detail page for a specific Author exports.author_detail = function(req, res, next) { res.send('NOT IMPLEMENTED: Author detail: ' + req.params.id); }; // Display Author create form on GET exports.author_create_get = function(req, res, next) { res.send('NOT IMPLEMENTED: Author create GET'); }; // Handle Author create on POST exports.author_create_post = function(req, res, next) { res.send('NOT IMPLEMENTED: Author create POST'); }; // Display Author delete form on GET exports.author_delete_get = function(req, res, next) { res.send('NOT IMPLEMENTED: Author delete GET'); }; // Handle Author delete on POST exports.author_delete_post = function(req, res, next) { res.send('NOT IMPLEMENTED: Author delete POST'); }; // Display Author update form on GET exports.author_update_get = function(req, res, next) { res.send('NOT IMPLEMENTED: Author update GET'); }; // Handle Author update on POST exports.author_update_post = function(req, res, next) { res.send('NOT IMPLEMENTED: Author update POST'); };
该模块首先需要我们稍后将使用的模型来访问和更新我们的数据。 然后它导出我们希望处理的每个URL的函数(创建,更新和删除操作使用表单,因此还有其他方法来处理表单发布请求 - 稍后我们将在"表单文章"中讨论这些方法 )。
所有函数都具有 Express中间件函数的标准形式,如果方法未完成请求,则调用请求,响应和 next
函数的参数 循环(在所有这些情况下,它!)。 这些方法只返回一个字符串,表示相关的页面尚未创建。 如果控制器函数期望接收路径参数,则这些包括在消息字符串中。
BookInstance controller
打开 /controllers/bookinstanceController.js 文件,并在以下代码中复制(与 Author
控制器模块具有相同的模式):
var BookInstance = require('../models/bookinstance'); // Display list of all BookInstances exports.bookinstance_list = function(req, res, next) { res.send('NOT IMPLEMENTED: BookInstance list'); }; // Display detail page for a specific BookInstance exports.bookinstance_detail = function(req, res, next) { res.send('NOT IMPLEMENTED: BookInstance detail: ' + req.params.id); }; // Display BookInstance create form on GET exports.bookinstance_create_get = function(req, res, next) { res.send('NOT IMPLEMENTED: BookInstance create GET'); }; // Handle BookInstance create on POST exports.bookinstance_create_post = function(req, res, next) { res.send('NOT IMPLEMENTED: BookInstance create POST'); }; // Display BookInstance delete form on GET exports.bookinstance_delete_get = function(req, res, next) { res.send('NOT IMPLEMENTED: BookInstance delete GET'); }; // Handle BookInstance delete on POST exports.bookinstance_delete_post = function(req, res, next) { res.send('NOT IMPLEMENTED: BookInstance delete POST'); }; // Display BookInstance update form on GET exports.bookinstance_update_get = function(req, res, next) { res.send('NOT IMPLEMENTED: BookInstance update GET'); }; // Handle bookinstance update on POST exports.bookinstance_update_post = function(req, res, next) { res.send('NOT IMPLEMENTED: BookInstance update POST'); };
Genre controller
打开 /controllers/genreController.js 文件,并在以下文本中复制(与 Author
和 BookInstance
文件格式相同):
var Genre = require('../models/genre'); // Display list of all Genre exports.genre_list = function(req, res, next) { res.send('NOT IMPLEMENTED: Genre list'); }; // Display detail page for a specific Genre exports.genre_detail = function(req, res, next) { res.send('NOT IMPLEMENTED: Genre detail: ' + req.params.id); }; // Display Genre create form on GET exports.genre_create_get = function(req, res, next) { res.send('NOT IMPLEMENTED: Genre create GET'); }; // Handle Genre create on POST exports.genre_create_post = function(req, res, next) { res.send('NOT IMPLEMENTED: Genre create POST'); }; // Display Genre delete form on GET exports.genre_delete_get = function(req, res, next) { res.send('NOT IMPLEMENTED: Genre delete GET'); }; // Handle Genre delete on POST exports.genre_delete_post = function(req, res, next) { res.send('NOT IMPLEMENTED: Genre delete POST'); }; // Display Genre update form on GET exports.genre_update_get = function(req, res, next) { res.send('NOT IMPLEMENTED: Genre update GET'); }; // Handle Genre update on POST exports.genre_update_post = function(req, res, next) { res.send('NOT IMPLEMENTED: Genre update POST'); };
Book controller
打开 /controllers/bookController.js 文件,并在以下代码中复制。 它遵循与其他控制器模块相同的模式,但另外还具有用于显示站点欢迎页面的 index()
函数:
var Book = require('../models/book'); exports.index = function(req, res, next) { res.send('NOT IMPLEMENTED: Site Home Page'); }; // Display list of all books exports.book_list = function(req, res, next) { res.send('NOT IMPLEMENTED: Book list'); }; // Display detail page for a specific book exports.book_detail = function(req, res, next) { res.send('NOT IMPLEMENTED: Book detail: ' + req.params.id); }; // Display book create form on GET exports.book_create_get = function(req, res, next) { res.send('NOT IMPLEMENTED: Book create GET'); }; // Handle book create on POST exports.book_create_post = function(req, res, next) { res.send('NOT IMPLEMENTED: Book create POST'); }; // Display book delete form on GET exports.book_delete_get = function(req, res, next) { res.send('NOT IMPLEMENTED: Book delete GET'); }; // Handle book delete on POST exports.book_delete_post = function(req, res, next) { res.send('NOT IMPLEMENTED: Book delete POST'); }; // Display book update form on GET exports.book_update_get = function(req, res, next) { res.send('NOT IMPLEMENTED: Book update GET'); }; // Handle book update on POST exports.book_update_post = function(req, res, next) { res.send('NOT IMPLEMENTED: Book update POST'); };
创建目录路由模块
接下来,我们为LocalLibrary网站所需的所有网址创建路线,这将调用我们在上一节中定义的控制器函数。
骨架已经有 ./ routes 文件夹,其中包含 index 和用户的路线。 在此文件夹中创建另一个路线文件 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。
处理程序函数都是从我们在上一节中创建的控制器模块导入的。
更新索引路由模块
我们已经设置了所有的新路线,但我们仍然有一条到原始页面的路线。 让我们将它重定向到我们在路径\'/ 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 index = require('./routes/index'); var users = require('./routes/users'); var catalog = require('./routes/catalog'); //Import routes for "catalog" area of site
接下来,将目录路由添加到中间件堆栈下面的其他路由,如下面粗体显示:
app.use('/', index); app.use('/users', users); app.use('/catalog', catalog); // Add catalog routes to middleware chain.
注意:我们已将目录模块添加到路径\'/ catalog\'
。 这是在目录模块中定义的所有路径的前面。 因此,例如,要访问图书列表,URL将是: / catalog / books /
。
而已。 我们现在应该为我们最终将在LocalLibrary网站上支持的所有URL启用路由和骨干功能。
测试路由
要测试路线,首先使用您常用的方法启动网站
- The default method
//Windows SET DEBUG=express-locallibrary-tutorial:* & npm start // Mac OS or Linux DEBUG=express-locallibrary-tutorial:* npm start
- If you previously set up nodemon, you can instead use:
//Windows SET DEBUG=express-locallibrary-tutorial:* & npm run devstart // Mac OS or Linux
DEBUG=express-locallibrary-tutorial:* npm run devstart
然后导航到多个LocalLibrary网址,并验证您没有收到错误页面(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路由的基本信息,以及一些用于构造我们的路由和控制器的方法。
在下一篇文章中,我们将使用视图(模板)和存储在模型中的信息为网站创建一个适当的欢迎页面。
也可以看看
- Basic routing (Express docs)
- Routing guide (Express docs)