Chapter 02

路由与控制器

掌握 Vapor Trie 树路由的高效匹配原理,用 RouteCollection 构建可维护的 RESTful API 结构。

路由系统原理:为什么用 Trie 树?

路由(Routing)是 Web 框架最核心的功能之一:将一个 HTTP 请求(方法 + 路径)映射到对应的处理函数。不同框架采用不同的路由匹配策略,性能差异显著。

最朴素的做法是遍历路由表,对每条规则进行字符串匹配——这是 O(n) 时间复杂度,路由越多越慢。Vapor 采用 Trie 树(前缀树)数据结构,将所有路由路径组织成一棵树,路径中每个 / 分隔的片段是一个节点。匹配时沿树向下走,时间复杂度接近 O(路径深度),与路由总数无关,适合路由规模大的应用。

注册路由: GET /users GET /users/:id GET /users/:id/posts POST /users GET /articles Trie 路由树结构: [root] / \ users articles / \ \ (GET) :id (GET) \ posts / \ (GET) (POST for /users) ← 注意:方法绑定在节点上 匹配 GET /users/42/posts: root → "users" → ":id"(capture 42) → "posts" → GET handler 时间复杂度:O(3),与路由总数无关

Trie 树的另一个优势是参数捕获的自然性:以 : 开头的节点(如 :id)会自动捕获对应位置的值,通配符 *catchall 可以捕获剩余所有路径片段。

核心概念词典

Route
一条路由规则,由 HTTP 方法(GET/POST/PUT/DELETE/PATCH 等)、路径模式(如 /users/:id)和处理函数(Handler)三部分组成。Vapor 中通过 app.get()app.post() 等方法注册。
RouteCollection
路由集合协议,让你把相关路由组织到一个类/结构体中,通过实现 boot(routes:) 方法注册路由。将路由按资源(User、Post 等)拆分到不同的 RouteCollection,保持 routes.swift 整洁。
PathComponent
路径组件的枚举类型,分为:.constant("users") 精确匹配、.parameter("id") 参数捕获、.anything 匹配任意单段、.catchall 匹配剩余所有段。字符串字面量会自动转换为对应类型。
HTTPMethod
HTTP 方法的类型安全枚举:.GET.POST.PUT.DELETE.PATCH.HEAD.OPTIONS。Vapor 为最常用的方法提供了简写(app.getapp.post 等)。
RoutesBuilder
路由构建器协议,ApplicationRouter 都实现了此协议。routes.grouped("api", "v1") 返回一个带前缀的子构建器,在其上注册的路由会自动加上前缀,避免重复书写。

路由注册基础

Vapor 提供了语义清晰的路由注册 API,支持所有标准 HTTP 方法。处理函数接收 Request 对象,返回任何实现了 AsyncResponseEncodable 协议的类型(包括 String、JSON Encodable 结构体、Response 等)。

import Vapor

func routes(_ app: Application) throws {

    // ── 基础 HTTP 方法 ──────────────────────────────
    app.get("hello")           { req async in "GET Hello"  }
    app.post("hello")          { req async in "POST Hello" }
    app.put("hello")           { req async in "PUT Hello"  }
    app.delete("hello")        { req async in "DEL Hello"  }
    app.patch("hello")         { req async in "PATCH Hello"}

    // ── 路径参数 :name ──────────────────────────────
    app.get("users", ":id") { req async throws -> String in
        // require 如果参数不存在或类型不匹配会 throw 400
        let id = try req.parameters.require("id", as: UUID.self)
        return "User ID: \(id)"
    }

    // ── 查询参数 ?page=1&limit=20 ───────────────────
    app.get("users") { req async throws -> String in
        let page  = req.query[Int.self, at: "page"]  ?? 1
        let limit = req.query[Int.self, at: "limit"] ?? 20
        return "page=\(page) limit=\(limit)"
    }

    // ── 通配符 *catchall ────────────────────────────
    app.get("files", "**") { req async throws -> String in
        let path = req.parameters.getCatchall().joined(separator: "/")
        return "File path: \(path)"
    }
}

RouteCollection:组织路由

将所有路由都堆在 routes.swift 里很快会变成灾难。RouteCollection 协议让你按资源类型拆分路由,每个控制器负责自己的路由段。这是 Vapor 官方推荐的代码组织方式。

// Sources/App/Controllers/UserController.swift
import Vapor

struct UserController: RouteCollection {

