实现一个简单可扩展的网络库

为何要造个新轮子

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源码

相关文章
相关标签/搜索