跳转至

第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,添加以下内容来创建一个符合RouteCollectionAcronymsController

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/acronymsGET请求调用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)

下面是这个的作用:

  1. 创建一个新的AcronymsController
  2. 在应用程序中注册新类型,以确保控制器的路由被注册。

建立并运行应用程序,然后在RESTed中创建一个新的请求。配置该请求如下:

  • URL: http://localhost:8080/api/acronyms/
  • method: GET

发送请求,你将得到你的数据库中现有的首字母缩写:

img

路由组

前面几章中为首字母缩写创建的所有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)

下面是这个的作用:

  1. 注册createHandler(_:)来处理对/api/acronymsPOST请求。
  2. 注册getHandler(_:)来处理对/api/acronyms/<ACRONYM ID>GET请求。
  3. 注册updateHandler(_:)来处理对/api/acronyms/<ACRONYM ID>PUT请求。
  4. 注册deleteHandler(_:)来处理对/api/acronyms/<ACRONYM ID>DELETE请求。
  5. 注册searchHandler(_:)来处理对/api/acronyms/searchGET请求。
  6. 注册getFirstHandler(_:)来处理对/api/acronyms/firstGET请求。
  7. 注册sortedHandler(_:)来处理对/api/acronyms/sortedGET请求。

建立并运行应用程序,然后在RESTed中创建一个新的请求。配置该请求如下:

  • URL: http://localhost:8080/api/acronyms/first
  • method: GET

发送请求,你会看到一个先前创建的缩写,使用新的控制器:

img

接下来去哪?

本章介绍了控制器是一种更好地组织代码的方法。它们有助于将路由处理程序分割成独立的责任区。这使得应用程序能够以一种可维护的方式发展。下一章将探讨如何在Fluent中把不同的模型与关系结合起来。