控制器与 RouteCollection 封面
Vapor 大型教程

控制器与 RouteCollection

当项目变大、路由越来越多时,一条条手写 router 会难以维护。本文教你用控制器(Controller)把零散路由组织成独立模块,让代码清晰、好维护。

为什么要用控制器

当项目很大时,我们不能一条条地写 router,因为之后很难进行维护。一般大型项目都要采用一定的设计模式,以确保项目可以有效地进行维护。在 Web 开发中很流行的一种模式就是 MVC,即 Model-View-Controller:以 Web 为例,数据库中的数据结构为模型(Model),给用户展示的 HTML 页面为视图(View),控制着数据展示到页面上的那部分逻辑为控制器(Controller)。

当工程比较大时,一条条的 Router 组成了请求返回逻辑,处理数据和视图的展示。使用 Controller 把一条条零散的 Router 组织成独立的模块,有利于划分概念、方便后期维护。例如针对数据的 CRUD 操作,就可以使用一个控制器组织在一起。

RouteCollection

routes.swift 可以大幅简化,把所有路由拆到各自的控制器里:

import Vapor

/// Register your application's routes here.
public func routes(_ router: Router) throws {

    let acronymsController = AcronymsController()
    try router.register(collection: acronymsController)
}

分组的所有 routes 都写入控制器中,控制器继承自 RouteCollection

import Vapor
import Fluent

struct AcronymsController: RouteCollection {
    func boot(router: Router) throws {
        router.post("api", "acronyms", use: createHandler)
    }

    func createHandler(_ req: Request) throws -> Future<Acronym>
    {
        // 这里手动解码为数据模型
        return try req.content.decode(Acronym.self)
            .flatMap(to: Acronym.self) { (acronym) in
                return acronym.save(on: req)
        }
    }
    ...
}

RouteGroup

上面的方式需要每条 route 都写明自己的全部路径。对于 RESTful 来说,如果控制器中有多条 route,它们的路径有很长一部分是共用的,那么后期如果要同时修改它们前面相同的部分,对维护来说将是灾难。所以有了 RouteGroup 的概念来解决这个问题。

import Vapor
import Fluent

struct AcronymsController: RouteCollection {
    func boot(router: Router) throws {
        let routeGroup = router.grouped("api", "acronyms")

        routeGroup.post(use: createHandler)
        routeGroup.get(use: getAllHandler)
        routeGroup.get(Acronym.parameter, use: getHandler)
        routeGroup.put(Acronym.parameter, use: updateHandler)
        routeGroup.delete(Acronym.parameter, use: deleteHandler)
        routeGroup.get("search", use: searchHandler)
        routeGroup.get("first", use: firstHandler)
        routeGroup.get("sorted", use: sortedHandler)


    }

    func createHandler(_ req: Request) throws -> Future<Acronym>
    {
        return try req.content.decode(Acronym.self)
            .flatMap(to: Acronym.self) { (acronym) in
                return acronym.save(on: req)
        }
    }

    func getAllHandler(_ req: Request) throws -> Future<[Acronym]> {
        return Acronym.query(on: req).all()
    }

    func getHandler(_ req: Request) throws -> Future<Acronym> {
        return try req.parameters.next(Acronym.self)
    }

    func updateHandler(_ req: Request) throws -> Future<Acronym> {
        return try flatMap(to: Acronym.self, req.parameters.next(Acronym.self), req.content.decode(Acronym.self)) { (acronym, updateAcronym) -> Future<Acronym> in
            acronym.short = updateAcronym.short
            acronym.long = updateAcronym.long

            return acronym.save(on: req)
        }
    }

    func deleteHandler(_ req: Request) throws -> Future<HTTPStatus> {
        return try req.parameters.next(Acronym.self)
            .delete(on: req)
            .transform(to: HTTPStatus.noContent)
    }

    func searchHandler(_ req: Request) throws -> Future<[Acronym]> {
        guard let searchItem = req.query[String.self, at: "term"] else {
            throw Abort(.badRequest)
        }
        return Acronym.query(on: req).group(.or) { (or) in
            or.filter(\.short == searchItem)
            or.filter(\.long == searchItem)
            }
            .all()
    }

    func firstHandler(_ req: Request) throws -> Future<Acronym> {
        return Acronym.query(on: req).first().map(to: Acronym.self) { (acronym)  in
            guard let acronym = acronym else {
                throw Abort(.notFound)
            }
            return acronym
        }
    }

    func sortedHandler(_ req: Request) throws -> Future<[Acronym]> {
        return Acronym.query(on: req).sort(\.short, .ascending).all()
    }
}

让 Vapor 自动解码请求体

另外,还可以改进一个 route 的使用方式:我们不用手动解码请求参数,以 createHandler 为例,在注册 route 时指明要解码的类型即可:

...
routeGroup.post(Acronym.self,use: createHandler)
...

func createHandler(_ req: Request, acronym: Acronym) throws -> Future<Acronym> {
    return acronym.save(on: req)
}
提示:在 route 上直接声明 Acronym.self,Vapor 会在调用 handler 前自动完成解码,handler 的第二个参数直接拿到已解码好的模型,省去手动 req.content.decode 的步骤。

本系列其他文章