1. 程式人生 > >如何設計一個面向協議的 iOS 網路請求庫

如何設計一個面向協議的 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 的請求有三種: requestupload 和 download,這三種請求都有相應的引數,MBNetwork 把這些引數抽象成了對應的協議,具體內容參見:MBForm.swift。這種做法有幾個優點:

  1. 對於類似 headers 這樣的引數,一般全域性都是一致的,可以直接 extension 指定。
  2. 通過協議的名字即可知道表單的功能,簡單明確。

下面是 MBNetwork 表單協議的用法舉例:

指定全域性 headers 引數:

extension MBFormable {
    public func headers() -> [String: String] {
        return ["accessToken":"xxx"];
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 1
  • 2
  • 3
  • 4
  • 5

建立具體業務表單:

struct WeatherForm: MBRequestFormable {
    var city = "shanghai"

    public func parameters() -> [String: Any] {
        return ["city": city]
    }

    var url = "https://raw.githubusercontent.com/tristanhimmelman/AlamofireObjectMapper/2ee8f34d21e8febfdefb2b3a403f18a43818d70a/sample_keypath_json"
    var method = Alamofire.HTTPMethod.get
}
  • 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 是一個空協議 。

///  Network request protocol, object conforms to this protocol can make network request
public protocol MBRequestable: class {

}
  • 1
  • 2
  • 3
  • 4
  • 1
  • 2
  • 3
  • 4

為什麼是空協議,因為不需要遵循這個協議的物件幹啥。

然後對它做 extension,實現網路請求相關的一系列介面:

func request(_ form: MBRequestFormable) -> DataRequest

func download(_ form: MBDownloadFormable) -> DownloadRequest

func download(_ form: MBDownloadResumeFormable) -> DownloadRequest

func upload(_ form: MBUploadDataFormable) -> UploadRequest

func upload(_ form: MBUploadFileFormable) -> UploadRequest

func upload(_ form: MBUploadStreamFormable) -> UploadRequest

func upload(_ form: MBUploadMultiFormDataFormable, completion: ((UploadRequest) -> Void)?)
  • 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 對應的介面。注意它們都返回了型別為 DataRequestUploadRequest或者 DownloadRequest 的物件,通過返回值我們可以繼續呼叫其他方法。

到這裡 MBRequestable 的實現就完成了,使用方法很簡單,只需要設定型別遵循 MBRequestable 協議,就可以在該型別內發起網路請求。如下:

class LoadableViewController: UIViewController, MBRequestable {
    override func viewDidLoad() {
        super.viewDidLoad()

        // Do any additional setup after loading the view.
        request(WeatherForm())
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

載入

對於載入我們關心的點有如下幾個:

  • 載入開始需要幹啥。
  • 載入結束需要幹啥。
  • 是否需要顯示載入遮罩。
  • 在何處顯示遮罩。
  • 顯示遮罩的內容。

對於這幾點,我對協議的劃分是這樣的:

  • MBContainable 協議。遵循該協議的物件可以做為載入的容器。
  • MBMaskable 協議。遵循該協議的 UIView 可以做為載入遮罩。
  • MBLoadable 協議。遵循該協議的物件可以定義載入的配置和流程。

MBContainable

遵循這個協議的物件只需要實現下面的方法即可:

func containerView() -> UIView?
  • 1
  • 1

這個方法返回做為遮罩容器的 UIView。做為遮罩的 UIView 最終會被新增到 containerView 上。

不同型別的容器的 containerView 是不一樣的,下面是各種型別容器 containerView 的列表:

容器 containerView
UIViewController view
UIView self
UITableViewCell contentView
UIScrollView 最近一個不是 UIScrollView 的 superview

UIScrollView 這個地方有點特殊,因為如果直接在 UIScrollView 上新增遮罩檢視,遮罩檢視的中心點是非常難控制的,所以這裡用了一個技巧,遞迴尋找 UIScrollView 的 superview,發現不是 UIScrollView 型別的直接返回即可。程式碼如下:

public override func containerView() -> UIView? {
    var next = superview
    while nil != next {
        if let _ = next as? UIScrollView {
            next = next?.superview
        } else {
            return next
        }
    }
    return nil
}
  • 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():載入結束回撥方法。

然後對協議要求實現的幾個方法做預設實現:

func mask() -> MBMaskable?