从开放源代码,到支持跨平台,再到出现了诸如Perfect,Vapor这样的服务端开发框架。可以说,近两年来,Swift在服务端开发领域取得了巨大的进步。如果,我们已经掌握了Swift编程语言,那么,用它来学习服务端开发,就变成了一个性价比很高的事情。
如果你之前没经历过服务端开发,没关系,在这个系列里,我们会从零开始,基于Swift Vapor,详细地讲述每一个服务端开发用到的技术细节。如果你有过诸如Node / PHP / Ruby等弱类型语言服务端开发框架的经验,也不妨体会下使用Swift这种静态类型编程语言进行服务端开发的感觉,这种差异微妙并且有趣。
好了,废话不多说,本着一直以来我们Learn by Doing的原则,第一件事,当然就是安装Vapor,并创建自己的工作环境。
安装Vapor
无论你在Mac还是Linux上安装Vapor,首先要做的,是确认一下当前的Swift环境是否兼容Vapor,在Terminal直接执行:
eval "$(curl -sL check.vapor.sh)"
就好了。如果一切正常,在Mac OS环境下,会看到类似下面的结果:
Xcode 9 is compatible with Vapor 2.
Xcode 9 is compatible with Vapor 3.
Swift 4.1 is compatible with Vapor 2.
Swift 4.1 is compatible with Vapor 3.
如果你在Linux环境安装Vapor,就不会有Xcode的兼容性检查了,只有Swift版本的检查。
接下来,就可以安装Vapor了,简单说,它是一个帮助我们管理各种服务端项目的工具。在Terminal,执行下面的命令:
brew install vapor/tap/vapor
在Debian环境,可以执行sudo apt-get install vapor -y进行安装。
这样,所有的准备工作就完成了。接下来,我们就通过一个简单的项目对所谓的服务端开发有一个感性的了解。
第一个服务端项目
准备一个目录,在其中执行下面的命令创建一个服务端API的模板:
vapor new HelloVapor --api --branch=beta
由于Vapor 3.0还没有正式发布,因此我们使用了--branch=beta
选项。成功后,就会看到类似下面这样的提示:

Vapor会创建一个叫做HelloVapor的目录,并在其中添加相关的项目文件。
上面
--api
是指创建一个HTTP API项目的模板,这也是默认的项目类型。实际上Vapor还支持一些其他类型的模板。我们可以执行vapor --help
查看。随着我们对Vapor的深入了解,后面会用到这些不同的类型。
了解项目构成
这个API的项目模板,看上去是这个样子的:

Package.swift
实际上,这就是一个通过SPM创建的项目。因此,我们先来看下Package.swift:
// swift-tools-version:4.0
import PackageDescription
let package = Package(
name: "HelloVapor",
dependencies: [
.package(url: "https://github.com/vapor/vapor.git",
from: "3.0.0-rc.2"),
.package(url: "https://github.com/vapor/fluent-sqlite.git",
from: "3.0.0-rc.2")
],
targets: [
.target(name: "App", dependencies: ["FluentSQLite", "Vapor"]),
.target(name: "Run", dependencies: ["App"]),
.testTarget(name: "AppTests", dependencies: ["App"])
]
)
可以看到,这个项目有两个依赖关系,一个是vapor,这是Vapor的核心代码;一个是fluent-sqlite,这是Vapor的ORM,等我们讲到和数据库相关的部分时,才会用到它。接下来,是项目的两个target。其中,App用于构建我们的API程序,Run用于执行。最后,是AppTests,用于测试。
其它的项目文件和目录
了解完Package.swift之后,我们看一下Vapor创建的其它文件和项目目录:
- Public:这里存放的是Web服务所需的静态资源,例如:图片、CSS或JS等;
- Sources:这是我们工作的主要目录,几乎全部服务端的代码都保存在这个目录里。其中,Sources/Run中存放的是Vapor程序的启动代码,通常我们不需要修改它;而Sources/App则是我们添加各种代码的地方;
- Tests:顾名思义,这是保存各种测试用例的目录;
除此之外,Vapor还会在HelloVapor中创建一些隐藏的文件和目录。例如,Vapor已经自动为我们创建了一个本地的git repository,以及.gitignore文件:
### Vapor ###
Config/secrets
### Vapor Patch ###
Packages
.build
xcuserdata
*.xcodeproj
DerivedData/
.DS_Store
# End of https://www.gitignore.io/api/vapor
其中包含了服务器的相关秘钥、SPM安装的各种依赖组件、生成的结果以及Xcode相关的项目文件等。显然,它们都不应该或没必要托管到repository中。
生成Xcode项目文件
对项目模板有了一个大体的了解之后,我们就该选择一个编辑器或者IDE来进行开发了。这里还是推荐大家使用Xcode或者AppCode,毕竟对于Vapor这个比较陌生的东西,这些IDE能给予的各种提示,在初期还是非常方便的。
为了可以使用Xcode打开Vapor生成的项目,我们可以在HelloVapor目录,执行:vapor xcode
,生成Xcode项目文件之后,Vapor会提示我们是否用Xcode打开:

生成项目的命令可能会执行一段时间,大家耐心等待就好。
输入y,就可以用Xcode打开这个Vapor项目了。此时,之前提到的Sources目录,就在下图中的红框里:

