vapor-routing

一个用于 Vapor 的路由库,专注于类型安全、组合和 URL 生成。


了解更多

这个库在 Point-Free 的一集节目中被讨论到,Point-Free 是一个探索函数式编程和 Swift 编程语言的视频系列,由 Brandon WilliamsStephen Celis 主持。

video poster image

动机

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