使用 Siesta 处理 Swift 网络请求

孟宗
• 阅读 2834

(原文地址:https://medium.freecodecamp.o...

今天我跟大家分享一下我的 iOS 网络库新欢,名字叫做 Siesta。“她有啥特殊的?为啥我不直接用 Almofire?”你也许会问。事实上,你仍然可以把 Alamofire 和 Siesta 一起使用!它是客户端之上的网络抽象层。

和 Moya 不同,Siesta 不会隐藏 HTTP。这种中间状态,是我使用 Siesta 构建 REST API 的理由。

通过资源为中心而不是请求为中心的设计,Siesta 提供一个全局的符合 RESTful 的可被观察的模型。

这意味着什么?一些非必要的网络和反序列化操作被大量减少,视图控制器和网络请求之间的关系被解耦。此外,它的响应解析十分透明,开箱即用。

这篇教程里,我将展示给你如何通过使用 Siesta,让你的网络处理代码变得更加 Swiftly。

初始化

从 Cocoapods 安装:

pod 'Siesta', '~> 1.0'

为了演示本教程,我将编写一个简单的 CRUD 应用程序配合 REST API 和 我部署到 HeroKu 上基于 JWT 的验证。

首先,创建一个名为 AwesomeAPI.swift 的文件。

定义基本的 API 配置:


import Siesta

let baseURL = "https://jwt-api-siesta.herokuapp.com"

let AwesomeAPI = _AwesomeAPI()

class _AwesomeAPI {
    
    // MARK: - Configuration
    
    private let service = Service(
        baseURL: baseURL,
        standardTransformers: [.text, .image]
    )
    
    fileprivate init() {
        // –––––– Global configuration ––––––
        
        #if DEBUG
            LogCategory.enabled = [.network]
        #endif
    }

    // MARK: - Resource Accessors
    func ping() -> Resource {
        return service.resource("/ping")
    }
}

我们在此定义了全局使用的单例 API 对象。我们配置服务的地址,还有standardTransforms (定义类型的转换标准),它提供了对文本类型、图片类型响应的解析。然后我们打开了 debug 模式,在调试 API 时这很有用。最后,我们定义了 resource accessor(资源访问)。一个访问我们API 的方法返回一个我们在 ViewController 中使用的资源对象。

从资源对象中访问网络并读取数据,我们需要在 ViewController 中创建一个观察者:


import Siesta

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()

        AwesomeAPI.ping().addObserver(self)
    }

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)

        AwesomeAPI.ping().loadIfNeeded()
    }
}

extension ViewController: ResourceObserver {
    func resourceChanged(_ resource: Resource, event: ResourceEvent) {
        if let text = resource.latestData?.text {
            print(text)
        }
    }
}

我们给ping返回的资源添加了一个观察者,并定义好了代理,当资源的状态改变时,代理会被调用。当收到新数据和被资源被添加时,资源的状态都会改变。

Siesta 支持对请求初始化和配置进行解耦,所以在请求资源的时候,不用担心过多关于请求具体的细节。

比如,你无需担心loadIfNeeded被调用的太频繁,Siesta 允许你在指定时间内忽略重复的请求。默认时间是30秒,值可配置。

现在如果你运行程序,你可能将看到类似这样的输出:

Siesta:network        │ GET https://jwt-api-siesta.herokuapp.com/ping
Siesta:network        │ Response:  200 ← GET https://jwt-api-siesta.herokuapp.com/ping
pong

转换器

让我们再做点有意思的。定义一些转换器可以实现自动解析原始 JSON 数据到一个模型对象。

/status 返回:

{
  "text": "ok"
}

我们使用 JSONDecoder 在后台对 JSON 进行解析,这是一个在 Swift 4 的新加入的。

首先,我们添加转换器:

fileprivate init() {

    ...
    
    let jsonDecoder = JSONDecoder()

    // –––––– Mapping from specific paths to models ––––––
    service.configureTransformer("/status") {
        try jsonDecoder.decode([String: String].self, from: $0.content)
    }
}

// MARK: - Resource Accessors
func status() -> Resource {
    return service.resource("/status")
}

[String: String] 意味着我们期待在我们的 JSON 响应对象中,返回一个 string-to-string 映射的字典。

然后我们对 ViewController 中观察方法进行更新。

