Express 教程 4: 路由和控制器 - 学习 Web 开发 编辑
本节将为 LocalLibrary 站点中所需的资源端点(Endpoint)配置路由。先用空的处理函数搭建起路由处理的模块结构(下节会将它们扩充为真实的处理函数)。并详细介绍了 Express 路由模块的创建方法。
预备知识: | 回顾 Express/Node 入门。 完成本教程之前小节(Express 教程 3:使用数据库 (Mongoose) 等)。了解服务器端编程,了解正则表达式。 |
---|---|
学习目标: | 理解简单路由的创建方法。设置示例中所有 URL 端点。 |
概览
上节 定义了与数据库交互的 Mongoose 模型,使用一个(独立)脚本创建了一些初始记录。现在可以编写代码来向用户展示这些信息。首先要确定页面中应显示哪些信息,然后定义适当的 URL 来返回这些资源。随后应创建路由(URL 处理器)和视图(模板)来显示这些页面。
下图展示了 HTTP 请求/响应处理的主数据流和需要实现的行为。图中除视图(View)和路由(Route)外,还展示了控制器(Controller),它们是实际的请求处理函数,与路由请求代码是分开的。
模型已经创建,现在要创建的主要是:
- 路由:把需要支持的请求(以及请求 URL 中包含的任何信息)转发到适当的控制器函数。
- 控制器:从模型中获取请求的数据,创建一个 HTML 页面显示出数据,并将页面返回给用户,以便在浏览器中查看。
- 视图(模板):供控制器用来渲染数据。
因此我们需要页面来显示藏书、藏书种类、作者、藏书副本的列表和详细信息,还需要页面来创建、更新和删除记录。这些内容对于本节来说不算少,因此本节将主要集中在路由和控制器设置。本节编写的这些函数都只有框架,后续章节再扩展控制器方法,以使用模型数据。
第一段提供了 Express 的 Router 中间件的“入门”知识。后续设置 LocalLibrary 路由时将用到这些知识。
路由入门
路由是一段 Express 代码,它将 HTTP 动词(GET
、POST
、PUT
、DELETE
等)、URL 路径/模式和处理函数三者关联起来。
创建路由有几种方法。本教程将使 express.Router
中间件,因为使用它可以将站点特定部分的路由处理程序打包,并使用通用路由前缀访问它们。我们会将所有与图书馆有关的路由保存在 catalog
模块中,在添加处理帐户或其他功能的路由时,可以分开保存。
注:Express 简介 > 创建路由处理程序 简要讨论了 Express 应用的路由机制。使用 Router
可以保证更好的模块化(下文所述),且用法与直接在 Express 应用对象定义路由非常类似。
本段以下内容介绍使用 Router
定义路由的方法。
定义和使用单独的路由模块
以下代码举例说明了如何创建路由模块,以及如何在 Express 应用中使用它。
首先,在 wiki.js 模块中创建一个维基路由。代码一开始导入 Express 应用对象,用它取得一个 Router
对象,然后用 get()
方法向其添加两个具体的路由。模块的最后导出该 Router
对象。
// wiki.js - 维基路由模块
const express = require('express');
const router = express.Router();
// 主页路由
router.get('/', (req, res) => {
res.send('维基主页');
});
// “关于页面”路由
router.get('/about', (req, res) => {
res.send('关于此维基');
});
module.exports = router;
注:上面的路由处理回调直接定义在了路由函数中。LocalLibrary 的回调将定义在单独的控制器模块中。
要在主应用中使用该路由模块,首先应 require
它(wiki.js),然后对 Express 应用对象调用 use()
(指定路径‘/wiki’),即可将其添加到中间件处理路径。
const wiki = require('./wiki.js');
// ...
app.use('/wiki', wiki);
这时 wiki
模块中定义的两个路由就可以从 /wiki/
和 /wiki/about/
访问了。
路由函数
上述模块定义了两个典型的路由函数。Router.get()
方法定义的 “about” 路由(下方重现的)仅响应 HTTP GET 请求。第一个参数是 URL 路径,第二个参数是一个回调,在收到带有路径的HTTP GET 请求将调用之。
router.get('/about', (req, res) => {
res.send('关于此维基');
});
该回调有三个参数(通常命名为:req
、res
、next
),分别是:HTTP 请求对象、HTTP 响应、中间件链中的下一个函数。
注:路由函数就是 Express 中间件,这意味着它们必须(通过响应)结束请求,否则必须调用链中的 next
函数。上述示例使用send()
完成了请求,所以没有使用 next
参数(参数表中将其省略)。
上述路由函数只需要一个回调,可以根据需要指定任意数量的回调参数,或一个回调函数数组。每个函数都将加入中间件链,并且将按添加顺序调用(若有回调完成请求则中止当前周期)。
此处的回调对响应对象调用 send()
,当收到带有路径('/about'
)的 GET 请求时将返回字符串“关于此维基”。还有许多其它可以结束请求/响应周期 响应方法,例如,可调用 res.json()
来发送 JSON 响应,或调用 res.sendFile()
来发送文件。构建 LocalLibrary 最常使用的响应方法是 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'、'/ book',等等。
路由路径也可以是字符串模式(String Pattern)。可用部分正则表达式语法来定义端点的模式。以下是所涉及的正则表达式(注意,连字符( -
)和点(.
)在字符串路径中解释为字面量,不能做为正则表达式):
?
:问号之前的一个字符只能出现零次或一次。例如,路由路径'/ab?cd'
路径匹配端点acd
或abcd
。+
:加号之前的一个字符至少出现一次。例如,路径路径'/ab+cd'
匹配端点abcd
、abbcd
、abbbcd
,等。*
:星号可以替换为任意字符串。例如,路由路径'ab\*cd'
匹配端点abcd
、abXcd
、abSOMErandomTEXTcd
,等。()
:将一个字符串视为一体以执行?
、+
、*
操作。例如。'/ab(cd)?e'
将对(cd)
进行匹配,将匹配到abe
和abcde
。
路由路径也可以是 JavaScript 正则表达式。例如,下面的路由路径将匹配 catfish
和 dogfish
,但不会匹配 catflap
、catfishhead
等。注意,正则表达式路径不再用引号 "..."
括起来,而是正则表达式语法 /.../
。
app.get(/.*fish$/, (req, res) => {
...
});
注:LocalLibrary 的大部分路由都只使用字符串,很少用字符串模式和正则表达式。接下来将讨论“路由参数”。
路由参数
路径参数是命名的 URL 段,用于捕获在 URL 中的位置指定的值。命名段以冒号为前缀,然后是名称(例如 /: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', (req, res) => {
// 通过 req.params.userId 访问 userId
// 通过 req.params.bookId 访问 bookId
res.send(req.params);
});
路由参数名必须由“单词字符”(/[A-Za-z0-9_]
/)组成。
注:URL /book/create
会匹配 /book/:bookId
这样的路由(将提取值为'create
' 的 'bookId
')。第一个与传入 URL 相匹配的路由会被使用,因此 /book/create
的路由处理器必须定义在 /book/:bookId
路由之前,才能妥善处理。
以上就是使用路由所有的预备知识。Express 文档中还能找到更多信息:基础路由 和 路由指南。以下是 LocalLibrary 路由和控制器的设置过程。
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 创建路由处理器回调函数和路由代码。
创建路由处理器回调函数
定义路由之前应先使用单独的“控制器”模块创建回调的结构骨架。(文件/模块结构没有限制,但以下结构很适合当前项目的规模)。
首先在项目根目录新建一个存放控制器的文件夹(/controllers),然后为每个模型创建单独的控制器文件(模块):
/express-locallibrary-tutorial // 项目根目录 /controllers authorController.js bookController.js bookinstanceController.js genreController.js
译注:上述四个文件可到 GitHub 下载 dummyControllers.zip。(链接已失效,请移步英文版查看具体代码)
Author 控制器
以下是 /controllers/authorController.js 文件的内容:
const Author = require('../models/author');
// 显示完整的作者列表
exports.author_list = (req, res) => { res.send('未实现:作者列表'); };
// 为每位作者显示详细信息的页面
exports.author_detail = (req, res) => { res.send('未实现:作者详细信息:' + req.params.id); };
// 由 GET 显示创建作者的表单
exports.author_create_get = (req, res) => { res.send('未实现:作者创建表单的 GET'); };
// 由 POST 处理作者创建操作
exports.author_create_post = (req, res) => { res.send('未实现:创建作者的 POST'); };
// 由 GET 显示删除作者的表单
exports.author_delete_get = (req, res) => { res.send('未实现:作者删除表单的 GET'); };
// 由 POST 处理作者删除操作
exports.author_delete_post = (req, res) => { res.send('未实现:删除作者的 POST'); };
// 由 GET 显示更新作者的表单
exports.author_update_get = (req, res) => { res.send('未实现:作者更新表单的 GET'); };
// 由 POST 处理作者更新操作
exports.author_update_post = (req, res) => { res.send('未实现:更新作者的 POST'); };
该模块首先导入了用于访问和更新数据的模型,然后为每个需要处理(添加、更新和删除表单,以及相应的 POST 请求,稍后在 使用表单 一节中讲解)的 URL 导出一个函数。
所有函数都遵循 Express 中间件函数的标准形式,三个参数依次为:请求 req
、响应 res
、当前方法无法完成请求循环时(这里不存在这种情况)调用的 next
函数。上述方法只返回一个字符串,显示相关页面尚未创建。接收路径参数的控制器函数可将参数输出到消息字符串中(代码中出现的 req.params.id
)。
BookInstance、Genre、Book 控制器
这三个控制器与 Author
的模式完全相同,只是 Book
有一个用于显示站点欢迎页面的 index()
函数:
// /controllers/bookController.js
const Book = require('../models/book');
exports.index = (req, res) => { res.send('未实现:站点首页'); };
...
创建藏书编目 catalog 路由模组
定义好控制器后,我们来为 LocalLibrary 网站创建完整的 URL 路由。
站点骨架中有一个 ./routes 文件夹,其中包含两个路由文件:index 和 user,在这里新建一个 catalog.js 路由文件,如下所示:
/express-locallibrary-tutorial // 项目根目录 /routes index.js users.js catalog.js
译注:可到 GitHub 下载完整的 catalog.js。
/routes/catalog.js 中有以下代码:
const express = require('express');
const router = express.Router();
// 导入控制器模块
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');
/// 藏书路由 ///
// GET 获取藏书编目主页
router.get('/', book_controller.index);
// GET 请求添加新的藏书。注意此项必须位于显示藏书的路由(使用了 id)之前。
router.get('/book/create', book_controller.book_create_get);
// POST 请求添加新的藏书
router.post('/book/create', book_controller.book_create_post);
// GET 请求删除藏书
router.get('/book/:id/delete', book_controller.book_delete_get);
// POST 请求删除藏书
router.post('/book/:id/delete', book_controller.book_delete_post);
// GET 请求更新藏书
router.get('/book/:id/update', book_controller.book_update_get);
// POST 请求更新藏书
router.post('/book/:id/update', book_controller.book_update_post);
// GET 请求藏书
router.get('/book/:id', book_controller.book_detail);
// GET 请求完整藏书列表
router.get('/books', book_controller.book_list);
/// 藏书副本、藏书种类、作者的路由与藏书路由结构基本一致,只是无需获取主页 ///
module.exports = router;
该模块导入了 express
并创建了一个 Router
对象 router
。所有路由都设置在 router
上,最后将其导出。
对 router
对象调用 .get()
或.post()
函数即可定义路由。这里所有路径都使用字符串定义(而不用字符串模式或正则表达式)。某些特定资源(如藏书)的路由使用路径参数从 URL 中获取对象标识。
处理函数均导入自上文的控制器模块。
更新 index 路由模块
新路由已经设置完毕,还需要设置一下 index
模块。我们将网站的首页重定向(redirect
)至刚创建的地址 '/catalog
'。
将 /routes/index.js 中的中间件修改一下:
// GET 请求主页
router.get('/', (req, res) => {
res.redirect('/catalog');
});
注:这是我们第一次使用 redirect() 响应方法。它会使路由重定向到指定的页面,默认发送 HTTP 状态代码“302 Found”。可以根据需要更改返回的状态代码、设置绝对或相对路径。
更新 app.js
最后一步是在 app.js
中将路由添加到中间件链。
打开 app.js,在 index
和 users
路由下方添加 catalog
路由:
const indexRouter = require('./routes/index');
const usersRouter = require('./routes/users');
const catalogRouter = require('./routes/catalog'); // 导入 catalog 路由
然后在已定义的路由下方将目录路由添加进中间件栈:
app.use('/', indexRouter);
app.use('/users', usersRouter);
app.use('/catalog', catalogRouter); // 将 catalog 路由添加进中间件链
注:我们将图书编目模块添加到了 '/catalog'
路径,该路径是 catalog 模块中所有路径的前缀。例如,访问藏书列表 的URL 为:/catalog/books/
。
大功告成。LocalLibrary 网站所需的所有 URL 的路由和框架函数都已准备完毕。
测试路由
要测试路由,先要启动网站
- 默认方法
$ DEBUG=express-locallibrary-tutorial:* npm start
- 如果设置过 nodemon,则可以使用:
$ DEBUG=express-locallibrary-tutorial:* npm run devstart
译注:以上命令只对 bash 有效,要在 Windows 上使用 bash,最简单的方法就是安装 Git。(详情参见 教程 2 相关译注)
接下来浏览上面一些 URL,确保不会收到错误页面(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 路由的基础知识,以及组织路由和控制器的一些方式。
下一节将使用视图 (模板) 和模型里的信息创建一个欢迎页面。
另请参阅
上一页 Overview: Express Nodejs 下一页本章目录
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论