路由系统原理:为什么用 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.get、app.post 等)。RoutesBuilder
路由构建器协议,
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 后,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,深入数据库操作的类型安全之道。