使用 Siesta 处理 Swift 网络请求
(原文地址: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 的验证。 首先,创建一个名为 定义基本的 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 对象。我们配置服务的地址,还有 从资源对象中访问网络并读取数据,我们需要在 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) } } } 我们给 Siesta 支持对请求初始化和配置进行解耦,所以在请求资源的时候,不用担心过多关于请求具体的细节。 比如,你无需担心 现在如果你运行程序,你可能将看到类似这样的输出: Siesta:network │ GET https://jwt-api-siesta.herokuapp.com/ping Siesta:network │ Response: 200 ← GET https://jwt-api-siesta.herokuapp.com/ping pong 转换器让我们再做点有意思的。定义一些转换器可以实现自动解析原始 JSON 数据到一个模型对象。
{ "text": "ok" } 我们使用 首先,我们添加转换器: 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") }
然后我们对 ViewController 中观察方法进行更新。 func resourceChanged(_ resource: Resource,event: ResourceEvent) { if let status: [String: String] = resource.typedContent() { print("(status)") } } 你可能注意到了,解析一个 JSON 我们使用 if let text: String = resource.typedContent() { print(text) } 验证在我们的 API 中,我们有两个需要验证权限的接口: 首先,增加一个属性,它将存储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 存储到钥匙串而不是 接下来,我们想在 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 请求给 最后,我们来实现在过期之前更新用户验证信息。使用计时器来实现: 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 }) } } } 我们测试接口的验证信息为 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)" } } 现在我们的请求已经被认证了,接着尝试去请求一些需要认证的资源,比如 { "amount": -50.0,"created_at": "2017-12-07T16:00:52.988245","description": "pizza","type": "TransactionType.EXPENSE" } 我们创建一个模型来存储返回值的这种格式。增加一个名为 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 } }
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 数组,通过 参考刚才的定义,我们增加一个 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",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! (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |