最近因為項目需要,又給我寫的網(wǎng)絡框架添加了不少功能。到目前看來秕狰,這個網(wǎng)絡框架已經(jīng)具備了最基本的項目需求,實現(xiàn)了以后大部分項目都能使用的現(xiàn)實需要躁染,但還有幾點令我不太滿意鸣哀。這篇文章就是記錄我設計這個框架的整體思路,以及記錄一下不滿意的地方吞彤,方便以后回顧修改我衬。
構建一個請求
首先叹放,我的設計思路是偏向 POP 的,也就是面向協(xié)議編程挠羔【觯框架并不直接提供 URLRequest 的構建接口,而是由繼承了 Requestable
協(xié)議的類與結構體自身來實現(xiàn)破加。
Requestable
協(xié)議是長這個樣子的:
public protocol Requestable {
associatedtype AnyResponse: Response
associatedtype AnyRequest: Request
var request: AnyRequest { get set }
var response: AnyResponse.Type { get set }
var extendErrorHandler: ((Data) -> Error?)? { get }
var logRequestTime: Bool { get }
}
可以看到俱恶,這其中還有兩個關聯(lián)類型,它們分別是負責描述請求的 Request 協(xié)議范舀。負責描述回調(diào)的 Response 協(xié)議合是。
它們的結構是這樣的:
public protocol Request {
var url: URL { get set }
var httpMethod: String { get set }
var body: Data? { get set }
var header: [String: String]? { get set }
var type: RequestType { get set }
var cachePolicy: CachePolicy { get set }
}
public protocol Response: JSON {
associatedtype Data
var code: Int? { get set }
var msg: String? { get set }
var data: Data? { get set }
}
而 Reponse 又要求實現(xiàn) JSON 協(xié)議,JSON 協(xié)議其實比較特殊锭环,它規(guī)范了你的數(shù)據(jù)模型必須是這樣的:
public protocol JSON {
var makeJSON: [String: Any]? { get }
static func makeModel(_ deserielized: String?) -> Self?
static func makeModel(_ deserielized: [String: Any]?) -> Self?
}
也就是聪全,你需要提供模型轉字典、字典轉模型這一基本能力辅辩,而這項操作是相對復雜且多元化的难礼,有非常多的第三方框架能幫助實現(xiàn)這一點,甚至自己寫也不麻煩玫锋,所以我索性將 JSON 協(xié)議設計為“可變實現(xiàn)”的協(xié)議蛾茉,這充分利用了 Swift 的協(xié)議特性。什么意思呢撩鹿,原理其實很簡單谦炬,首先我定義一個類型別名,也就是 typealias三痰,它的內(nèi)容是“任意一個能實現(xiàn)字典模型互轉的類型(簡稱類型 M) & JSON”吧寺,假定這個別名取為 Model,那么散劫,只要我全局實現(xiàn) Model稚机,那么就等于實現(xiàn)了 JSON,且依賴于類型 M获搏,我能讓所有 Model 都實現(xiàn)字典與模型互轉赖条。
具體做起來也很簡單,首先我創(chuàng)建一個類型別名:
public typealias Model = JSON & HandyJSON
這里我使用 HandyJSON 來作為類型 M 的示例常熙,接下來纬乍,我需要將 HandyJSON 提供的字典模型互轉方法作為 JSON 的默認實現(xiàn),方法如下:
public extension JSON where Self: HandyJSON {
var makeJSON: [String: Any]? {
toJSON()
}
static func makeModel(_ deserielized: String?) -> Self? {
deserialize(from: deserielized)
}
static func makeModel(_ deserielized: [String: Any]?) -> Self? {
deserialize(from: deserielized)
}
}
此時裸卫,只要類型是 Model 的結構和類仿贬,都會擁有字典模型互轉的能力了。最后墓贿,我們再賦予 JSON 一個默認實現(xiàn):
public extension JSON {
var makeJSON: [String: Any]? {
nil
}
static func makeModel(_ deserielized: String?) -> Self? {
nil
}
static func makeModel(_ deserielized: [String: Any]?) -> Self? {
nil
}
}
這一點主要是方便于其它沒有實現(xiàn) HandyJSON 但又想實現(xiàn) JSON 以使用網(wǎng)絡框架的結構和類茧泪。
而我為什么要大張旗鼓地做到這一點呢蜓氨,主要是考慮到了 HandyJSON 的不穩(wěn)定性,HandyJSON 在最近的幾次 Swift 語言更新中队伟,都體現(xiàn)出了不穩(wěn)定性穴吹,很容易就導致 App 崩潰,如果遇到了這種情況嗜侮,甚至 Swift 或 iOS 發(fā)生了一些 HandyJSON 無法解決的更新港令,HandyJSON 用就無法使用,那么我只需要修改 Model 的定義锈颗,并提供新的默認實現(xiàn)顷霹,就能拯救我的 App,而無需去修改每一個文件宜猜,使得去除 HandyJSON泼返。
然后硝逢,我給 Requestable 提供一個默認方法 resume姨拥,這個方法用于返回一個可訂閱的信號源,只要訂閱了這個信號渠鸽,就能獲得這個信號的狀態(tài)叫乌,Rx 對這些狀態(tài)做了很好的區(qū)分,你可以選擇性的訂閱想要的信號類型徽缚,例如成功憨奸、失敗、完成狀態(tài)凿试。
resume 的實現(xiàn)是這樣的:
func resume() -> Observable<AnyResponse?> {
ObservableCreate(requestable: self).resume()
}
struct ObservableCreate<R: Requestable> {
var requestable: R
func resume() -> Observable<R.AnyResponse?> {
let network = Network(requestable)
let url = requestable.request.url
var request = URLRequest(url: url)
request.httpMethod = requestable.request.httpMethod
request.httpBody = requestable.request.body
request.timeoutInterval = NetworkConfiguration.timeoutInterval
network.cachePolicy = requestable.request.cachePolicy
requestable.request.header?.forEach({ (key, value) in
request.setValue(value, forHTTPHeaderField: key)
})
network.urlRequest = request
network.errorHandler = requestable.extendErrorHandler
return equestable.request.type.resume(network)
}
}
通過 ObservableCreate 來統(tǒng)一創(chuàng)建這個信號排宰,那么你可能已經(jīng)發(fā)現(xiàn)了 Network 這個類,這個類就是框架的核心部分那婉,實際的對請求完成構建板甘、發(fā)送、回調(diào)處理详炬、緩存等操作盐类,同時也是對 Alamofire 的一個封裝。
除此之外呛谜,Requestable 還有兩個額外屬性在跳,它們是:
var extendErrorHandler: ((Data) -> Error?)? { get }
var logRequestTime: Bool { get }
這兩個屬性的 get 方法是有默認實現(xiàn)的,也就是 Requestable 不強制你實現(xiàn)它們隐岛,但依然是構建一個請求較為重要的部分猫妙。
extendErrorHandler 是一個閉包,你可以在這里拿到所有請求回調(diào)的數(shù)據(jù)聚凹,但我不建議你在這里對數(shù)據(jù)進行操作割坠,這里指應該關心回調(diào)中的錯誤信息逻悠,也就是說,此時請求是成功的韭脊,但服務器可能返回的不是一個標準的數(shù)據(jù)童谒,或直接包裝了 Error 數(shù)據(jù)模型返回沪羔。如果在這里返回了一個 Error饥伊,那么請求將不會被標記為成功,最終會走到錯誤回調(diào)中蔫饰,也就是 onError 事件琅豆。
logRequestTime 默認是 false,它的作用是用來記錄每次請求的時間篓吁,并且會把日志輸出到一個磁盤文件中茫因,便于接口優(yōu)化。
那么現(xiàn)在杖剪,我們就可以動手構建一個請求了冻押,大概是這樣的:
struct BindUser: Requestable {
var request: BindUserRequest = .init()
var response: BindUserResponse.Type = BindUserResponse.self
}
struct BindUserRequest: Request {
static var path: String {
#if DEBUG
return "http://debug.com”
#else
return "http://release.com”
#endif
}
var cachePolicy: CachePolicy = .withoutCache
var url: URL = URL.init(string: “\(BindUserRequest.path)/api“)!
var httpMethod: String = "POST"
var body: Data?
var header: [String : String]? = [“Content-Type”: “application/x-www-form-urlencoded”]
var type: RequestType = .request
let appToken: String = "API"
init() {
}
}
struct BindUserResponse: Response {
var code: Int?
var msg: String?
var data: String?
}
struct BindUserModel: Model {
var status: String?
var message: String?
var data: String?
}
這是一個用于綁定用戶信息的后臺接口,在實現(xiàn)了這個接口的定義后盛嘿,我就可以在需要用到的地方進行調(diào)用:
BindUser().resume()
最后洛巢,通過對 resume 返回的信號源進行訂閱,我就可以對數(shù)據(jù)進行處理了次兆。
訂閱一個請求
前文提到稿茉,該框架實際上就是一個對 RxSwift + Alamofire 的封裝,所以當請求發(fā)出后芥炭,你可以立刻對請求進行訂閱漓库,框架會對數(shù)據(jù)進行二次封裝,然后通過 RxSwift 這一層對數(shù)據(jù)進行篩選园蝠。
數(shù)據(jù)處理的結果渺蒿,符合 RxSwift 對數(shù)據(jù)的定義,也就是說:
onNext 信號:請求成功且無任何錯誤回調(diào)砰琢。
onError 信號:請求發(fā)生了任何錯誤蘸嘶,包括下層應用層錯誤和上層返回了自定義錯誤。
onComplete 信號:請求完成了陪汽,包括請求成功和請求失敗训唱。
現(xiàn)在就可以根據(jù)需要來對數(shù)據(jù)進行篩選,例如挚冤,我只關心請求是否結束况增,我不關心成功與失敗,那么只需要訂閱 onComplete 信號就足夠了训挡。
// 在界面上顯示一個“正在請求”的圖標澳骤。
Alert.show()
_ = BindUser().resume().subscribe(onComplete: {
// 請求完成了歧强,不論成功或失敗,都應該隱藏“正在請求”的圖標为肮。
Alert.hide()
})
緩存
框架的緩存策略是基于請求域名+相對路徑的摊册,也就是不論 GET 參數(shù)是什么、協(xié)議頭是什么颊艳,都不會影響到緩存機制茅特。
在實現(xiàn) Request 協(xié)議時,或者手動創(chuàng)建 Network 對象時棋枕,你就可以指定一個緩存策略白修,目前的緩存策略有如下幾種:
/// 始終使用緩存,如果沒有緩存重斑,則在第一次請求成功時建立緩存兵睛。
case alwaysUseCache
/// 首先使用緩存,當請求成功后窥浪,會發(fā)出請求的數(shù)據(jù)祖很,并覆蓋原緩存。
case useCacheAndRequest
/// 使用緩存寒矿,當請求成功后突琳,會發(fā)出請求的數(shù)據(jù)若债,但不覆蓋原有緩存符相。
case useCacheAndRequestWithoutRecache
/// 默認。始終不使用緩存蠢琳,請求的數(shù)據(jù)也不會緩存到本地啊终。
case withoutCache
到目前,框架還不會對非 JSON 串的二進制數(shù)據(jù)進行緩存傲须,例如圖片蓝牲、視頻等,所以需要依賴其他方式泰讽。
緩存的底層實現(xiàn)是利用了 SQLite 數(shù)據(jù)庫例衍,在需要的時候直接將數(shù)據(jù)寫入數(shù)據(jù)庫,或從數(shù)據(jù)庫讀取緩存的數(shù)據(jù)已卸。
目前的緩存策略還非常的原始佛玄,以后我需要為它添加更多能力,例如最大緩存大小累澡、清除緩存梦抢、緩存過期時間,同時還需要能夠緩存二進制文件愧哟,能識別這個二進制文件在服務器上是否發(fā)生了修改奥吩,如果修改了哼蛆,則要建立新的緩存,如果沒有修改霞赫,那么直接使用緩存即可腮介。
最后
對框架不滿意的部分,其實已經(jīng)體現(xiàn)在上文中了端衰。包括:
框架是對 Alamofire 的二次封裝萤厅,依賴項太龐大了,需要一個輕量化的靴迫,基于原生 URLRequest 的封裝惕味。
框架的緩存機制還非常原始。
框架的性能還未在考慮之中玉锌。
源碼可能會在我實現(xiàn)了全面的緩存機制之后傳到 GitHub 上名挥。