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!
假如如今接口须要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 假数据,以方便前期客户端开发。
整个实现贯彻了单一职责、接口隔离、开闭原则以及面向协议,灵活可扩展,主要方法都是纯函数,可测试,定义接口的时候都是声明式的组合使用。