func resourceChanged(_ resource: Resource, event: ResourceEvent) {
    if let status: [String: String] = resource.typedContent() {
        print("\(status)")
    }
}

你可能注意到了,解析一个 JSON 我们使用 typedContent(),它返回一个可选值,解包后使用。注意我们需要明确提供数据类型([String: String]),这里的数据类型不能被推倒出来。同样的,对 /ping 的调用修改如下:

if let text: String = resource.typedContent() {
    print(text)
}

验证

在我们的 API 中,我们有两个需要验证权限的接口:incomesexpenses。他们需要认证权限,所以我们需要先获得 JWT token。我们来增加认证方法。这里没有采用增加一个方法去返回带有认证信息的资源,而是把验证信息增加到每个请求中。

首先,增加一个属性,它将存储JWT token用于验证。

private var authToken: String? {
    didSet {
        service.invalidateConfiguration()

        guard let token = authToken else { return }

        let jwt = try? JWTDecode.decode(jwt: token)
        tokenExpiryDate = jwt?.expiresAt
    }
}

这个属性被赋值的时候,我们将当前的配置作废掉,这样做是必须的,当下一次资源(resource)被获取的时候,请求的头会被刷新。刚刚配置的最新的 token 会被放到 HTTP 头中。

还需要考虑将 token 存储到钥匙串而不是 NSUserDefaults 或者其他不安全的存储方式。我们这里使用 JWTDecode 来解析 JWT token 和过期时间。

接下来,我们想在 token 过期的时候自动刷新。更成熟的设计是提供有一个专门刷新 token 的接口,调用它去刷新 token。在我们的例子中,我们考虑一个简化的实现,只是重新发送一次登录请求。

下面是发送登录请求并得到 token 的代码:

@discardableResult func login(_ email: String, _ password: String, onSuccess: @escaping () -> Void, onFailure: @escaping (String) -> Void) -> Request {
    let request = service.resource("/login")
        .request(.post, json: ["email": email, "password": password])
        .onSuccess { entity in
            guard let json: [String: String] = entity.typedContent() else {
                onFailure("JSON parsing error")
                return
            }

            guard let token = json["jwt"] else {
                onFailure("JWT token missing")
                return
            }

            self.authToken = token
            onSuccess()
        }
        .onFailure { (error) in
            onFailure(error.userMessage)
    }

    return request
}

我们发送一个携带用户验证信息的 POST 请求给/login。在onSuccessonFailure两个方法中处理返回信息,如果验证成功,则存储起来。

最后,我们来实现在过期之前更新用户验证信息。使用计时器来实现:

private var refreshTimer: Timer?

public private(set) var tokenExpiryDate: Date? {
    didSet {
        guard let tokenExpiryDate = tokenExpiryDate else { return }

        let timeToExpire = tokenExpiryDate.timeIntervalSinceNow

        // try to refresh JWT token before the expiration time
        let timeToRefresh = Date(timeIntervalSinceNow: timeToExpire * 0.9)

        refreshTimer = Timer.scheduledTimer(withTimeInterval: timeToRefresh.timeIntervalSinceNow, repeats: false) { _ in
            AwesomeAPI.login("test", "test", onSuccess: {}, onFailure: { _ in })
        }
    }
}

我们测试接口的验证信息为testtestAwesomeAPI.login()很容易集成进 ViewController。解析登录请求返回的信息,同样需要定义一个转换器:

service.configureTransformer("/login", requestMethods: [.post]) {
    try jsonDecoder.decode([String: String].self, from: $0.content)
}

调用 API 的时候需要我们将 JWT token 信息放在 Authorization HTTP 头中。为了达到这个目的,我们增加一项配置:

service.configure("**") {
    if let authToken = self.authToken {
        $0.headers["Authorization"] = "Bearer \(authToken)"
    }
}

现在我们的请求已经被认证了,接着尝试去请求一些需要认证的资源,比如/expenses。这个断点返回一个数组,成员结构包含以下字段:

{
    "amount": -50.0,
    "created_at": "2017-12-07T16:00:52.988245",
    "description": "pizza",
    "type": "TransactionType.EXPENSE"
}

我们创建一个模型来存储返回值的这种格式。增加一个名为Expense的类。接下来使用JSONDecoder,从 Codable 继承:

import Foundation

struct Expense: Decodable {
    let amount: Float
    let createdAt: Date
    let description: String
    let type: String

