Vapor(一)基础篇

  1. 文件结构
  2. 服务(Services)
  3. 容器(Container)
  4. 路由(Route)
  5. 环境(Environment)
  6. 配置(Config)
  7. 内容(Content)
  8. 异步(Async)

其实这个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" 转载请保留原文链接及作者。

目录
×

喜欢就点赞,疼爱就打赏

宝贝回家