路由系统原理:为什么用 Trie 树?
路由(Routing)是 Web 框架最核心的功能之一:将一个 HTTP 请求(方法 + 路径)映射到对应的处理函数。不同框架采用不同的路由匹配策略,性能差异显著。
最朴素的做法是遍历路由表,对每条规则进行字符串匹配——这是 O(n) 时间复杂度,路由越多越慢。Vapor 采用 Trie 树(前缀树)数据结构,将所有路由路径组织成一棵树,路径中每个 / 分隔的片段是一个节点。匹配时沿树向下走,时间复杂度接近 O(路径深度),与路由总数无关,适合路由规模大的应用。
Trie 树的另一个优势是参数捕获的自然性:以 : 开头的节点(如 :id)会自动捕获对应位置的值,通配符 *catchall 可以捕获剩余所有路径片段。
核心概念词典
/users/:id)和处理函数(Handler)三部分组成。Vapor 中通过 app.get()、app.post() 等方法注册。boot(routes:) 方法注册路由。将路由按资源(User、Post 等)拆分到不同的 RouteCollection,保持 routes.swift 整洁。.constant("users") 精确匹配、.parameter("id") 参数捕获、.anything 匹配任意单段、.catchall 匹配剩余所有段。字符串字面量会自动转换为对应类型。.GET、.POST、.PUT、.DELETE、.PATCH、.HEAD、.OPTIONS。Vapor 为最常用的方法提供了简写(app.get、app.post 等)。Application 和 Router 都实现了此协议。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 实现者(通常是 struct)的实例在 register() 调用后就只是路由注册的媒介,Vapor 不会保留它。如果你将数据库连接池、缓存引用等存储在 RouteCollection 的属性上,会导致意料之外的生命周期问题。路由处理函数需要的服务应通过 req.application(如 req.db、req.redis)或 Request.Storage 访问,而不是在 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())
}
路由调试与列表
开发过程中,有时需要快速查看当前注册了哪些路由(特别是路由冲突、路由找不到时)。Vapor 提供了内置命令和 HTTP 端点来检查路由表:
// configure.swift — 开发环境开启路由列表端点
if app.environment == .development {
// GET /routes 返回所有注册路由的 JSON 列表
app.routes.get("routes") { req -> [[String: String]] in
req.application.routes.all.map {
["method": $0.method.rawValue, "path": $0.path.string]
}
}
}
// 命令行方式(Vapor CLI)
// $ swift run App routes
// 输出示例:
// +--------+-----------------------+
// | GET | /api/v1/users |
// | POST | /api/v1/users |
// | GET | /api/v1/users/:id |
// | PUT | /api/v1/users/:id |
// | DELETE | /api/v1/users/:id |
// +--------+-----------------------+
参数捕获的高级用法
Vapor 的路径参数不仅支持字符串,还可以直接要求解码为特定 Swift 类型,类型不匹配时自动返回 400:
// 基础参数捕获
app.get("users", ":id") { req async throws -> UserResponse in
// 要求 id 必须能解析为 UUID,否则自动 400 Bad Request
let id = try req.parameters.require("id", as: UUID.self)
...
}
// 通配符:捕获剩余全部路径(用于反向代理、静态文件服务等场景)
app.get("files", "**") { req async throws -> Response in
// req.parameters.getCatchall() 返回 [String]
let path = req.parameters.getCatchall().joined(separator: "/")
return req.fileio.streamFile(at: "Public/\(path)")
}
// 自定义 LosslessStringConvertible 类型可直接作为路径参数
enum UserRole: String, LosslessStringConvertible {
case admin, user, moderator
init?(_ description: String) { self.init(rawValue: description) }
var description: String { rawValue }
}
// GET /roles/admin/users — role 参数自动解析为 UserRole 枚举
app.get("roles", ":role", "users") { req async throws -> [UserResponse] in
let role = try req.parameters.require("role", as: UserRole.self)
return try await User.query(on: req.db)
.filter(\.$role == role)
.all()
.map { UserResponse(from: $0) }
}
GET /users/me(静态路径,返回当前用户)和 GET /users/:id(动态参数)同时注册时,Vapor 的 Trie 树会优先匹配更具体的静态路径。这是 Vapor 有保证的行为:静态节点优先于参数节点,参数节点优先于通配符节点。但如果两条路由的优先级相同(如注册了两个 GET /users/:id),后注册的会覆盖前一个,不会报错——这是需要特别注意的隐患。
req.query.decode())避免了逐个手动读取的繁琐。路由设计规范:静态路径优先于参数路径;路径参数应使用 .require() 附带类型要求,让框架自动处理类型错误;RouteCollection 本身不应持有可变状态,服务通过 req.application 访问。