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

发布于:2019-09-12 22:39,阅读数:2794,点赞数: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.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.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.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.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.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中属性包装器与数据库的交互不理解,能不能出一期关于属性包装器封装数据库操作的文章


返回列表

返回归档

返回主页