我又新写了一款 Swift 服务端框架,这次是基于 Vapor 4 - 技术博文 - 平行宇宙

我又新写了一款 Swift 服务端框架,这次是基于 Vapor 4 2023-07-03 00:42 767 13 3 ## 引言 我在四年前发过一篇博客:[「回首三年Swift后端之旅,今年我用Swift写前端了」](https://blog.yuusann.com/posts/article/19006)。在这篇文章中,我介绍了当时我最新写的 Swift 后端框架和前端框架。 已经有几年没有更新博客的技术栈了,今天我会在这篇文章里做一点更新。 ![](//cdn.blog.yuusann.com/img/posts/23001_07.jpg) ## 回顾一下过去 ### 1.0 时代 2016 年,这是一个相当蛮荒的时代。 ![](//cdn.blog.yuusann.com/img/posts/23001_01.jpg) 当时 Swift 刚刚开源不久,在 Linux 上运行可谓是遍地是坑。我当时使用的框架是`Perfect`,这是个相当“渐进式”的框架,使用了大量 C 接口来让整个框架可以跑起来。 由于整个项目是出于探索 Swift for Linux 的意图,在网页方面我用非常原始地用 Bootstrap / JQuery 简单写了一个就匆匆上线跑着了。 当年甚至还别出心裁地做了个线上可以跑代码的平台: ![](//cdn.blog.yuusann.com/img/posts/23001_05.jpg) ### 2.0 时代 2017 年,我的北京元年,我来到了北京加入了百度。 ![](//cdn.blog.yuusann.com/img/posts/23001_04.jpg) 我在前一份工作中接触了 Python 著名后端框架`Django`。我基于`Django`的业务抽象,在`Perfect`提供的能力上搭了一个有业务能力的框架出来,取名为`Pjango`。 之后我也在这个框架的基础上重写了网页,并一起上线了重写后的新博客。 ![](//cdn.blog.yuusann.com/img/posts/23001_06.jpg) ### 3.0 时代 2019 年,旧世界末年(指新冠前)。 ![](//cdn.blog.yuusann.com/img/posts/23001_03.jpg) `Pjango`虽然拥有了一定的业务抽象,但过分参考了 Python 的设计,在诸如环境变量等等许多写法上非常不 Swifty,于是这一年我开启了新的计划:`Project Virgo`。 在这个计划中我重新设计了前后端,两个项目分别为:`Heze`(后端) 和`SPiCa`(前端) 这篇博客:[「回首三年Swift后端之旅,今年我用Swift写前端了」](https://blog.yuusann.com/posts/article/19006)就是在发布这个项目之后写的。 > 小知识: > > 这个计划的命名灵感来自于初音未来的《SPiCa》,于是我找到了一些相关的内容来组成这个项目的名称。在“关于”页面里的头像用的也就是这首歌的图。 > > Virgo:室女座。选择个星座是因为 SPiCa 属于这个星座。 > > SPiCa:室女座 α 星,角宿一,是室女座的最亮星。 > > Heze:室女座 ζ 星,角宿二。 这个项目极具创意: - Heze 的数据库驱动能一行代码不写就把一条记录渲染成 JSON,并提供了自定义字段等等能力,以及巧妙地利用 GCD 实现了许多异步操作。 - SPiCa 则是作为代码生成器,网页端的 Vue 代码是由她的 Swift 代码生成的。截止到这篇文章发布位置,读者你看到的网页仍然是`SPiCa`生成的。 ### 然后呢? `Heze`虽然拥有一些相对先进的能力,但是地基依然是 Perfect。而很不幸的是,这个项目已经没有人维护了。 在那个年代,Swift 在 Linux 和 macOS 上仍然由许多不同的行为。Swift 近几年 Feature 井喷,随着版本迭代,代码甚至出现了编译问题,我不得不维护了两个分支: - Linux 分支:仍然停留在 Swift 4.2。 - macOS 分支:支持到最新 Swift / Xcode。 我曾经好几次想把 Swift 版本 bump 上去,都失败了。 随着`async/await`等 Feature 在 Swift 新版本上出现,我们可能需要考虑使用新的异步 API 来实现了。可能真的是时候让 Perfect 退出了。 ![](//cdn.blog.yuusann.com/img/posts/23001_08.jpg) ## 回到现在 2022 年底,我开始了新的计划:`Helios`。 这个计划会重写整个服务端框架,使用最新的 Vapor 4 以及最新的 Swift 版本进行开发。完全切到异步 API 上。 Vapor 4 的数据库驱动能够使用`KeyPath`写出结构化的 SQL 查询语句和 Model 查询方法,还是挺炫酷的。 > 小剧场: > > 这个命名灵感来自于米哈游《崩坏3》的早期(2016 年)的开局 PV。这个 PV 现在已经没有了,但能在视频网站上搜到别人的录像。 > > 剧情是琪亚娜从空中跳下着陆在 Helios 号运输舰上。嗯,没别的,就只是喜欢 Helios 这个名字罢了。 那么现在就来具体看一下 Helios 的设计。 ### 路由 路由的设计基本照抄了`Heze`的设计,用`Path: Method: Handler`的层级来分发路由。 func routes(app: HeliosApp) -> [String : [HTTPMethod : HeliosHandlerBuilder]] { return [ // Posts list "/api/posts/list": [ .GET: PostsListApi.builder, ], ] } Swift 一个简单的查库 + 渲染的 API: import Foundation import Vapor import Helios class PostsListApi: HeliosHandler { required init() { } func handle(req: Request) async throws -> AsyncResponseEncodable { let postsList = try await PostsInfoModel.query(on: req.db) .sort(\PostsInfoModel.$date, .descending) .all() for posts in postsList { try await posts.preRender(db: req.db) } return jsonResponse(data: postsList.map { $0.render() }) } } Swift 在 Vapor 4 的数据库驱动中,`where`, `on`, `order`等操作都可以用`KeyPath`来操作,以写出非常 Swifty 的结构化代码。 在 Vapor 4 中,为了返回符合`AsyncResponseEncodable`协议的对象进行渲染,需要通过额外定义遵循`Content`协议的结构来实现。这里引入了一些学习成本,我就直接放代码了: struct HeliosHandlerResponse: Codable, Content { let success: Bool let msg: String let data: T } extension HeliosHandler { func jsonResponse(data: T) -> AsyncResponseEncodable { let response = HeliosHandlerResponse( success: true, msg: "nil", data: data) return response } } Swift ## 数据库 Vapor 的数据库驱动还是非常全的。 `Heze`的数据库驱动是我自己写的,我甚至连事务都懒得实现,但在 Vapor 里这些都是现成的。 模型的注册也是照抄了`Heze`的设计。 func models(app: HeliosApp) -> [HeliosAnyModelBuilder] { return [ // ... PostsInfoModel.builder, // ... ] } Swift 模型的定义需要遵循 Vapor 的定义,因此没有太多的操作空间。 class PostsInfoModel: HeliosModel { public static let schema = "Posts" @ID(custom: "id") public var id: String? @Field(key: "PID") public var pid: Int @Field(key: "TITLE") public var title: String // ...... required public init() { } } Swift 有一个非常有意思的设计:我把 Vapor 的`Migration`结合到了`HeliosModel`的协议中,把这俩写在一块儿,使框架能够自动建表。方法大概长这样: public func creator(database: Database) -> SchemaBuilder { database.schema(Self.schema) .field(.id, .string, .identifier(auto: false)) .field(_pid.key, .int, .required) .field(_title.key, .string, .required) // ...... } Swift 最后是渲染部分。刚刚提到我们需要有遵守`Content`协议的结构来承接`AsyncResponseEncodable`协议。 extension PostsInfoModel { public func render() -> PostsInfo { return PostsInfo(model: self) } } struct PostsInfo: Codable, Content { let PID: Int let TITLE: String // ... init(model: PostsInfoModel) { PID = model.pid TITLE = model.title // ... } } Swift 这个结构也直接承载和客户端交互的任务,所以这一层也不是完全多余的,可以做一些额外的工作,比如计算一些自定义字段。 ## 插件系统 Vapor 本身提供了`Middleware`中间件系统,`Helios`在`Heze`的基础上,也抽象出了`Filter`,`Timer`等等插件,注册的时候大概是这样: func filters(app: HeliosApp) -> [HeliosFilterBuilder] { return [ AccessFilter.builder, // ... ] } func timers(app: HeliosApp) -> [HeliosTimerBuilder] { return [ ActivityPlugin.builder, // ... ] } Swift 作为`Filter`,它的活自然是过滤 Request 和 Response,做一些添油加醋的工作。这个协议的定义大概长这样: public protocol HeliosFilter: AsyncMiddleware { init() func filterRequest(request: Request) async throws -> Response? func filterResponse(request: Request, response: Response) async throws -> Response } Swift 至于`Timer`,则是有`schedule`和`run`两个事件。以下是一个真实的 Timer 的实现: class ActivityPlugin: HeliosTimer { func schedule(queue: Application.Queues) { queue.schedule(self) .minutely() .at(0) } func run(context: QueueContext) async throws { try await ActivityCenter.shared.updateCache() } required init() { } } Swift ### 视图 视图的设计也参考了`Heze`。整个协议大概长这样: public protocol HeliosView: HeliosHandler { func template(req: Request) async throws -> String func canHandle(req: Request) async throws -> Bool func render(req: Request) async throws -> Codable? } Swift 从`SPiCa`开始,我的网页都是`Single-page Application`,因此对爬虫和 SEO 是极不友好的。我在 2020 年单独做了一套 SSR(服务端渲染)的页面给爬虫们玩儿。因此就会有个类似 Proxy 的设计,大概长这样: func template(req: Request) async throws -> String { return isSpider(req: req) ? try await spiderView.template(req: req) : try await userView.template(req: req) } func canHandle(req: Request) async throws -> Bool { return isSpider(req: req) ? try await spiderView.canHandle(req: req) : try await userView.canHandle(req: req) } func render(req: Request) async throws -> Codable? { triggerSession(req: req) return isSpider(req: req) ? try await spiderView.render(req: req) : try await userView.render(req: req) } Swift 有一点不同的是,Vapor 自带的 HTML 渲染器是 Leaf 的,所以我也一并修改了模板的占位符。 ### 小结 我于 2022 年底启动这个计划,其实早就把`Helios`实现完了,但一直没有把博客迁过去。最近由于一些众所周知的原因也终于有时间来做这个事情了。至于这件事,我会在后面再写一篇文章说。 在做完这些后,我算是拥有了一个真正基于异步 API 的 Swift 服务端框架。同时,它也将会比较容易地迁到更新的 Swift 的版本上以获得新的能力。我也彻底摆脱了 Perfect 的魔咒。它无法升级 Swift 版本的痛苦伴随着我已经很久了。 目前这个项目处于闭源状态,其原因有二: - 项目相对稚嫩,我不鼓励大家使用,以免遭受损失。 - 我也不希望大家走这个方向,有许多更好的选择。 说到底这只不过是我的一个玩具罢了。 最近我也在做我的 Home Server 计划。所有的服务将会部署在家里的机器上,并由云服务器穿回来以提供服务。 ![](//cdn.blog.yuusann.com/img/posts/23001_09.jpg) 期望的是把所有资源丢到 CDN 后,本地只提供 API 服务。这样能够最大程度压缩成本。 ## 结语 / 未来呢? 老实说我不知道。 这已经是我做 Swift 服务端的第七年了,Swift 服务端依旧冷门。但随着近几年的异步 API 开放,以及官方的 Swift NIO 的支持,事情似乎在往好的方向发展。 本次更新我下掉了许多原本博客的功能,比如邮件订阅,数据后台等等。我逐渐发现我并不需要这些东西。 我们每天的工作里有太多数据相关的事情,使得我们好像不冲着数据就不会做事了一样。 只看数据的世界太无趣了,我需要有一篇自留地,这里和任何数据无关,我想做啥就做啥。 https://blog.yuusann.com/posts/article/23001?continueFlag=80bb619576e267512bef940817a9cc9b