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

1,305 阅读6分钟

为什么要造个新轮子

just for fun!

哈哈,其实在真正的项目中我还是推荐你使用知名的网络库,比如 Moya/Alamofire/AFNetworking 的,毕竟这些功能够强大,久经考验,代码优秀,非要说缺点可能就是略显臃肿,不方便用在SDK之中,并且对于后两者一般还要二次封装。这次要实现的就是够用够轻量够强大的网络库。

让我们开始实现一个直接基于系统URLSession的简单并强大的网络库吧!

目标

我想要达成的效果是这样的:

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)
    }
}

使用的时候需要是最简洁的,只传递必要的会变化的参数,并且完全是类型安全的,所有的类型都已确定,最后处理的时候不需要任何判断。

抽象

进行一个网络请求的过程中,涉及到了哪些对象?无非就是请求、响应、数据处理操作以及衔接这些东西的对象。

一个请求,实际上就是一个接口,至少需要请求地址、请求方法、请求参数、参数类型,它才是一个完整的请求。当客户端和后端约定好一个接口时,除了参数的值不确定以外,其他(包括返回体结构)一般都不会改变了,其他的参数本身也只是这个接口自身的事情,实现应该在请求内部,而不应该由调用方去告诉,调用方调用的时候只需要去描述这个接口就行了。

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将足够简单:

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