實現一個簡單可擴展的網絡庫

爲何要造個新輪子

just for fun!ios

哈哈,其實在真正的項目中我仍是推薦你使用知名的網絡庫,好比 Moya/Alamofire/AFNetworking 的,畢竟這些功可以強大,久經考驗,代碼優秀,非要說缺點可能就是略顯臃腫,不方便用在SDK之中,而且對於後二者通常還要二次封裝。此次要實現的就是夠用夠輕量夠強大的網絡庫。git

讓咱們開始實現一個直接基於系統URLSession的簡單並強大的網絡庫吧!github

目標

我想要達成的效果是這樣的:json

let client = HTTPClient(session: .shared)
let req = HTTPBinPostRequest(foo: "bar")
client.send(req) { (result) in
    switch result {
    case .success(let response):
        print(response)
    case .failure(let error):
        print(error)
    }
}
複製代碼

使用的時候須要是最簡潔的,只傳遞必要的會變化的參數,而且徹底是類型安全的,全部的類型都已肯定,最後處理的時候不須要任何判斷。swift

抽象

進行一個網絡請求的過程當中,涉及到了哪些對象?無非就是請求、響應、數據處理操做以及銜接這些東西的對象。後端

一個請求,實際上就是一個接口,至少須要請求地址、請求方法、請求參數、參數類型,它纔是一個完整的請求。當客戶端和後端約定好一個接口時,除了參數的值不肯定之外,其餘(包括返回體結構)通常都不會改變了,其餘的參數自己也只是這個接口自身的事情,實現應該在請求內部,而不該該由調用方去告訴,調用方調用的時候只須要去描述這個接口就好了。api

public protocol Request {
    associatedtype Response: Decodable
    
    var url: URL { get }
    var method: HTTPMethod { get }
    var parameters: [String: Any] { get }
    var contentType: ContentType { get }
}
複製代碼

請求已經抽象出來了,不過這只是方便上層使用,底層咱們還得把它轉換成URLRequest才能真正地請求,所以:安全

public extension Request{
    func buildRequest() throws -> URLRequest {
        let req = URLRequest(url: url)
      // 這裏須要對req進行各類賦值,各類ifelse,很容易寫成麪條式代碼
        return request
    }
}
複製代碼

咱們要對基本的URLRequest進行各類修改操做,好比賦值請求方法、各類header字段、查詢字段以及請求體,很顯然若是你把全部邏輯都寫到buildRequest裏面,這裏將會很複雜。咱們須要抽象出一個統一的接口專門處理URLRequest的修改操做。網絡

public protocol RequestAdapter {
    func adapted(_ request: URLRequest) throws -> URLRequest
}
複製代碼

如今buildRequest將足夠簡單:session

func buildRequest() throws -> URLRequest {
        let req = URLRequest(url: url)
        let request = try adapters.reduce(req) { try $1.adapted($0) }
        return request
}
複製代碼

URLRequest有了,該拿給URLSession去請求了:

public struct HTTPClient {
public func send<Req: Request>(_ request: Req, desicions: [Decision]? = nil, handler: @escaping (Result<Req.Response, Error>) -> Void) -> URLSessionDataTask? {
        let urlRequest: URLRequest
        do {
            urlRequest = try request.buildRequest()
        } catch {
            handler(.failure(error))
            return nil
        }
        
        let task = session.dataTask(with: urlRequest) { (data, response, error) in
                 // 判斷是否有data和response,response是否合法,最後解析數據
        }
        task.resume()
        return task
    }
}
複製代碼

顯然,響應和數據的可能性有不少,這裏的代碼又將變的複雜,同上面同樣,咱們須要把對響應數據的處理行爲抽象出來:

public protocol Decision: AnyObject {
    // 是否應該進行這個決策,判斷響應數據是否符合這個決策執行的條件
    func shouldApply<Req: Request>(request: Req, data: Data, response: HTTPURLResponse) -> Bool
    func apply<Req: Request>(request: Req, data: Data, response: HTTPURLResponse, done: @escaping (DecisionAction<Req>) -> Void)
}
複製代碼

對一次請求的響應數據的處理可能不止一種,咱們須要順序處理,所以咱們還須要知道某次處理的狀態或者接下來的動做:

public enum DecisionAction<Req: Request> {
    case continueWith(Data, HTTPURLResponse)
    case restartWith([Decision])
    case error(Error)
    case done(Req.Response)
}
複製代碼

接下來就能正確處理了:

let task = session.dataTask(with: urlRequest) { (data, response, error) in
            guard let data = data else {
                handler(.failure(error ?? ResponseError.nilData))
                return
            }
            guard let response = response as? HTTPURLResponse else {
                handler(.failure(ResponseError.nonHTTPResponse))
                return
            }
            self.handleDecision(request, data: data, response: response, decisions: desicions ?? request.decisions,
                                handler: handler)
            
        }
複製代碼
func handleDecision<Req: Request>(_ request: Req, data: Data, response: HTTPURLResponse, decisions: [Decision], handler: @escaping (Result<Req.Response, Error>) -> Void) {
        guard !decisions.isEmpty else {
            fatalError("No decision left but did not reach a stop")
        }
        var decisions = decisions
        let first = decisions.removeFirst()
        
        guard first.shouldApply(request: request, data: data, response: response) else {
            handleDecision(request, data: data, response: response, decisions: decisions, handler: handler)
            return
        }
        first.apply(request: request, data: data, response: response) { (action) in
            switch action {
            case let .continueWith(data, response):
                self.handleDecision(request, data: data, response: response, decisions: decisions, handler: handler)
            case .restartWith(let decisions):
                self.send(request, desicions: decisions, handler: handler)
            case .error(let error):
                handler(.failure(error))
            case .done(let value):
                handler(.success(value))
            }
        }
    }