这时,直接按Cmd + B
应该可以成功通过编译。
根据Swift以及Vapor版本的差异,可能在构建的过程中会有一些警告,这应该不会影响正常的功能。
但是,按Cmd + R
执行,Xcode却没有任何反应,这是因为Xcode默认的target,是Package.swift
中的App
,我们在Scheme中,改成Run
就好了:

这时,按Cmd + R
执行,就会在控制台看到下面的提示:

这表示,Vapor已经开始工作了,我们可以直接访问http://localhost:8080/hello
,就会看到下面的结果:

这样,Vapor初期的开发环境就准备好了。不过,有一点需要注意的是,当通过Xcode进行开发的时候,最好通过Xcode来管理项目中的文件,而不要直接在Terminal或Finder中创建文件,这种在外部添加的文件,不会被Xcode自动包含到项目中,我们还是要手工拖动进来。
理解Vapor的启动过程
在正式开始动手之前,我们来了解一下Vapor大体的执行过程。这一切,都是从Sources/Run/main.swift开始的。
首先,是引入的modules:
import App
import Service
import Vapor
import Foundation
这里,App
就是SPM中创建的App Module,也就是Sources/App这个文件夹的内容,Service
和Vapor
是Vapor自身的两个Module,提供了Vapor的核心功能,Foundation
则是Mac OS自身的Library。
其次,Vapor分别定义了表示服务端配置、执行环境以及相关服务的对象,然后,用App.configure
方法加载了这些内容:
var config = Config.default()
var env = try Environment.detect()
var services = Services.default()
try App.configure(&config, &env, &services)
我们先不用管这些方法的首先细节,只要从语义上明白这些代码的功能就好了。
第三,我们用config
,env
和services
创建了一个Application
对象,它代表正在执行的Vapor程序:
let app = try Application(
config: config,
environment: env,
services: services
)
第四,把这个app
对象传递给App module中的boot
方法:
try App.boot(app)
这个boot
就定义在Sources/App/boot.swift中,默认情况,这是一个空函数:
public func boot(_ app: Application) throws {
// your code here
}
之所以要这样做,是Vapor给了我们一个机会,让程序开始执行之前,做一些必要的准备工作。当然现在我们不用做什么,因此让它留空就好了。
最后,执行app.run()
启动Vapor,这样我们的应用就就准备就绪,可以接受http请求了。
理解了Vapor启动过程之后,我们再来看一下Vapor的配置过程,这部分代码在Sources/App/configure.swift中:
public func configure(
_ config: inout Config,
_ env: inout Environment,
_ services: inout Services
) throws {
/// Register providers first
try services.register(FluentSQLiteProvider())
/// Register routes to the router
let router = EngineRouter.default()
try routes(router)
services.register(router, as: Router.self)
/// ...
}
为了简单,我们只节选了一部分代码。这里,用services.register
的形式,我们向Vapor注册了一些功能,例如一开始说的FluentSQLite以及EngineRouter,这些注册进来的功能,就叫做Vapor中的Services。这里,EngineRouter
是Vapor中专门处理路由的服务。所谓路由,就是把访问的URL和对应的程序逻辑对应起来的过程。简单说,就是之前访问/hello
,得到字符串"Hello world!"这样的过程。
自定义路由
接下来,我们修改一下之前访问/hello
的路由,让它可以接受一个参数,并根据我们传递的参数动态返回响应的内容。访问每一个URL时,对应的执行逻辑,是在Sources/App/routes.swift中定义的,Vapor默认创建的是这样的:
public func routes(_ router: Router) throws {
router.get("hello") { req in
return "Hello, world!"
}
}
这里,router
就是我们之前在configure
中注册的EngineRouter
对象,get
表示处理HTTP GET请求,这个方法接受两个参数:第一个参数表示请求的URL,第二个参数表示接受到请求之后执行的逻辑。这就是为什么我们访问/hello
可以得到Hello, world!的原因。
现在,我们要修改一下这个路由,让这个GET /hello
请求可以接受一个参数,例如:/hello/Mars
。当我们在服务端接受到这个参数之后,动态返回一个欢迎信息。把之前处理/hello
的路由改成这样:
router.get("hello", String.parameter) {
req -> String in
let name = try req.parameters.next(String.self)
return "Hello, \(name)!"
}
这次,get
接受了第二参数,String.parameter
是一个PathComponent
对象,表示URL中的一部分。而String.parameter
就表示URL中的一段字符串,当然也可以理解成是我们添加在GET请求中的参数。例如:GET /hello/Mars
,此时的String.parameter
就是Mars。
接下来,在它的closure里,我们可以通过req.parameters.next()
这样的方式获取添加在URL后面的参数,后面跟了几个参数,我们就可以调用几次next()
,这样就可以在服务端依次得到客户端传递的参数了。这里由于我们只打算通过/hello
传递一个参数,因此,只调用了一次next
。
得到了name
之后,我们生成了一个特定的欢迎信息Hello, \(name)!
,这样传递不同的请求,我们在网页中看到的欢迎信息就可以动态变化了。
What's next?
以上,就是关于Vapor的一个感性的体验。我们开发了一个只能接受一个GET请求的HTTP API。虽然它很简单,但却表达了一些很重要的概念。接下来,我们就围绕着服务端开发要面对的常见场景,来逐步深入了解Vapor相关组件的用法。