(原文地址:https://medium.freecodecamp.o...)git
今天我跟你們分享一下個人 iOS 網絡庫新歡,名字叫作 Siesta。「她有啥特殊的?爲啥我不直接用 Almofire?」你也許會問。事實上,你仍然能夠把 Alamofire 和 Siesta 一塊兒使用!它是客戶端之上的網絡抽象層。github
和 Moya 不一樣,Siesta 不會隱藏 HTTP。這種中間狀態,是我使用 Siesta 構建 REST API 的理由。json
經過資源爲中心而不是請求爲中心的設計,Siesta 提供一個全局的符合 RESTful 的可被觀察的模型。swift
這意味着什麼?一些非必要的網絡和反序列化操做被大量減小,視圖控制器和網絡請求之間的關係被解耦。此外,它的響應解析十分透明,開箱即用。api
這篇教程裏,我將展現給你如何經過使用 Siesta,讓你的網絡處理代碼變得更加 Swiftly。數組
從 Cocoapods 安裝:安全
pod 'Siesta', '~> 1.0'
爲了演示本教程,我將編寫一個簡單的 CRUD 應用程序配合 REST API 和 我部署到 HeroKu 上基於 JWT 的驗證。網絡
首先,建立一個名爲 AwesomeAPI.swift
的文件。app
定義基本的 API 配置:ide
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 中,咱們有兩個須要驗證權限的接口:incomes
和expenses
。他們須要認證權限,因此咱們須要先得到 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
。在onSuccess
和onFailure
兩個方法中處理返回信息,若是驗證成功,則存儲起來。
最後,咱們來實如今過時以前更新用戶驗證信息。使用計時器來實現:
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 }) } } }
咱們測試接口的驗證信息爲test
和test
,AwesomeAPI.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!