Vapor学习之Content

Content

Vapor3 中所有的 content 类型(JSONprotobufFormURLEncodedMultipart等等)都是被同等对待的。你只需要解析和序列化 content 即可,该 content 必须是一个实现了Codable的类或结构体
为了方便介绍,我们将以 JSON为例。但是注意这个 API 对任何支持 content 类型的都是一样的。

Request

如何解析下面的这个 HTTP request。

POST /login HTTP/1.1
Content-Type: application/json

{
    "email": "user@vapor.codes",
    "password": "don't look!"
}

一、创建一个结构体或类,用来表示你期望的数据

import Vapor

struct LoginRequest: Content {
    var email: String
    var password: String
}

二、让该结构体或类实现Content,可以开始decode 这个HTTP request了

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
    }
}

这里我们使用.map(to:) 来返回一个 future 。

提示: 从请求解码这个过程是异步的,因为HTTP允许使用分块传输编码方式将主体分为多个部分

Router

为了使传入请求的内容解码更加容易,Vapor在 Router 中提供了一些自动完成此功能的扩展。

router.post(LoginRequest.self, at: "login") { req, loginRequest in
    print(loginRequest.email) // user@vapor.codes
    print(loginRequest.password) // don't look!
    return HTTPStatus.ok
}

Detect Type (检测类型)

由于本例中的HTTP请求中我们将JSON声明为其内容类型,因此Vapor会自动使用JSON解码器。 对于以下请求,同样的方法也可以正常工作

POST /login HTTP/1.1
Content-Type: application/x-www-form-urlencoded

email=user@vapor.codes&don't+look!

所有HTTP请求都必须包含一个内容类型才能生效。 因此,如果遇到未知类型,Vapor将自动选择适当的解码器或者抛出错误。

提示: > 你可以 配置 Vapor使用的默认编码器和解码器

Custom

如果需要你可以重写Vapor的编解码器,并传入

let user = try req.content.decode(User.self, using: JSONDecoder())
print(user) // Future<User>

Response

如何创建 HTTP response

HTTP/1.1 200 OK
Content-Type: application/json

{
    "name": "Vapor User",
    "email": "user@vapor.codes"
}

一、 decoding 一样,首先创建一个结构体或类,用来表示你期望的数据

import Vapor

struct User: Content {
    var name: String
    var email: String
}

二、让结构体或类实现Content,可以开始 encode 这个 HTTP response了

router.get("user") { req -> User in
    return User(
        name: "Vapor User",
        email: "user@vapor.codes"
    )
}

这会创建一个默认的 带有200 OK 状态码和最小标题的 Response 。你可以通过便利方法encode(...) 来自定义响应体

router.get("user") { req -> Future<Response> in
    return User(name: "Vapor User", email: "user@vapor.codes")
        .encode(status: .created)
}

Override Type

Response的内容默认会编码成JSON,你可以通过 as: 并传入参数来改变编码方式

try res.content.encode(user, as: .urlEncodedForm)

你也可以修改类或者结构体使用的媒体类型

struct User: Content {
    /// See `Content`.
    static let defaultContentType: MediaType = .urlEncodedForm

    ...
}

Client

编码一个有Client 发送的请求内容和编码服务器HTTP响应的方式类似


Request

我们来看看如何编码下面请求

POST /login HTTP/1.1
Host: api.vapor.codes
Content-Type: application/json

{
    "email": "user@vapor.codes",
    "password": "don't look!"
}

Encode

一、首先创建一个结构体或者类来实现你期望的数据类型

import Vapor

struct LoginRequest: Content {
    var email: String
    var password: String
}

二、现在我们已经准备好发起我们的请求了,假设我们在一个路由闭包内发起这个请求,我们将使用传入的请求作为一个容器

let loginRequest = LoginRequest(email: "user@vapor.codes", password: "don't look!")
let res = try req.client().post("https://api.vapor.codes/login") { loginReq in
    // encode the loginRequest before sending
    try loginReq.content.encode(loginRequest)
}
print(res) // Future<Response>

Response

继续我们在 encode 部分的例子,让我们看看如何解码来自client的Response

HTTP/1.1 200 OK
Content-Type: application/json

{
    "name": "Vapor User",
    "email": "user@vapor.codes"
}

一、首先要创建一个结构体或类来表示期望的数据类型

import Vapor

struct User: Content {
    var name: String
    var email: String
}

二、Decode
现在我们准备好解码 Client 响应了

let res: Future<Response> // from the Client

let user = res.flatMap { try $0.content.decode(User.self) }
print(user) // Future<User>

Example

