Vapor(一)基础篇
其实这个Vapor系列的文章和官网介绍的差不多,但为了加深个人理解和方便日后查阅,还是逐点再撸一遍,并且将知识点做了一下归类。过程中会有类比到其它系统、语言,又或者是旧版本的Vapor,分析它们的特性、设计模式等,使学习起来更加融会贯通。
第一篇,先介绍一下Vapor的一些基础概念,像文件结构、服务、路由、异步等等。
文件结构
├── Public
├── Sources
│ ├── App
│ │ ├── Controllers
│ │ ├── Models
│ │ ├── boot.swift
│ │ ├── configure.swift
│ │ └── routes.swift
│ └── Run
│ └── main.swift
├── Tests
│ └── AppTests
└── Package.swift
Public
Public文件夹用于存放样式表、图片和脚本。当Vapor响应一个请求时会优先检查该文件夹里是否有要请求的内容,有则跳过后台应用的逻辑并直接使用该文件返回。例如请求localhost:8080/favicon.ico
,会检查Public/favicon.ico
,有则直接返回。
App
两个存放源码的地方之一,这是最重要的文件夹,所有后台应用的逻辑均在此处
- Controllers:组织应用逻辑的地方,有大量的函数来接收请求和回馈响应。
- Models:存放支持Content类型的模型和Fluent模型的地方。
- boot.swift:后台应用启动后,但在真正运行前,会执行其中的函数。适合将需要在应用启动时初始化的工作放在此处。(可以看作是iOS AppDelegate中的didFinishLaunching方法)
- configure.swift:此处包含一个函数用于接收配置、环境和服务,给后台应用作为输入。适合将注册服务或更改配置放到此处执行。
- routes.swift:配置路由的地方。
AppTests
存放单元测试文件的地方
Package.swift
依赖包清单。
服务(Services)
Vapor使用个 Services 是一个依赖注入式的框架,在应用里可以通过它来注册、配置、初始化任何需要的内容。
Vapor不支持静态访问,若要访问一个类或结构体,则要通过方法或初始化。因为避免静态访问变量有助于使Vapor通过停止使用线程安全锁或信号量的来提升性能。
使用服务好处是能够解耦调用者与实现者的关系,而且Services相当于一个大型的调控中心,将各种服务保存起来,供需要时从这统一调用,注册服务时也统一通过它来进行。
在注册服务的时候可以指定一致的协议
/// Register routes to the router
let router = EngineRouter.default()
try routes(router)
services.register(router, as: Router.self) // services belongs to app
let router = app.make(Router.self)
let engineRouter = app.make(EngineRouter.self)
print(router) // Router (actually EngineRouter)
print(engineRouter) // EngineRouter
print(router === engineRouter) // true
容器(Container)
容器由四个部分组成
- Services
- Config
- Environment
- Worker
Vapor里常用的容器有 Application、Request、Response
通过容器能够创建出服务(遵指定特定协议或类来获得一个对象,前提是容器要预先注册好该服务),因为容器里面有属于它自己的一个Services属性(当然使用Services的单例除外)
/// Register to sevices.
let sqlite = SQLiteDatabase(storage: .memory)
services.register(sqlite) // services belongs to app
let db = app.make(SQLiteDatabase.self)
print(db) // SQLiteDatabase (the one we registered earlier)
let leaf = try req.make(LeafRenderer.self)
print(leaf) /// Definitely a LeafRenderer
let view = try req.make(ViewRenderer.self)
print(view) /// ViewRenderer, might be a LeafRenderer
路由(Route)
在Vapor里默认的路由类是EngineRouter
,也可以自定义路由类,但需要遵从Router
协议。
若自定义了路由类的话,在configure.swift
中应使用自定义路由类创建对象来注册服务。
在routes.swift
的配置路由的函数routes中使用入参router配置路由时,支持用英文逗号分隔开路径,或直接使用反斜杠连接显示
router.get("path", "to", "something") {...}
router.get("path/to/something") {...}
可以将路由的实现交由controller的方法完成
let helloController = HelloController()
router.get("greet", use: helloController.greet)
final class HelloController {
func greet(_ req: Request) throws -> String {
return try req.make(BCryptHasher.self).hash("hello")
}
}
获取参数
router.get("users", Int.parameter) { req -> String in
let id = try req.parameters.next(Int.self)
return "requested id #\(id)"
}
GET /users/:id
环境(Environment)
通过环境的设置,能赋予应用方便地动态切换情景,执行不一样的(配置、服务、初始化)版本,在启动App时指定
swift run Run --env prod
容器在默认环境下是.development,可以通过.environment属性获取,一般用在configure.swift
中使用
services.register { container -> BCryptConfig in
let cost: Int
switch container.environment {
case .production: cost = 12
default: cost = 4
}
return BCryptConfig(cost: cost)
}
let sqlite: SQLiteDatabase
if env.isRelease {
/// Create file-based SQLite db using $SQLITE_PATH from process env
sqlite = try SQLiteDatabase(storage: .file(path: Environment.get("SQLITE_PATH")!))
} else {
/// Create an in-memory SQLite database
sqlite = try SQLiteDatabase(storage: .memory)
}
services.register(sqlite)
配置(Config)
当有多个服务遵从同一个协议时,需要使用Config
结构体来声明该协议优先对应的服务
/// Declare preference for MemoryKeyedCache anytime a container is asked to create a KeyedCache
config.prefer(MemoryKeyedCache.self, for: KeyedCache.self)
/// ...
/// Create a KeyedCache using the Request container
let cache = req.make(KeyedCache.self)
print(cache is MemoryKeyedCache) // true
内容(Content)
在Vapor 3中,JSON, protobuf, URLEncodedForm, Multipart等,所有的Content类型都一样,需要序列化和解析的Content都要是一个Codable的类或结构体(即是要遵从Content
协议)。
像下面返回Response的例子中,因为User遵从了Content
协议,所以在下面注册路由里的实现能直接返回User对象作为Response,回调得到的User对象会被调用其Content
协议中的方法进行编码成JSON返回
//HTTP响应
HTTP/1.1 200 OK
Content-Type: application/json
{
"name": "Vapor User",
"email": "user@vapor.codes"
}
//预期的结构体或类
struct User: Content {
var name: String
var email: String
}
//编码HTTP响应
router.get("user") { req -> User in
return User(
name: "Vapor User",
email: "user@vapor.codes"
)
}
Request的用法也差不多,指定需要解码出的对象类型,就能从JSON转换成对应的模型对象
//HTTP请求
POST /login HTTP/1.1
Content-Type: application/json
{
"email": "user@vapor.codes",
"password": "don't look!"
}
//预期的结构体或类
struct LoginRequest: Content {
var email: String
var password: String
}
//解码HTTP请求
router.post("login") { req -> Future<HTTPStatus> in
return req.content.decode(LoginRequest.self).map(to: HTTPStatus.self) { loginRequest in
print(loginRequest.email) // user@vapor.codes
print(loginRequest.password) // don't look!
return .ok
}
}
异步(Async)
当应用启动时通常会创建一个事件循环给运行它的每个CPU核心。每个事件循环有一条线程。但和Node.js中的只能运行一个事件循环不同,Vapor基于Swift支持多线程的特性,其能够在一个进程中运行多个事件循环,而其它方面则都非常地相似。
每一次有客户端连接服务器时,服务器就能够指派一个事件循环给客户端进行通信,并且均在此同一个事件循环上进行。
事件循环负责跟踪每一个连接了的客户状态。如果有一个等待服务端接收的请求,事件循环会触发接收通知来促使数据被接收。当整个请求被接收完毕后,某个等待返回请求数据future就会被触发完成。
核心就是利用Future<T>
这样一个类型
- 去接收异步任务的返回;
- 执行指定的回调任务;
- 实现链式响应。
第一个最简单的例子,futureString获得等待异步任务返回的任命后,在它身上设置上异步任务返回后需要执行的回调任务,指定的方法就是.map(to:),参数是说明回调执行后的返回值,闭包中的string其实就是futureString收到异步返回后的真正结果。
但大家可能会有疑问,若就在这么一行代码之间,设置回调前异步的任务就已经返回了的话,后面的设的回调会不会没有机会执行了,不用慌,.map(to:)方法里有状态判断到底futureString等待的异步任务是否返回了,若已经返回了的话当然设置完回调就立马执行了。
还有一个要注意的点,.map(to:)返回的结果也是Future<T>
,其中的T就是to参数中的类型了,继续返回Future的类型是为了下面会说到链式响应而设计的。当你还要用这个最后的future结果处理其它逻辑时,可继续给它指定回调任务,如此类推地设置你需要在第一级异步任务执行后的一系列任务。
/// Assume we get a future string back from some API
let futureString: Future<String> = ...
/// Map the future string to an integer
let futureInt = futureString.map(to: Int.self) { string in
print(string) // The actual String
return Int(string) ?? 0
}
/// We now have a future integer
print(futureInt) // Future<Int>
第二个例子,当回调里的任务也是异步的怎么办?最后得到的结果岂不是变成Future<Future
为了避免这个问题,Vapor定义了 .flatMap(to:) 这样一个方法,直译就是“平面映射”。原理应该是当前future在执行其回调闭包后,获得返回结果也是future对象时,会等待该返回的future等待的异步任务完成后,再将其t对象拿出传入下一个future的回调闭包中去。
反正就是若回调闭包中返回Future类型的结果,则需要使用.flatMap(to:)方法设置回调。
/// Assume we get a future string back from some API
let futureString: Future<String> = ...
/// Assume we have created an HTTP client
let client: Client = ...
/// Flat-map the future string to a future response
let futureResponse = futureString.flatMap(to: Response.self) { string in
return client.get(string) // Future<Response>
}
/// We now have a future response
print(futureResponse) // Future<Response>
Future的链式响应
/// Assume we get a future string back from some API
let futureString: Future<String> = ...
/// Assume we have created an HTTP client
let client: Client = ...
/// Transform the string to a url, then to a response
let futureResponse = futureString.map(to: URL.self) { string in
guard let url = URL(string: string) else {
throw Abort(.badRequest, reason: "Invalid URL string: \(string)")
}
return url
}.flatMap(to: Response.self) { url in
return client.get(url)
}
print(futureResponse) // Future<Response>
变换,用来修改future对象的值,一般用于修改Future
/// Assume we get a void future back from some API
let userDidSave: Future<Void> = ...
/// Transform the void future to an HTTP status
let futureStatus = userDidSave.transform(to: HTTPStatus.ok)
print(futureStatus) // Future<HTTPStatus>
Future对象一般是通过Promises类来创建,但大部分情况下,Promises不需我们管理,而在Vapor的API里完成创建Future对象并返回
Application、Request、Response这三个类同时也是最常用的Worker,所谓Worker就是执行异步任务或者访问事件循环。
/// Assume we have a Request and some ViewRenderer
let req: Request = ...
let view: ViewRenderer = ...
/// Render the view, using the Request as a worker.
/// This ensures the async work happens on the correct event loop.
///
/// This assumes the signature is:
/// func render(_: String, on: Worker)
view.render("home.html", on: req)
若没有多重的异步任务执行时,可以使用Future简单的do/catch方法来等待异步的返回即可
/// Assume we get a future string back from some API
let futureString: Future<String> = ...
futureString.do { string in
print(string) // The actual String
}.catch { error in
print(error) // A Swift Error
}
也可以使用always方法来执行异步返回后的回调,无论成功与否
/// Assume we get a future string back from some API
let futureString: Future<String> = ...
futureString.always {
print("The future is complete!")
}
同步等待,注意千万不要用在路由的闭包里,因为在事件循环中阻塞了线程,会使所有用户都无法访问,直到阻塞结束。
/// Assume we get a future string back from some API
let futureString: Future<String> = ...
/// Block until the string is ready
let string = try futureString.wait()
print(string) /// String
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 mingfungliu@gmail.com
文章标题:Vapor(一)基础篇
文章字数:3k
本文作者:Mingfung
发布时间:2018-08-13, 23:34:00
最后更新:2018-08-28, 21:59:23
原始链接:http://blog.ifungfay.com/后端/Vapor(一)基础篇/版权声明: "署名-非商用-相同方式共享 4.0" 转载请保留原文链接及作者。