    enum CodingKeys: String, CodingKey {
        case amount
        case createdAt = "created_at"
        case description
        case type
    }
}

CodingKeys 枚举允许我们映射返回的 JSON 字段名到刚刚创建的结构体的属性名。这里映射了日期字段(createdAt)。因为我们的自定义了日期格式,我们还需要通过JSONDecoder.dateDecodingStrategy来进行配置。

let jsonDecoder = JSONDecoder()
let jsonDateFormatter = DateFormatter()
jsonDateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.A"
jsonDecoder.dateDecodingStrategy = .formatted(jsonDateFormatter)

最后,创建这个类的转换器:

service.configureTransformer("/expenses") {
    try jsonDecoder.decode([Expense].self, from: $0.content)
}

我们期待得到 Expense 数组,通过[Expense]定义。

参考刚才的定义,我们增加一个expenses()资源访问器,然后我们可以调用需要验证信息的资源:

import Siesta

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()

        AwesomeAPI.expenses().addObserver(self)
    }

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)

        AwesomeAPI.login("test", "test", onSuccess: {
            AwesomeAPI.expenses().loadIfNeeded()
        }, onFailure: { error in
            print(error)
        })
    }
}

extension ViewController: ResourceObserver {
    func resourceChanged(_ resource: Resource, event: ResourceEvent) {
        if let expenses: [Expense] = resource.typedContent() {
            print(expenses)
        }
    }
}

最后一件事

最后我想讨论一下认证信息过期之后的一些实践。配合 Siesta,我们能自动执行认证以及重试因为认证失败的请求。

增加配置:

service.configure("**") {
    // Retry requests on auth failure
    $0.decorateRequests {
        self.refreshTokenOnAuthFailure(request: $1)
    }
}

将请求串联起来,然后带着新 token 再次调用。

func refreshAuth(_ username: String, _ password: String) -> Request {
    return self.login(username, password, onSuccess: {
        }, onFailure: { error in
    })
}

func refreshTokenOnAuthFailure(request: Siesta.Request) -> Request {
    return request.chained {
        guard case .failure(let error) = $0.response,  // Did request fail…
            error.httpStatusCode == 401 else {           // …because of expired token?
                return .useThisResponse                    // If not, use the response we got.
        }

        return .passTo(
            self.refreshAuth("test", "test").chained {             // If so, first request a new token, then:
                if case .failure = $0.response {           // If token request failed…
                    return .useThisResponse                  // …report that error.
                } else {
                    return .passTo(request.repeated())       // We have a new token! Repeat the original request.
                }
            }
        )
    }
}

最后,项目地址奉上:https://github.com/nderkach/A...

Happy hacking!

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Jacquelyn38 Jacquelyn38
4年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Stella981 Stella981
3年前
KaliTools说明书+BurpSuit实战指南+SQL注入知识库+国外渗透报告
!(https://oscimg.oschina.net/oscnet/d1c876a571bb41a7942dd9752f68632e.gif"15254461546.gif")0X00KaliLinux Tools中文说明书!(https://oscimg.oschina.net/oscnet/
Stella981 Stella981
3年前
SpringBoot整合Redis乱码原因及解决方案
问题描述:springboot使用springdataredis存储数据时乱码rediskey/value出现\\xAC\\xED\\x00\\x05t\\x00\\x05问题分析:查看RedisTemplate类!(https://oscimg.oschina.net/oscnet/0a85565fa
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
3年前
PHP创建多级树型结构
<!lang:php<?php$areaarray(array('id'1,'pid'0,'name''中国'),array('id'5,'pid'0,'name''美国'),array('id'2,'pid'1,'name''吉林'),array('id'4,'pid'2,'n
Easter79 Easter79
3年前
SpringBoot整合Redis乱码原因及解决方案
问题描述:springboot使用springdataredis存储数据时乱码rediskey/value出现\\xAC\\xED\\x00\\x05t\\x00\\x05问题分析:查看RedisTemplate类!(https://oscimg.oschina.net/oscnet/0a85565fa
Wesley13 Wesley13
3年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
1年前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这
美凌格栋栋酱 美凌格栋栋酱
5个月前
Oracle 分组与拼接字符串同时使用
SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(
孟宗
孟宗
Lv1
惊鸿一瞥而后不知所踪,这大概就是晚霞的美之所在。
文章
5
粉丝
0
获赞
0