如何設計一個面向協議的 iOS 網路請求庫
最近開源了一個面向協議設計的網路請求庫 MBNetwork
,基於 Alamofire
和 ObjectMapper
實現,目的是簡化業務層的網路請求操作。
需要幹些啥
對於大部分 App 而言,業務層做一次網路請求通常關心的問題有如下幾個:
- 如何在任意位置發起網路請求。
- 表單建立。包含請求地址、請求方式(GET/POST/……)、請求頭等……
- 載入遮罩。目的是阻塞 UI 互動,同時告知使用者操作正在進行。比如提交表單時在提交按鈕上顯示 “菊花”,同時使其失效。
- 載入進度展示。下載上傳圖片等資源時提示使用者當前進度。
- 斷點續傳。下載上傳圖片等資源發生錯誤時可以在之前已完成部分的基礎上繼續操作,這個
Alamofire
- 資料解析。因為目前主流服務端和客戶端資料交換採用的格式是
JSON
,所以我們暫時先考慮JSON
格式的資料解析,這個ObjectMapper
可以支援。 - 出錯提示。發生業務異常時,直接顯示服務端返回的異常資訊。前提是服務端異常資訊足夠友好。
- 成功提示。請求正常結束時提示使用者。
- 網路異常重新請求。顯示網路異常介面,點選之後重新發送請求。
為什麼是 POP
而不是 OOP
關於 POP
和 OOP
這兩種設計思想及其特點的文章很多,所以我就不廢話了,主要說說為啥要用 POP
來寫 MBNetwork
。
- 想嘗試一下一切皆協議的設計方式。所以這個庫的設計只是一次極限嘗試,並不代表這就是最完美的設計方式。
- 如果以
OOP
的方式實現,使用者需要通過繼承的方式來獲得某個類實現的功能,如果使用者還需要另外某個類實現的功能,就會很尷尬。而POP
是通過對協議進行擴充套件來實現功能,使用者可以同時遵循多個協議,輕鬆解決OOP
的這個硬傷。 OOP
繼承的方式會使某些子類獲得它們不需要的功能。- 如果因為業務的增多,需要對某些業務進行分離,
OOP
的方式還是會碰到子類不能繼承多個父類的問題,而POP
則完全不會,分離之後,只需要遵循分離後的多個協議即可。 OOP
繼承的方式入侵性比較強。POP
可以通過擴充套件的方式對各個協議進行預設實現,降低使用者的學習成本。- 同時
POP
還能讓使用者對協議做自定義的實現,保證其高度可配置性。
站在 Alamofire
的肩膀上
很多人都喜歡說 Alamofire
是 Swift
版本的 AFNetworking
,但是在我看來,Alamofire
比 AFNetworking
更純粹。這和 Swift
語言本身的特性也是有關係的,Swift
開發者們,更喜歡寫一些輕量的框架。比如 AFNetworking
把很多
UI 相關的擴充套件功能都做在框架內,而 Alamofire
的做法則是放在另外的擴充套件庫中。比如 AlamofireImage
和 AlamofireNetworkActivityIndicator
而 MBNetwork
就可以當做是 Alamofire
的一個擴充套件庫,所以,MBNetwork
很大程度上遵循了 Alamofire
介面的設計規範。一方面,降低了 MBNetwork
的學習成本,另一方面,從個人角度來看,Alamofire
確實有很多特別值得借鑑的地方。
POP
首先當然是 POP
啦,Alamofire
大量運用了 protocol
+ extension
的實現方式。
enum
做為檢驗寫 Swift
姿勢正確與否的重要指標,Alamofire
當然不會缺。
鏈式呼叫
這是讓 Alamofire
成為一個優雅的網路框架的重要原因之一。這一點 MBNetwork
也進行了完全的
Copy。
@discardableResult
在 Alamofire
所有帶返回值的方法前面,都會有這麼一個標籤,其實作用很簡單,因為在 Swift
中,返回值如果沒有被使用,Xcode
會產生告警資訊。加上這個標籤之後,表示這個方法的返回值就算沒有被使用,也不產生告警。
當然還有 ObjectMapper
引入 ObjectMapper
很大一部分原因是需要做錯誤和成功提示。因為只有解析服務端的錯誤資訊節點才能知道返回結果是否正確,所以我們引入ObjectMapper
來做 JSON
解析。
而只做 JSON
解析的原因是目前主流的服務端客戶端資料互動格式是 JSON
。
這裡需要提到的就是另外一個 Alamofire
的擴充套件庫 AlamofireObjectMapper
,從名字就可以看出來,這個庫就是參照 Alamofire
的
API 規範來做ObjectMapper
做的事情。這個庫的程式碼很少,但實現方式非常 Alamofire
,大家可以拜讀一下它的原始碼,基本上就知道如何基於 Alamofire
做自定義資料解析了。
注:被 @Foolish 安利,正在接入
ProtoBuf
中…
一步一步來
表單建立
Alamofire
的請求有三種: request
、upload
和 download
,這三種請求都有相應的引數,MBNetwork
把這些引數抽象成了對應的協議,具體內容參見:MBForm.swift。這種做法有幾個優點:
- 對於類似
headers
這樣的引數,一般全域性都是一致的,可以直接 extension 指定。 - 通過協議的名字即可知道表單的功能,簡單明確。
下面是 MBNetwork
表單協議的用法舉例:
指定全域性 headers
引數:
- 1
- 2
- 3
- 4
- 5
- 1
- 2
- 3
- 4
- 5
建立具體業務表單:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
表單協議化可能有過度設計的嫌疑,有同感的仍然可以使用
Alamofire
對應的介面去做網路請求,不影響MBNetwork
其他功能的使用。
基於表單請求資料
表單已經抽象成協議,現在就可以基於表單傳送網路請求了,因為之前已經說過需要在任意位置傳送網路請求,而實現這一點的方法基本就這幾種:
- 單例。
- 全域性方法,
Alamofire
就是這麼幹的。 - 協議擴充套件。
MBNetwork
採用了最後一種方法。原因很簡單,MBNetwork
是以一切皆協議的原則設計的,所以我們把網路請求抽象成 MBRequestable
協議。
首先,MBRequestable
是一個空協議 。
- 1
- 2
- 3
- 4
- 1
- 2
- 3
- 4
為什麼是空協議,因為不需要遵循這個協議的物件幹啥。
然後對它做 extension
,實現網路請求相關的一系列介面:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
這些就是網路請求的介面,引數是各種表單協議,介面內部呼叫的其實是 Alamofire
對應的介面。注意它們都返回了型別為 DataRequest
、UploadRequest
或者 DownloadRequest
的物件,通過返回值我們可以繼續呼叫其他方法。
到這裡 MBRequestable
的實現就完成了,使用方法很簡單,只需要設定型別遵循 MBRequestable
協議,就可以在該型別內發起網路請求。如下:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
載入
對於載入我們關心的點有如下幾個:
- 載入開始需要幹啥。
- 載入結束需要幹啥。
- 是否需要顯示載入遮罩。
- 在何處顯示遮罩。
- 顯示遮罩的內容。
對於這幾點,我對協議的劃分是這樣的:
MBContainable
協議。遵循該協議的物件可以做為載入的容器。MBMaskable
協議。遵循該協議的UIView
可以做為載入遮罩。MBLoadable
協議。遵循該協議的物件可以定義載入的配置和流程。
MBContainable
遵循這個協議的物件只需要實現下面的方法即可:
- 1
- 1
這個方法返回做為遮罩容器的 UIView
。做為遮罩的 UIView
最終會被新增到 containerView
上。
不同型別的容器的 containerView
是不一樣的,下面是各種型別容器 containerView
的列表:
容器 |
containerView |
---|---|
UIViewController |
view |
UIView |
self |
UITableViewCell |
contentView |
UIScrollView |
最近一個不是 UIScrollView 的 superview |
UIScrollView
這個地方有點特殊,因為如果直接在 UIScrollView
上新增遮罩檢視,遮罩檢視的中心點是非常難控制的,所以這裡用了一個技巧,遞迴尋找 UIScrollView
的 superview
,發現不是 UIScrollView
型別的直接返回即可。程式碼如下:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
最後我們對 MBContainable
做 extension
,新增一個 latestMask
方法,這個方法實現的功能很簡單,就是返回 containerView
上最新新增的、而且遵循MBMaskable
協議的 subview
。
MBMaskable
協議內部只定義了一個屬性 maskId
,作用是用來區分多種遮罩。
MBNetwork
內部實現了兩個遵循 MBMaskable
協議的 UIView
,分別是 MBActivityIndicator
和 MBMaskView
,其中 MBMaskView
的效果是參照 MBProgressHUD
實現,所以對於大部分場景來說,直接使用這兩個 UIView
即可。
注:
MBMaskable
協議唯一的作用是與containerView
上其它subview
做區分。
MBLoadable
做為載入協議的核心部分,MBLoadable
包含如下幾個部分:
func mask() -> MBMaskable?
:遮罩檢視,可選的原因是可能不需要遮罩。func inset() -> UIEdgeInsets
:遮罩檢視和容器檢視的邊距,預設值UIEdgeInsets.zero
。func maskContainer() -> MBContainable?
:遮罩容器檢視,可選的原因是可能不需要遮罩。func begin()
:載入開始回撥方法。func end()
:載入結束回撥方法。
然後對協議要求實現的幾個方法做預設實現: