一个用于 Vapor 的路由库,专注于类型安全、组合和 URL 生成。
这个库在 Point-Free 的一集节目中被讨论到,Point-Free 是一个探索函数式编程和 Swift 编程语言的视频系列,由 Brandon Williams 和 Stephen Celis 主持。
Vapor 中的路由有一个简单的 API,类似于其他语言中流行的 Web 框架,例如 Ruby 的 Sinatra 或 Node 的 Express。它对于简单的路由来说工作良好,但由于缺乏类型安全以及无法为站点上的页面生成正确的 URL,复杂性会随着时间的推移而增加。
为了说明这一点,请考虑一个用于获取与特定用户关联的书籍的端点
// GET /users/:userId/books/:bookId
app.get("users", ":userId", "books", ":bookId") { req -> BooksResponse in
guard
let userId = req.parameters.get("userId", Int.self),
let bookId = req.parameters.get("bookId", Int.self)
else {
struct BadRequest: Error {}
throw BadRequest()
}
// Logic for fetching user and book and constructing response...
async let user = database.fetchUser(user.id)
async let book = database.fetchBook(book.id)
return BookResponse(...)
}
当向服务器发出 URL 请求,其方法和路径与上述模式匹配时,将执行闭包来处理该端点的逻辑。
请注意,我们必须在端点的逻辑中加入验证代码和错误处理,以便将字符串类型的参数强制转换为一流的数据类型。这掩盖了端点的真正逻辑,并且对路由模式的任何更改都必须与验证逻辑保持同步,例如,如果我们重命名 :userId
或 :bookId
参数。
除了这些缺点之外,我们经常需要能够为各种服务器端点生成有效的 URL。例如,假设我们想要生成一个 HTML 页面,其中包含用户的所有书籍列表,包括指向每本书的链接。我们别无选择,只能手动插值一个字符串来形成 URL,或者构建我们自己的 ad hoc 辅助函数库,在底层执行这种字符串插值
Node.ul(
user.books.map { book in
.li(
.a(.href("/users/\(user.id)/book/\(book.id)"), book.title)
)
}
)
<ul>
<li><a href="/users/42/book/321">Blob autobiography</a></li>
<li><a href="/users/42/book/123">Life of Blob</a></li>
<li><a href="/users/42/book/456">Blobbed around the world</a></li>
</ul>
我们有责任确保这个插值字符串与 Vapor 路由中指定的完全匹配。这可能很繁琐且容易出错。
事实上,上面的代码中存在一个错别字。构造的 URL 指向 "/book/:bookId",但实际上它应该是 "/books/:bookId"
- .a(.href("/users/\(user.id)/book/\(book.id)"), book.title)
+ .a(.href("/users/\(user.id)/books/\(book.id)"), book.title)
该库旨在解决在 Vapor 应用程序中处理路由时的这些问题以及更多问题,通过为 URL Routing 包提供 Vapor 绑定。
要使用此库,首先要构建一个枚举,描述您的网站支持的所有路由。例如,上面描述的书籍端点可以表示为一个特定的 case
enum SiteRoute {
case userBook(userId: Int, bookId: Int)
// more cases for each route
}
然后,您构建一个路由器,它是一个能够将 URL 请求解析为 SiteRoute
值,并将 SiteRoute
值打印回 URL 请求的对象。这样的路由器可以从库提供的各种类型构建,例如 Path
用于匹配特定的路径组件,Query
用于匹配特定的查询项,Body
用于解码请求体数据等等
import VaporRouting
let siteRouter = OneOf {
// Maps the URL "/users/:userId/books/:bookId" to the
// SiteRouter.userBook enum case.
Route(.case(SiteRoute.userBook)) {
Path { "users"; Digits(); "books"; Digits() }
}
// More uses of Route for each case in SiteRoute
}
注意:路由器构建于 Parsing 库之上,该库为将更模糊的数据解析为一流的数据类型(例如将 URL 请求解析为应用程序的路由)提供了通用解决方案。
一旦完成少量的前期工作,使用路由器看起来与使用 Vapor 的原生路由工具没有太大区别。首先,您将路由器挂载到应用程序以处理所有路由职责,您可以通过提供一个将 SiteRoute
转换为响应的闭包来完成此操作
// configure.swift
public func configure(_ app: Application) throws {
...
app.mount(siteRouter, use: siteHandler)
}
func siteHandler(
request: Request,
route: SiteRoute
) async throws -> any AsyncResponseEncodable {
switch route {
case let .userBook(userId: userId, bookId: bookId):
async let user = database.fetchUser(user.id)
async let book = database.fetchBook(book.id)
return BookResponse(...)
// more cases...
}
}
请注意,处理 .userBook
case 完全专注于端点的逻辑,而不是解析和验证 URL 中的参数。
完成此操作后,您现在可以使用类型安全、简洁的 API 轻松地为网站的任何部分生成 URL。例如,现在生成书籍链接列表看起来像这样
Node.ul(
user.books.map { book in
.li(
.a(
.href(siteRouter.path(for: .userBook(userId: user.id, bookId: book.id)),
book.title
)
)
}
)
请注意,这里没有字符串插值,也没有猜测路径应该是什么形状。所有这些都由路由器处理。我们只需要提供用户和书籍 ID 的数据,路由器会处理其余的事情。如果我们更改 siteRouter
,例如识别单数形式 "/user/:userId/book/:bookId",那么所有路径都将自动更新。我们不需要在代码库中搜索以将 "users" 替换为 "user",并将 "books" 替换为 "book"。
发行版和主分支的文档可在此处获得
此库在 MIT 许可证下发布。有关详细信息,请参阅 LICENSE。