    // boot 方法:在此注册该控制器的所有路由
    func boot(routes: RoutesBuilder) throws {
        // 路由前缀:/users
        let users = routes.grouped("users")

        users.get(use: index)           // GET  /users
        users.post(use: create)          // POST /users
        users.get(":id", use: show)      // GET  /users/:id
        users.put(":id", use: update)    // PUT  /users/:id
        users.delete(":id", use: delete) // DEL  /users/:id
    }

    // GET /users — 获取用户列表
    func index(req: Request) async throws -> [UserResponse] {
        let users = try await User.query(on: req.db).all()
        return users.map { UserResponse(from: $0) }
    }

    // POST /users — 创建用户
    func create(req: Request) async throws -> UserResponse {
        let dto = try req.content.decode(CreateUserDTO.self)
        let user = User(name: dto.name, email: dto.email)
        try await user.save(on: req.db)
        return UserResponse(from: user)
    }

    // GET /users/:id — 获取单个用户
    func show(req: Request) async throws -> UserResponse {
        let id = try req.parameters.require("id", as: UUID.self)
        guard let user = try await User.find(id, on: req.db) else {
            throw Abort(.notFound, reason: "User not found")
        }
        return UserResponse(from: user)
    }

    // PUT /users/:id — 更新用户
    func update(req: Request) async throws -> UserResponse {
        let id  = try req.parameters.require("id", as: UUID.self)
        let dto = try req.content.decode(UpdateUserDTO.self)
        guard let user = try await User.find(id, on: req.db) else {
            throw Abort(.notFound)
        }
        user.name = dto.name ?? user.name
        try await user.save(on: req.db)
        return UserResponse(from: user)
    }

    // DELETE /users/:id — 删除用户
    func delete(req: Request) async throws -> HTTPStatus {
        let id = try req.parameters.require("id", as: UUID.self)
        guard let user = try await User.find(id, on: req.db) else {
            throw Abort(.notFound)
        }
        try await user.delete(on: req.db)
        return .noContent   // 204
    }
}
// routes.swift — 注册所有 RouteCollection
func routes(_ app: Application) throws {
    // API v1 前缀:/api/v1
    let api = app.routes.grouped("api", "v1")

    try api.register(collection: UserController())
    try api.register(collection: PostController())
    try api.register(collection: AuthController())
}
保持路由文件整洁 使用 RouteCollection 后,routes.swift 只负责"注册"各控制器,每个控制器文件只包含自己资源的路由。当你需要找某个路由的处理逻辑时,直接去对应的 Controller 文件,不用在数百行代码中搜索。

查询参数的结构化解析

查询参数除了逐个读取,Vapor 还支持将整个查询字符串直接解码为一个 Decodable 结构体,代码更简洁,类型更安全:

// 定义查询参数结构体
struct UserQuery: Content {
    var page:    Int     = 1
    var limit:   Int     = 20
    var search:  String?
    var sortBy:  String  = "createdAt"
}

// GET /users?page=2&limit=10&search=alice&sortBy=name
app.get("users") { req async throws -> [UserResponse] in
    let query = try req.query.decode(UserQuery.self)
    // query.page = 2, query.limit = 10, query.search = "alice"
    var builder = User.query(on: req.db)
    if let search = query.search {
        builder = builder.filter(\.name, .contains, search)
    }
    return try await builder
        .range((query.page - 1) * query.limit ..< query.page * query.limit)
        .all()
        .map { UserResponse(from: $0) }
}

路由分组与版本控制

生产 API 通常需要版本控制(v1、v2)和公共/私有路由的区分。Vapor 的 grouped() 方法支持路径前缀和中间件两个维度的分组,两者可以组合使用:

func routes(_ app: Application) throws {

    // 公开路由(无需认证)
    let publicAPI = app.routes.grouped("api", "v1")
    publicAPI.post("register", use: AuthController().register)
    publicAPI.post("login",    use: AuthController().login)

    // 需要 JWT 认证的路由(组合路径前缀 + 中间件)
    let protected = app.routes
        .grouped("api", "v1")
        .grouped(UserAuthenticator(), User.guardMiddleware())

    try protected.register(collection: UserController())
    try protected.register(collection: PostController())
}
本章小结 Trie 树路由让 Vapor 的路径匹配性能与路由数量无关;RouteCollection 是组织大型项目路由的最佳实践;查询参数可以结构化解码,避免逐个手动读取。下一章将学习 Fluent ORM,深入数据库操作的类型安全之道。