複製代碼

實現

抽象完了咱們就實際場景開始實現吧!

假如須要進行一個 post 請求,請求體須要是 json:

struct JSONRequestDataAdapter: RequestAdapter {
    let data: [String: Any]
    func adapted(_ request: URLRequest) throws -> URLRequest {
        var request = request
        request.httpBody = try JSONSerialization.data(withJSONObject: data, options: [])
        return request
    }
}
複製代碼

假如是一個form表單請求:

struct URLFormRequestDataAdapter: RequestAdapter {
    let data: [String: Any]
    func adapted(_ request: URLRequest) throws -> URLRequest {
        var request = request
        request.httpBody =
            data.map({ (pair) -> String in
            "\(pair.key)=\(pair.value)"
            })
            .joined(separator: "&").data(using: .utf8)
        return request
    }
}
複製代碼

當響應數據返回以後,假如statusCode不正確,須要進行重試操做:

public class RetryDecision: Decision {
    let count: Int
    public init(count: Int) {
        self.count = count
    }

    public func shouldApply<Req>(request: Req, data: Data, response: HTTPURLResponse) -> Bool where Req : Request {
        let isStatusCodeValid = (200..<300).contains(response.statusCode)
        return !isStatusCodeValid && count > 0
    }

    public func apply<Req>(request: Req, data: Data, response: HTTPURLResponse, done: @escaping (DecisionAction<Req>) -> Void) where Req : Request {
        let nextRetry = RetryDecision(count: count - 1)
        let newDecisions = request.decisions.replacing(self, with: nextRetry)
        done(.restartWith(newDecisions))
    }
}
複製代碼

真正的數據解析操做:

public class ParseResultDecision: Decision {
    public func shouldApply<Req>(request: Req, data: Data, response: HTTPURLResponse) -> Bool where Req : Request {
        return true
    }
    
    public func apply<Req>(request: Req, data: Data, response: HTTPURLResponse, done: (DecisionAction<Req>) -> Void) where Req : Request {
        do {
            let value = try JSONDecoder().decode(Req.Response.self, from: data)
            done(.done(value))
        } catch {
            done(.error(error))
        }
    }
}
複製代碼

使用

如今我要定義一個真正的請求來試試這強大的能力了:

struct HTTPBinPostRequest: Request {
    typealias Response = HTTPBinPostResponse

    var url: URL = URL(string: "https://httpbin.org/post")!
    var method: HTTPMethod = .POST
    var contentType: ContentType = .json
    var parameters: [String : Any] {
        return ["foo": foo]
    }
    
    let foo: String
  
    var decisions: [Decision] {
        return [RetryDecision(count: 2),
                BadResponseStatusCodeDecision(valid: 200..<300),
                DataMappingDecision(condition: { (data) -> Bool in
            return data.isEmpty
        }, transform: { (data) -> Data in
            "{}".data(using: .utf8)!
        }),
        ParseResultDecision()]
    }
}

struct HTTPBinPostResponse: Codable {
    struct Form: Codable { let foo: String? }
    let form: Form
    let json: Form
}
複製代碼

顯然,已經達成了一開始的目標了,nice!

Screen Shot 2020-03-02 at 9.02.13 PM

擴展

假如如今接口須要token認證,咱們只須要新增一個TokenAdapter便可

struct TokenAdapter: RequestAdapter {
    let token: String?
    init(token: String?) {
        self.token = token
    }

    func adapted(_ request: URLRequest) throws -> URLRequest {
        guard let token = token else {
            return request
        }
        var request = request
        request.setValue("Bearer " + token, forHTTPHeaderField: "Authorization")
        return request
    }
}
複製代碼
public extension Request{
    var adapters: [RequestAdapter] {
        return [TokenAdapter(token: "token"),method.adapter,
                RequestContentAdapter(method: method, content: parameters, contentType: contentType)]
    }
}
複製代碼

這樣每一個請求都會帶上token。

讓咱們再看一個常見的場景,順便實現一個Decision

相信客戶端的同窗都遇到過相似的問題,就是說好的一個 json 結構,後端卻返回了 null,而後默認轉成NSNull,而後你仍是照常取字段致使 crash,或者使用Codable解析時直接異常。

public class DataMappingDecision: Decision {
    let condition: (Data) -> Bool
    let transform: (Data) -> Data
    
    public init(condition: @escaping (Data) -> Bool, transform: @escaping (Data) -> Data) {
        self.condition = condition
        self.transform = transform
    }

    public func shouldApply<Req>(request: Req, data: Data, response: HTTPURLResponse) -> Bool where Req : Request {
        return condition(data)
    }
    
    public func apply<Req>(request: Req, data: Data, response: HTTPURLResponse, done: (DecisionAction<Req>) -> Void) where Req : Request {
        done(.continueWith(transform(data), response))
    }
}
複製代碼

有心的同窗可能注意到上面HTTPBinPostRequest已經用了:

DataMappingDecision(condition: { (data) -> Bool in
            return data.isEmpty
        }, transform: { (data) -> Data in
            "{}".data(using: .utf8)!
        })
複製代碼

這個DataMappingDecision還能夠用來 mock 假數據,以方便前期客戶端開發。

總結

整個實現貫徹了單一職責、接口隔離、開閉原則以及面向協議,靈活可擴展,主要方法都是純函數,可測試,定義接口的時候都是聲明式的組合使用。

完整代碼

參考:王巍 iPlayground 演講

lineSDK源碼

相關文章
相關標籤/搜索