第8章:控制器¶
在前几章中,你已经在routes.swift
中编写了所有的路由处理程序。这对于大型项目来说是不可持续的,因为文件很快就会变得太大、太杂乱了。本章介绍了控制器的概念,以帮助管理你的路由和模型,同时使用基本控制器和RESTful
控制器。
Note
本章要求你已经设置并配置了PostgreSQL
。按照第6章"配置数据库"的步骤,在Docker
中设置PostgreSQL
并配置Vapor
应用程序。
控制器¶
Vapor
中的控制器与iOS
中的控制器有类似的作用。它们处理来自客户端的交互,例如请求,处理它们并返回响应。控制器提供了一种方法来更好地组织你的代码。在一个专门的控制器中处理所有与模型的交互是很好的做法。例如,在TIL
应用程序中,一个首字母缩写控制器可以处理所有关于首字母缩写的CRUD
操作。
控制器也被用来组织你的应用程序。例如,你可以使用一个控制器来管理你的API的旧版本,另一个控制器来管理当前的版本。这样可以在你的代码中明确区分责任,并保持代码的可维护性。
开始使用控制器¶
在Xcode
中,创建一个新的Swift
文件来存放缩写控制器。在Sources/App/Controllers
中创建该文件,并将其称为AcronymsController.swift
。
路由集合¶
在一个控制器内,你定义了不同的路由处理程序。要访问这些路由,你必须向路由器注册这些处理程序。一个简单的方法是,从routes.swift
中调用你控制器内部的函数。比如说:
app.get(
"api",
"acronyms",
use: acronymsController.getAllHandler)
这个例子在acronymsController
上调用getAlllHandler(_:)
。这个调用就像你在第7章写的路由处理程序。然而,你不是把一个闭包作为最后的参数,而是传递要使用的函数。
这对小型应用来说很有效。但如果你有大量的路由需要注册,routes.swift
又会变得难以管理。对于控制器来说,负责注册他们所控制的路由是很好的做法。Vapor
提供了协议RouteCollection
来实现这一点。
在Xcode
中打开AcronymsController.swift
,添加以下内容来创建一个符合RouteCollection
的AcronymsController
:
import Vapor
import Fluent
struct AcronymsController: RouteCollection {
func boot(routes: RoutesBuilder) throws {
}
}
RouteCollection
要求你实现boot(route:)
来注册路由。在boot(route:)
之后添加一个新的路由处理程序:
func getAllHandler(_ req: Request)
-> EventLoopFuture<[Acronym]> {
Acronym.query(on: req.db).all()
}
处理程序的主体与你之前写的相同,签名与你之前使用的闭包的签名一致。在boot(router:)
中注册路由:
routes.get("api", "acronyms", use: getAllHandler)
这使得对/api/acronyms
的GET
请求调用getAllHandler(_:)
。你之前在routes.swift
中也写了这个路由。现在,是时候删除那条了。打开routes.swift
,删除以下处理程序:
app.get("api", "acronyms") {
req -> EventLoopFuture<[Acronym]> in
Acronym.query(on: req.db).all()
}
接下来,在routes(_:)
的末尾添加以下内容:
// 1
let acronymsController = AcronymsController()
// 2
try app.register(collection: acronymsController)
下面是这个的作用:
- 创建一个新的
AcronymsController
。 - 在应用程序中注册新类型,以确保控制器的路由被注册。
建立并运行应用程序,然后在RESTed
中创建一个新的请求。配置该请求如下:
- URL: http://localhost:8080/api/acronyms/
- method: GET
发送请求,你将得到你的数据库中现有的首字母缩写:
路由组¶
前面几章中为首字母缩写创建的所有REST
路由都使用相同的初始路径,例如:
app.post("api", "acronyms") {
req -> EventLoopFuture<Acronym> in
let acronym = try req.content.decode(Acronym.self)
return acronym.save(on: req.db).map { acronym }
}
如果你需要改变/api/acronyms/
路径,你必须在多个地方改变路径。如果你添加一个新的路径,你必须记住添加路径的两个部分。Vapor
提供了路由组来简化这个问题。打开AcronymsController.swift
,在boot(routes:)
的开头创建一个路由组:
let acronymsRoutes = routes.grouped("api", "acronyms")
这将为路径/api/acronyms
创建一个新的路由组。接下来,替换:
routes.get("api", "acronyms", use: getAllHandler)
为以下内容:
acronymsRoutes.get(use: getAllHandler)
这和以前一样,但大大简化了代码,使其更容易维护。
接下来,打开routes.swift
,删除剩余的缩写路由处理程序:
router.post("api", "acronyms")
router.get("api", "acronyms", Acronym.parameter)
router.put("api", "acronyms", Acronym.parameter)
router.delete("api", "acronyms", Acronym.parameter)
router.get("api", "acronyms", "search")
router.get("api", "acronyms", "first")
router.get("api", "acronyms", "sorted")
接下来,从模板中删除任何其他路由。你应该在routes(_:)
中只留下AcronymsController
注册。接下来,打开AcronymsController.swift
,在boot(router:)
后面添加以下各项,重新创建处理程序:
func createHandler(_ req: Request) throws
-> EventLoopFuture<Acronym> {
let acronym = try req.content.decode(Acronym.self)
return acronym.save(on: req.db).map { acronym }
}
func getHandler(_ req: Request)
-> EventLoopFuture<Acronym> {
Acronym.find(req.parameters.get("acronymID"), on: req.db)
.unwrap(or: Abort(.notFound))
}
func updateHandler(_ req: Request) throws
-> EventLoopFuture<Acronym> {
let updatedAcronym = try req.content.decode(Acronym.self)
return Acronym.find(
req.parameters.get("acronymID"),
on: req.db)
.unwrap(or: Abort(.notFound)).flatMap { acronym in
acronym.short = updatedAcronym.short
acronym.long = updatedAcronym.long
return acronym.save(on: req.db).map {
acronym
}
}
}
func deleteHandler(_ req: Request)
-> EventLoopFuture<HTTPStatus> {
Acronym.find(req.parameters.get("acronymID"), on: req.db)
.unwrap(or: Abort(.notFound))
.flatMap { acronym in
acronym.delete(on: req.db)
.transform(to: .noContent)
}
}
func searchHandler(_ req: Request) throws
-> EventLoopFuture<[Acronym]> {
guard let searchTerm = req
.query[String.self, at: "term"] else {
throw Abort(.badRequest)
}
return Acronym.query(on: req.db).group(.or) { or in
or.filter(\.$short == searchTerm)
or.filter(\.$long == searchTerm)
}.all()
}
func getFirstHandler(_ req: Request)
-> EventLoopFuture<Acronym> {
return Acronym.query(on: req.db)
.first()
.unwrap(or: Abort(.notFound))
}
func sortedHandler(_ req: Request)
-> EventLoopFuture<[Acronym]> {
return Acronym.query(on: req.db)
.sort(\.$short, .ascending).all()
}
这些处理程序中的每一个都与你在第7章中创建的那些相同。如果你需要提醒你它们是做什么的,那就去看看吧!
最后,使用路由组注册这些路由处理程序。在boot(route:)
的底部添加以下内容:
// 1
acronymsRoutes.post(use: createHandler)
// 2
acronymsRoutes.get(":acronymID", use: getHandler)
// 3
acronymsRoutes.put(":acronymID", use: updateHandler)
// 4
acronymsRoutes.delete(":acronymID", use: deleteHandler)
// 5
acronymsRoutes.get("search", use: searchHandler)
// 6
acronymsRoutes.get("first", use: getFirstHandler)
// 7
acronymsRoutes.get("sorted", use: sortedHandler)
下面是这个的作用:
- 注册
createHandler(_:)
来处理对/api/acronyms
的POST
请求。 - 注册
getHandler(_:)
来处理对/api/acronyms/<ACRONYM ID>
的GET
请求。 - 注册
updateHandler(_:)
来处理对/api/acronyms/<ACRONYM ID>
的PUT
请求。 - 注册
deleteHandler(_:)
来处理对/api/acronyms/<ACRONYM ID>
的DELETE
请求。 - 注册
searchHandler(_:)
来处理对/api/acronyms/search
的GET
请求。 - 注册
getFirstHandler(_:)
来处理对/api/acronyms/first
的GET
请求。 - 注册
sortedHandler(_:)
来处理对/api/acronyms/sorted
的GET
请求。
建立并运行应用程序,然后在RESTed中创建一个新的请求。配置该请求如下:
- URL: http://localhost:8080/api/acronyms/first
- method: GET
发送请求,你会看到一个先前创建的缩写,使用新的控制器:
接下来去哪?¶
本章介绍了控制器是一种更好地组织代码的方法。它们有助于将路由处理程序分割成独立的责任区。这使得应用程序能够以一种可维护的方式发展。下一章将探讨如何在Fluent
中把不同的模型与关系结合起来。