回首三年Swift后端之旅,今年我用Swift写前端了

发布于:2019-09-12 22:39,阅读数:4171,点赞数:11


# 引言 三年前,我开始用 Swift 写后台来搭建网站。三年来,Server-side Swift 一直不温不火的。期间我也写了一些文章,今天就以本文来对三年来的 Swift 服务端开发做个总结吧。 今天除了会聊聊最近开发的新项目的内容,还会聊一些性能、生态、开发体验上的事情。 # 总览 三年来做过的项目不少,2016 年的代码基本已经不在运行了,2017 年以来的代码基本上都还在服役。目前仍然活跃的项目: |项目|类型|描述|语言|代码规模| |:--:|:--:|:--:|:--:|:--:| |Bootstrap|服务|微服务部署和守护|Swift |361| |Watchdog|服务|反向代理服务器|Swift |171| |Home|服务|主页<br>(目前只有重定向)|Swift|34| |Blog|服务|新博客后端业务|Swift|3911| |Blog-web|客户端|新博客前端业务|Swift<br>少量JS|10062| |Heze|框架|新后端框架|Swift|1403| |SPiCa|框架|新前端框架|Swift|1484| |Virgo|框架|基础架构|Swift|235| |Publisher|客户端|博文内容发布器|Swift|301| |Calatrava|服务|2017年博客后台<br>(这里不统计前端)|Swift|5320| |Pjango|框架|2017年的后端框架|Swift|1503| |Enumsblog|服务|2016年博客后台<br>(仅保留了重定向)|Swift|225| |SwiftyScript|框架|Shell交互框架|Swift|323| |Postman|服务|自加密的穿墙服务器|Swift|233| |总计||||25566| 以上项目中的一部分是服务端,一部分是客户端,目前活跃的服务: ![](//cdn.blog.yuusann.com/img/posts/19006_1.png) 这些服务大多数经过了两年的线上验证,最起码连续跑几个月没有什么问题的,可以满足一般业务的需求。 # 选型 在三年前我刚开始开发时,`Perfect`和`Vapor`都已经推出了,这里着重讲一下这两个框架。 ## 相同点 - 二者都拥有完整的 Socket 能力。作为服务器软件时,前面是不需要架设 nginx 这类服务的。 - 二者都是单进程模型,一个服务就是一个进程,所有客户端都接到这一个进程来,如果这个进程崩了就全崩了。 - 二者都有自己的生态,囊括了通用工具库,常用数据库、消息队列等。 - 二者最后的产品都是原生二进制,不依赖任何虚拟机,意味着高性能和低内存占用。一个最基本的服务占用的内存不超过`5M`。 ## 不同点 - Vapor 有更完善的业务架构抽象;Perfect 则是一个个独立的功能模块,没有业务抽象。 - Vapor 已经全面切到 NIO,开发时采用异步编程的方式,Perfect 则还是同步模型。(Perfect-NIO是独立仓库,看上去并不和之前的兼容) - Vapor 对 SwiftCore 的依赖更强,Perfect 依赖的则大部分是操作系统 C 接口。 ## 怎么选? 我认为 Perfect 几乎全面胜出,选择 Vapor 的理由只有 NIO 一个,但缺点实在让人无法接受。 - Perfect 一个个独立模块导致它是渐进式的,你可以只用 Perfect 的邮件模块来实现邮件收发接入现有系统,Vapor 的业务抽象有些复杂,它要求你按照它的规范开发,入侵性太强。 - Perfect 的生态更完整一点,表现在 SMTP,Python,TensorFlow 等跨领域的生态,触手可及之处更远。 - Vapor 过分依赖 SwiftCore,但 SwiftCore 在 Linux 上存在非常多问题,表现在有些 API 和 Mac 上结果并不一致,或是性能问题,特别是经常出现一些莫名其妙的输出,而在 Linux 上调试相对困难,过分依赖它是很愚蠢的。而操作系统的 C 接口则非常稳定,实践中基本没有遇到过跨平台兼容问题。 - Vapor 在使用过程中发现一个致命点,Static Handler 不支持 Range。对于一个发展了三年多的框架,这点是无法接受的。Safari 在播放视频时会先发一个 Range 长度是 1 字节长度的请求,Vapor 连在 Safari 里播放视频这样简单的需求都做不到,Vapor 根本就不是一个候选项。 # 开发 Swift 的开发效率很高,编译器能够帮开发者做很多的检查,一般情况下能跑起来线上就不会崩溃。但没有 IDE 支撑,写 Swift 会是件比较痛苦的事情。 有可能的话,请想办法让环境能在 Mac 上运行。如果项目的有些依赖只在 Linux 上有,那么你可能只能在 Xcode 里写而没法运行,那么整个开发体验可能会有点糟糕。 # 部署 我的多数服务的内存占用都在`10M`以下,因此一台底配的云服务器就能够跑起来了。 服务器请尽量选择 Ubuntu,因为官方只提供了 Ubuntu 的环境。如果使用其他系统,你可能需要自己编译 Swift。而根据 @南栀倾寒 的亲生经历,自己编译环境并不容易,机器需要 64GB 以上的内存,否则编译会失败。 如果选择 Ubuntu,一切都变的很简单了。官方的包解压即用,部署上可以选择推源码到服务器编译,或者在 Mac 交叉编译等等。 你可以选择低配机器来节约成本,虽然服务本身不占内存,但是数据库、消息队列等基础设施是占内存的。一台 1GB 内存的服务器,跑个 MySQL 需要 300M,如果你还要用 redis,kafka,请选择内存稍微大一点的机器。 Swift 编译器正常编译时会占用 200M 左右的内存,如果机器内存太小,在代码复杂度比较高的时候可能会直接 OOM,我在开发过程中发生过由于 OOM 机器宕机,只能重启回滚代码回 Mac 上排查的情况。 如果拆分微服务,你可能需要像我一样自己构建一套部署和监控的工具来提高效率。 # 有坑吗? 在长达三年的踩坑之旅中,最让我不爽坑的是 SPM 的依赖处理。如果你的依赖中,版本发生了冲突,SPM 有几率会直接 hang 住。 例如: 你的项目依赖 A、B、C,与此同时,B 也依赖了 C,但你项目所依赖的这个版本的 B 所依赖的 C 版本和你项目直接依赖的 C 的版本冲突时,SPM 可能就直接 hang 住了。表现为流程卡住,CPU 100%(单核)。发生这种情况你可能需要检查一下版本依赖的情况。 这个问题体现在 Mac 上时,表现为安装完依赖以后提示`Package.resolved`不是最新,再次更新仍然会提示这个,流程也就卡在这里无法前进了。发生这种情况也需要检查一下依赖的版本冲突问题。 # 新框架 说说这次的新框架吧。 从七月下旬开始,我就计划着更换博客的旧框架,因为旧后端框架的源码不够优雅。旧的框架叫 [Pjango](https://github.com/enums/Pjango),开发于 2017 年,是一个基于 Perfect,业务抽象上参考 Django 的 Swift 后端框架。 虽然这个项目在线上跑了两年多没有什么问题,但我必须得承认当时的设想有点蠢。Django 的优雅和 Swift 的优雅根本就是两码事。以至于我写出了个非常不 Swifty 的框架。而且它不支持 HTTPS,数据库能力薄弱(这一点在 Heze 里仍然没有得到加强,因为我没有太多数据库经验),以及不包含现代前端的相关能力,前端还是主要以手写 HTML 为主,无法开发出一些复杂的内容,导致整个网站内容落后时代至少五年。 于是我就开始了这个代号为 Virgo 的项目。项目分为四个部分:后端框架(Heze)、前端框架(SPiCa)、后端业务、前端业务。旨在把后端代码更加 Swift 化,同时让支持一些现代前端的能力,让站点能够追一追现代前端的脚步。 下面就针对 Heze 和 SPiCa 两个端的框架来展开讲一下,这一部分涉及的代码内容会比较多。 ## Heze Heze 的业务抽象参考了 Pjango 的设计,其本质是`路由`+`模型抽象`+`插件`。代码实现上更加优雅,更加符合 Swift 开发者的习惯。 ### 路由 路由的能力由 PerfectHTTP 提供,Heze 在此之上做了业务抽象,以`路径:方法:句柄`的方式进行路由注册。 ```swift func registerViews() -> [HezeHandlerPath: [HTTPMethod: HezeMetaHandler]] { return [ "/student": [ .get: StudentListView.meta, .post: StudentAddApi.meta, ] // ... ] } ``` 如果是 API 类型接口,则通过 HezeHandler 及其子类来返回内容;如果是网页类型接口,则通过 HezeView 及其子类来渲染网页模板。 ### 模型抽象 模型抽象能力主要实现了数据库以及缓存的屏蔽。每个模型都是`HezeModel`的子类,每个字段都是`HezeModelField`对象。 ```swift class StudentModel: HezeModel { var name = HezeModelField(name: "NAME", type: .string, length: 10) var age = HezeModelField(name: "AGE", type: .int) override func registerFields() -> [HezeModelField] { return [ name, age ] } } ``` 最后进行模型注册。 ```swift func registerModels() -> [HezeMetaModel] { return [ StudentModel.meta, // ... ] } ``` ### 插件 插件主要分两类:过滤器和定时器。 过滤器分为请求过滤器和应答过滤器,过滤器可以在请求的各个阶段修改、拦截请求,以适用于不同的场景。如自定义 404 时,可以通过应答过滤器拦截状态 404 的应答,返回一个自定义的页面。 而定时器则用于处理定时任务,如每日生成报告,每日清理缓存等等。 这些都作为插件进行注册。 ```swift func registerPlugins() -> [HezeMetaPlugin] { return [ CounterTimer.meta, NotFoundFilter.meta // ... ] } ``` ### 上下文 Heze 支持统一进程多实例化,每一个`HezeApp`都使用独立的`HezeContext`,无论是路由、模型还是插件都可以读取自身的上下文来获取当前实例的相关属性。 ### 实例 一个简单的查表 API 只用几行代码就可以实现。 ```swift class PostsListApi: HezeHandler { override func handle(_ req: HTTPRequest, _ res: HTTPResponse) -> HezeResponsable? { guard let posts = PostsModel.query() else { return HezeResponse(false, "内部错误") } return HezeResponse(true, data: posts.render()) } } ``` 渲染一个列表页面也只需要几行代码。 ```swift class StudentListView: HezeListView { override var view: String? { return "student_list.html" } override var modelSet: HezeModelSet { return [ "student_list": StudentModel.query() ?? [] ] } } ``` ### 最后 Heze 已经开源在:[https://github.com/enums/Heze](https://github.com/enums/Heze),包含一个简单的例子。 更多细节请查看源码,请勿使用在商业生产上。 ## SPiCa SPiCa 是前端框架。前端的各能力其实是由 Vue 提供的,SPiCa 负责将业务代码转换为 Vue 代码。(SPiCa 不会开源,因此在这章节里代码相关的内容会比较多) ![](//cdn.blog.yuusann.com/img/posts/19006_2.png) 其难点在于,如何提升用 Swift 写 JS、HTML、CSS 时的体验。 ### 摆脱字符串 我不希望在写业务代码时满篇全是字符串,但前端的接口、属性太多,把这些都封装起来是不现实的。在这里我使用了一个 Swift 4 开始的特性:`dynamicMemberLookup`。 SPiCa 中使用这个特性完成了一个叫`ConstCode`的特性。 ```swift public let c = ConstCode() @dynamicMemberLookup public class ConstCode { public subscript(dynamicMember member: String) -> Code { get { return member } } } ``` 在项目的任何地方,使用`c.hello`就可以得到字符串`hello`了。 当然,很多特殊字符不允许出现在属性中,因此设计了前缀、中缀、后缀替换,如: - `c._not_show` = `!show` - `c._at_click` = `@click` - `c.hello__world` = `hello_world` - `c.hello_spc_world` = `hello world` - `c._100_pct_` = `100%` 这个设计能大幅减少字符串在代码中出现的频率。 ### HTML 代码 先来看一下 HTML 的结构该如何抽象。 ![](//cdn.blog.yuusann.com/img/posts/19006_3.png) 事实上 class 也是一个 property,所以标签里的内容基本上就是个 key-value 的结构,加上一个 addition(极少使用)。 Style 可以看做是 property,也可以看做是一个 CSS。因为其内部的字符串渲染时是根据 CSS 的渲染规则,SPiCa 选择把它当做一个 CSS 看待。 Child 可以是多个,也可以是没有,所以这里是个嵌套自身的数组。 Content 是个字符串,或是没有。 除此之外,还有 Vue 中负责处理过渡动画的`transition`和`transition-group`标签也可以抽象成为这个 HTML 的一个属性,只要最后渲染成代码时候能够辨认出来就行了。 有了这些分析,实现起来就很容易了。 ```swift @dynamicMemberLookup open class HTMLCode: CodeProtocol, HTMLConstructorProtocol { // store property public var _storage: SPiCaKeyPathContent<Code> // tag name public var tag: HTMLTag public var id: HTMLId? public var classes = [HTMLClassNameProtocol]() public var style = CSSCode(CSSNameNull) public var content: Code? public var additions = [Code]() public var children = [HTMLCode]() // dynamicMember(property) 读写 public subscript(dynamicMember member: String) -> Code { get { return _storage[dynamicMember: member] } set(newValue) { _storage[dynamicMember: member] = newValue } } // ... 其他内容以实现更加定制的需求 } ``` 渲染代码时,把这个结构按格式输出成字符串应该不是件难事吧? ### Maker 和 Constructor SPiCa 为了追求更简洁的嵌套,许多接口通过 Maker 来利用尾随闭包以减少括号嵌套。 例如在描述一个 HTML 标签时,设置 style 和添加 child 的接口原型为: ```swift public typealias ViewMaker = () -> View public typealias ViewListMaker = () -> [View] public typealias CSSConstructor = (CSSCode) -> Void @discardableResult open func add(_ maker: ViewMaker) -> Self @discardableResult open func add(_ maker: ViewListMaker) -> Self @discardableResult open func styl(_ constructor: CSSConstructor) -> Self ``` 那么使用时就可以最大限度利用尾随闭包写出漂亮的嵌套: ```swift let view = View(s).cls(s.color_default) .styl { s in s.height = "1rem" s.cursor = c.pointer .add { View(s).ctnt("hello world") .styl { s in ... } .add { ... } } ``` ### HTML 实例 来看一个项目中的实例吧,博客首页的导航按钮: ![](//cdn.blog.yuusann.com/img/posts/19006_4.png) 其前端代码为: ```swift let navi = Row(s).cls(s.size_full, s.color_interactive) .styl {s in s.height = "1.6rem" s.border_top_style = c.solid s.border_bottom_style = c.solid s.border_width = c._1px } .prop { p in p.type = c.flex p.align = c.middle p.justify = c.space__around } .add { [("fa-calendar", "动态聚合", "$router.push('/update/list')"), ("fa-file-text-o", "技术博文", "$router.push('/posts/list')"), ("fa-book", "文集文章", "$router.push('/corpus/list')"), ("fa-area-chart", "数据后台", "$router.push('/report/daily/today')"), ("fa-user", "关于博客", "$router.push('/about')"), ("fa-ellipsis-h", "更多", "$store.commit('showSidebar')"), ].map { icon, text, action in Row(s).cls(s.size_full_h, s.link_button) .styl { s in s.width = "16.6666%" s.padding_top = "0.1rem" } .prop { p in p.type = c.flex p.align = c.middle p.justify = c.center p.vo_click_n = "() => { \(action) }" } .add {[ Icon(icon, s).styl { s in s.font_size = "0.5rem" }, Row(s).cls(s.size_full_w).ctnt(text) .styl { s in s.font_size = "0.3rem" }.prop { p in p.type = c.flex p.align = c.middle p.justify = c.center }, ]} } } ``` 代码中,Row 为标签是`a-row`的 View,Icon 为封装好的用来展示 fontawesome 符号的 View。 渲染后的代码为: ```html <a-row class="size_full color_interactive" style=" border-top-style: solid; border-bottom-style: solid; height: 1.6rem; border-width: 1px" type="flex" justify="space-around" align="middle"> <a-row class="size_full_h link_button" style=" width: 16.6666%; padding-top: 0.1rem" @click.native="() => { $router.push('/update/list') }" justify="center" align="middle" type="flex"> <div class="fa fa-fw fa-calendar" style=" font-size: 0.5rem" display="inline-block" /> <a-row class="size_full_w" style=" font-size: 0.3rem" type="flex" align="middle" justify="center">动态聚合</a-row> </a-row> <a-row class="size_full_h link_button" style=" padding-top: 0.1rem; width: 16.6666%" justify="center" align="middle" @click.native="() => { $router.push('/posts/list') }" type="flex"> <div class="fa fa-fw fa-file-text-o" style=" font-size: 0.5rem" display="inline-block" /> <a-row class="size_full_w" style=" font-size: 0.3rem" type="flex" justify="center" align="middle">技术博文</a-row> </a-row> <a-row class="size_full_h link_button" style=" width: 16.6666%; padding-top: 0.1rem" type="flex" align="middle" @click.native="() => { $router.push('/corpus/list') }" justify="center"> <div class="fa fa-fw fa-book" style=" font-size: 0.5rem" display="inline-block" /> <a-row class="size_full_w" style=" font-size: 0.3rem" align="middle" type="flex" justify="center">文集文章</a-row> </a-row> <a-row class="size_full_h link_button" style=" padding-top: 0.1rem; width: 16.6666%" type="flex" align="middle" justify="center" @click.native="() => { $router.push('/report/daily/today') }"> <div class="fa fa-fw fa-area-chart" style=" font-size: 0.5rem" display="inline-block" /> <a-row class="size_full_w" style=" font-size: 0.3rem" type="flex" align="middle" justify="center">数据后台</a-row> </a-row> <a-row class="size_full_h link_button" style=" width: 16.6666%; padding-top: 0.1rem" type="flex" align="middle" justify="center" @click.native="() => { $router.push('/about') }"> <div class="fa fa-fw fa-user" style=" font-size: 0.5rem" display="inline-block" /> <a-row class="size_full_w" style=" font-size: 0.3rem" type="flex" justify="center" align="middle">关于博客</a-row> </a-row> <a-row class="size_full_h link_button" style=" padding-top: 0.1rem; width: 16.6666%" type="flex" align="middle" justify="center" @click.native="() => { $store.commit('showSidebar') }"> <div class="fa fa-fw fa-ellipsis-h" style=" font-size: 0.5rem" display="inline-block" /> <a-row class="size_full_w" style=" font-size: 0.3rem" type="flex" justify="center" align="middle">更多</a-row> </a-row> </a-row> ``` ### CSS 代码 CSS 代码就比较简单了,是一堆 key-value 组合,以 CSS 的格式进行输出即可。 ```swift @dynamicMemberLookup open class CSSCode: CodeProtocol { // store property public var _storage: SPiCaKeyPathContent<Code> // name public var name: CSSName // ... 其他 } ``` 和 ConstCode 类似的,CSS 也需要一个字符串替换步骤来覆盖一些特殊符号。 - Name 中,`__`中缀替换为`-` - Name 中,`_hover`后缀替换为`:hover` - 属性名中,`_id_`前缀替换为`#` - 属性名中,`_at_`前缀替换为`@` - 属性名中,`_raw_`前缀则原封不动输出后面内容 - ...... 使用 Constructor 的模式能让 CSS 更优雅地被创建,下面来看一个实例。 ### CSS 实例 ```swift let css: CSS = .c { s in s.height = "1rem" s.color = "#f9f9f9" s.background_color = "#006CAC" s.padding_left = "0.2rem" s.padding_right = "0.2rem" s.font_size = "0.4rem" } ``` 代码中的`.c`方法原型为: ```swift public typealias CSSConstructor = (CSSCode) -> Void open class func c(_ constructor: CSSConstructor) -> CSS ``` 快捷创建的方法不需要 CSS 名,创建完后由其他方进行修改。 最后被渲染成代码时为: ```css .index_toolbar { padding-right: 0.2rem; font-size: 0.4rem; padding-left: 0.2rem; height: 1rem; color: #f9f9f9; background-color: #006CAC } ``` ### JS 代码 JS 的结构分为两种形式: - 一行代码的形式,如函数调用 - key-value 形式,即一个对象 因此,实现起来也很简单: ```swift @dynamicMemberLookup open class JSCode: CodeProtocol, ExpressibleByStringLiteral, ExpressibleByBooleanLiteral, ExpressibleByNilLiteral, ExpressibleByFloatLiteral, ExpressibleByIntegerLiteral, ExpressibleByDictionaryLiteral { // 存储 JS 对象形式,如果访问了不存在的键就创建一个空的 public var _storage = SPiCaKeyPathContent<JSCode>(errorHandler: { content, member in let new = JSCode() content._data[member] = new return new }) // 存储一行调用的形式,或 raw code public var _content: Code? // ... } ``` 同样的,这也是个 dynamicMemberLookup 对象,属性可以直接进行赋值。对于函数,SPiCa 设计了一些快捷方法来添加函数,如: ```swift public typealias Code = String public typealias CodeMaker = () -> Code public typealias JSFunctionNameArgsCombine = String open class func raw(_ maker: CodeMaker) -> JSCode { let code = JSCode() code._content = maker() return code } public func f(_ name: JSFunctionName, _ args: JSFunctionArgs, maker: CodeMaker) { f("\(name):\(args)", maker: maker) } public func f(_ nameAndArgs: JSFunctionNameArgsCombine, maker: CodeMaker) { let strs = nameAndArgs.replacingOccurrences(of: " ", with: "").components(separatedBy: ":") let name = strs[0] let args = strs.count > 1 ? strs[1] : "" self[dynamicMember: name] = .raw { """ function(\(args)) { \(maker()) } """ } } ``` 使用时: ```swift let js = JS() // 函数名 参数列表 js.f("say", "name, age") { // 函数体 """ console.log(name + age) """ } ``` 在书写函数体时主要还是以字符串的形式,因此太复杂的逻辑更适合用 VSCode 等工具写在公共的函数库或是 Vuex 的逻辑里。 JS 代码在渲染时保留了换行符,所以最终输出的格式可能会有点混乱,不利于阅读,需自行格式化后才能阅读。 ### Vue 抽象 在有了编写和渲染 HTML、CSS 和 JS 代码的能力以后,就是针对 Vue 的业务抽象了。编写 SPA 时,`index.html`不由 SPiCa 接管,需要手动编写。其余的,从`main.js`开始到各个组件均有 SPiCa 接管。 ### Main.js 这个文件在这里不准备过多披露,无非是固定进行模块 import,路由配置的组装。 ### Vue 组件 一个 Vue 组件由三部分组成:HTML 模板,JS 和 CSS。SPiCa 将这三部分抽象成三份: - `View`:其实是一颗 View 树,根标签作为模板的一部分。 - `Export`:JS 代码的集合。 - `Style`:CSS 的集合。 在定义一个组件时候需要重载以下三个方法来返回组件的内容: ```swift open func makeStyle(s: Style) -> Style open func makeView(s: Style) -> View open func makeExport(s: Style, l: Library) -> Export ``` 构造时的传入组件为全局的 Style 和 Library,方便在组件里使用全局的 CSS 和 JS。 ### Vue 组件实例 来看一个博客项目中的完整例子: ```swift class ProjectListPage: Page { override var name: ComponentName { return "ProjectListPage" } override var title: String { return "项目列表 - 业余项目" } override func makeView(s: Style) -> View { return View(s).cls(s.color_default) .styl { s in s.margin_top = "1.4rem" } .add {[ // head // Navibar 是个 Swift 封装,渲染时会被直接展开,类似 inline Navibar("业余项目", left: ("fa-angle-left", "() => { $router.go(-1) }"), s), // list Row(s) .prop { p in p.v_show = "!isProjectLoading" } .add { // 这里调用了 ProjectCell 组件 View("project-cell", s).trans(s.project_list).prop { p in p.v_for = "project in projectList" p.vb_key = "project.PID" p.vb_pid = "project.PID" p.vo_projectCellClicked = "cellClicked" } }, // 这里调用了 LoadingIcon 组件 View("loading-icon", s) .prop { p in p.v_show = "isProjectLoading" p.vb_iconShow = "isProjectLoading" }, NoMoreView(s).prop { $0.v_show = "!isProjectLoading" } ]} } override func makeExport(s: Style, l: Library) -> Export { return Export(name) { e in e.module("ProjectCell") e.module("LoadingIcon") e.module("FooterBar") e.hooks { h in h.f(.mounted) { """ this.initSelf() $('html, body').scrollTop(0) """ } } e.methods { m in m.f("initSelf") { """ this.$store.commit("showSearchbar") this.$store.commit("showFooterbar") this.$store.commit("fetchProject") """ } m.f("cellClicked", "url") { """ window.open(url, '_blank') """ } } e.computed { c in c.f("projectList") { """ return this.$store.state.project.projectList """ } c.f("isProjectLoading") { """ return this.$store.state.project.isFetching """ } } e.watch { w in w.f("$route", "n, o") { """ if (n.path != "/project/list") { return } this.$store.commit("showSearchbar") this.$store.commit("showFooterbar") """ } } } } override func makeStyle(s: Style) -> Style { return Style() { s in // 转场动画 s.project_list = ListFadeTransition() } } } ``` 渲染后的的 Vue 代码: ```html <template> <div class="color_default" style=" margin-top: 1.4rem"> <a-row class="size_navi_head color_reverse" align="middle" justify="center" type="flex"> <a-row class="navi_button" style=" width: 1.6rem; height: 100%" justify="center" align="middle" type="flex" @click.native="() => { $router.go(-1) }"> <div class="fa fa-fw fa-angle-left" display="inline-block" /> </a-row> <a-row style=" flex-grow: 1" justify="center" type="flex" align="middle">业余项目</a-row> <div style=" height: 100%; width: 1.6rem" /> </a-row> <a-row v-show="!isProjectLoading"> <transition-group name="trans-list-fade" enter-class="trans-list-fade-enter" enter-active-class="trans-list-fade-enter-active"> <project-cell v-bind:key="project.PID" v-bind:pid="project.PID" v-for="project in projectList" v-on:projectCellClicked="cellClicked" /> </transition-group> </a-row> <loading-icon v-bind:iconShow="isProjectLoading" v-show="isProjectLoading" /> <a-row style=" font-size: 0.4rem; height: 1.2rem; color: #959495" v-show="!isProjectLoading" type="flex" align="middle" justify="center">没有更多内容了</a-row> </div> </template> ``` ```javascript <script src="./ProjectCell.vue"></script> <script src="./LoadingIcon.vue"></script> <script src="./FooterBar.vue"></script> <script> import ProjectCell from './ProjectCell.vue' import LoadingIcon from './LoadingIcon.vue' import FooterBar from './FooterBar.vue' export default { name: 'ProjectListPage', props: { }, data: function() { return { } }, computed: { isProjectLoading: function() { return this.$store.state.project.isFetching }, projectList: function() { return this.$store.state.project.projectList } }, methods: { cellClicked: function(url) { window.open(url, '_blank') }, initSelf: function() { this.$store.commit("showSearchbar") this.$store.commit("showFooterbar") this.$store.commit("fetchProject") } }, components: { ProjectCell, LoadingIcon, FooterBar }, watch: { $route: function(n,o) { if (n.path != "/project/list") { return } this.$store.commit("showSearchbar") this.$store.commit("showFooterbar") } }, mounted: function() { this.initSelf() $('html, body').scrollTop(0) } } </script> ``` ```css <style> .trans-list-fade-enter { opacity: 0 } .trans-list-fade-enter-active { transition: all 0.4s } </style> ``` ### 组件注册 SPiCa 也拥有和 Heze 类似的 AppDelegate 机制。 ```swift // 注册公共 CSS 库 public func registerStyle() -> Style { return SharedStyle() } // 注册公共 JS 函数库 public func registerLibrary() -> Library { return SharedLibrary() } // 注册 Vuex public func registerStore() -> Store { return SharedStore() } // 注册非 Page 的组件 public func registerComponents() -> [Component] { return [ ProjectCell.meta, // ... ] } // Page 和路由一起在这里注册 public func registerRoutes() -> [(String, MetaPage, PageLevel)] { return [ ("/", IndexPage.meta, 0), // ... ] } ``` ### 最后 我不计划开源 SPiCa 的重要原因是,它写起来很麻烦。我是处于完全不想写 HTML 的前提,再加上觉得 Vue 的模块管理方式不够 Productive,才开发了这个框架。 在 Blog-web 开发完成之后我进行了代码统计,Swift 的原始代码超过了 10000 行,最终渲染输出的 Vue 代码只有它的一半。 当然,它也是有优点的。除了不用写 HTML,模块管理更加 Productive 以外,还能进行一些代码检查。 如,可以找到代码中错误引用的 class。 ![](//cdn.blog.yuusann.com/img/posts/19006_5.png) # 结语 Project Virgo 从开始有想法到最终上线历时一个半月,这一个半月的业余时间几乎没有干别的,最终它把我的博客变成了一个 PWA,我也从一个快要被时代抛弃的只会手撸 Bootstrap 和 jQuery 的旧时代 Web 开发者变成了半只脚跨进现代前端的开发者。 在五年前我开始学习 Swift 1.1 以前,我没有进行过 iOS 开发;在三年前开始 Swift 后端开发之前,我没有进行过后端开发;在今年开始 SPiCa 项目之前,我没有进行过现代前端的开发,对于前端的接触也仅限于 Bootstrap 和 jQuery 等。 因为有了 Swift,我渐渐地开始接触跨界开发的学习,回头看去,仿佛应征了我选择 Perfect 和 Vue 的理由。冰冻三尺非一日之寒,渐进式对于一个初学者来说是如此重要。 谨以此文总结三年来的 Swift Server-side 开发之路。最后,各位学习的时候注意休息,少熬夜。🧐


评论:4条


1楼:2019-09-12 22:56:43

Alice:

“注意休息少熬夜”?


2楼:2019-10-24 17:50:34

其实就叫众里:

### 感谢分享 听了博客,刷了GitHub,最后来到了这里,感觉很神器。 最初用Swift学习写服务端貌似也是通过你的微博,这次正好有个机会,既可以自由发挥,又允许尝试新技术,所以 Swift Server-side走起?


3楼:2020-03-24 10:14:41

菜鸟666666:

博主牛逼啊。收下我的膝盖。


4楼:2020-05-28 22:31:01

浮生若梦:

您好,我对Vapor4中属性包装器与数据库的交互不理解,能不能出一期关于属性包装器封装数据库操作的文章


返回列表

返回主页