现在让我们看看完整的 Client 请求,它们都会对内容进行编解码

// 创建 LoginRequest 数据
let loginRequest = LoginRequest(email: "user@vapor.codes", password: "don't look!")
// POST /login
let user = try req.client().post("https://api.vapor.codes/login") { loginReq in 
    // 请求发送前编码
    return try loginReq.content.encode(loginRequest) 
}.flatMap { loginRes in
    // 接收到响应后解码
    return try loginRes.content.decode(User.self) 
}
print(user) // Future<User>

Query String

URL-Encode的表单数据也可以像Content那样通过HTTP请求的URI query string来编解码. 所有你需要做的就是创建一个类或者结构体并且遵守 Content 协议。在这个例子里我们将使用下面的结构体

struct Flags: Content {
     var search: String?
     var isAdmin: Bool?
}

Decode

所有的 Request 都有一个 QueryContainer 你可以解码 query string

let flags = try req.query.decode(Flags.self)
print(flags) // Flags

Encode

你也可以编码内容。这在使用 Client 来编码查询字符串时很有用

let flags: Flags ...
try req.query.encode(flags)

Dynamic Properties

关于Content的最多的问题是:

如何添加属性到单独的一个Response

Vapor3处理 Content 的方式完全基于 Codable。在任何时候(可公开访问的),你的数据都应该在一个类似 [String:Any]的数据结构中,你可以按你期望的方式修改它。因此,你的App接收和返回的数据结构都必须是静态定义的。

让我们来看看一个常见的场景,以便更好的理解这一点。通常你创建一个用户时,需要几种不同的数据格式:

  • create: 提供两次密码以便检查是否匹配
  • internal: 你应该存储一个散列函数而不是明文密码
  • public: 列出用户时不应该包含密码的哈希值

为了实现这个,你需要创建三种类型的数据.

// Data required to create a user
struct UserCreate: Content {
    var email: String
    var password: String
    var passwordCheck: String
}

// Our internal User representation
struct User: Model {
    var id: Int?
    var email: String
    var passwordHash: Data
}

// Public user representation
struct PublicUser: Content {
    var id: Int
    var email: String
}

// Create a router for POST /users
router.post(UserCreate.self, at: "users") { req, userCreate -> PublicUser in
    guard userCreate.password == passwordCheck else { /* some error */ }
    let hasher = try req.make(/* some hasher */)
    let user = try User(
        email: userCreate.email, 
        passwordHash: hasher.hash(userCreate.password)
    )
    // save user
    return try PublicUser(id: user.requireID(), email: user.email)
}

对于其它方法,例如 PATCHPUT ,你或许需要创建更多的类型来支持唯一语义

好处 Benefits

与动态解决方案相比,这种方法看起来可能有点冗余,但它拥有很多关键优势:

  • 静态类型 : 基于Swift和Codable,
  • 可读性 : 使用Swift类型时,无需使用字符串和可选链
  • 可维护性 : 在大型项目中,信息的分离使项目非常干净
  • 可共享 : 定义路由所接受,返回的类型,可用于OpenAPI规范,甚至可以直接和客户端共享
  • 性能 : 使用Swift的类型,比使用[Sting:Any]字典性能更好

JSON

JSON是一种非常流行的API编码格式,日期,数据,浮点型等编码方式是非标准的。 正因为如此,使用自定义的JSONDecoder可以让Vapor在与其他API进行交互时更很容易。

// Conforms to Encodable
let user: User ... 
// Encode JSON using custom date encoding strategy
try req.content.encode(json: user, using: .custom(dates: .millisecondsSince1970))

也可以用下面方法解码

// Decode JSON using custom date encoding strategy
let user = try req.content.decode(json: User.self, using: .custom(dates: .millisecondsSince1970))

如果你想在全局使用自定义JSON编解码器,你可以在 configuration 中进行

Configure

使用 ContentConfig 来注册应用程序的编解码器,这些编解码器将用于任何您使用 content.encode/content.decode 的地方

/// Create default content config
var contentConfig = ContentConfig.default()

/// Create custom JSON encoder
var jsonEncoder = JSONEncoder()
jsonEncoder.dateEncodingStrategy = .millisecondsSince1970

/// Register JSON encoder and content config
contentConfig.use(encoder: jsonEncoder, for: .json)
services.register(contentConfig)
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,539评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,594评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,871评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,963评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,984评论 6 393
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,763评论 1 307
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,468评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,357评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,850评论 1 317
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,002评论 3 338
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,144评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,823评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,483评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,026评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,150评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,415评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,092评论 2 355

推荐阅读更多精彩内容