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 假數據,以方便前期客戶端開發。
整個實現貫徹了單一職責、接口隔離、開閉原則以及面向協議,靈活可擴展,主要方法都是純函數,可測試,定義接口的時候都是聲明式的組合使用。