iOS 元件化實踐思考

薛定諤發表於2017-10-27

元件化的應用背景和優勢在此不再贅述,下面我們將從實踐的角度,討論一下如何應用元件化的思想,下面將以我自己的理解逐步展開,拋磚引玉。

哪些內容需要元件化

在我的理解中,一個專案可以拆分為以下幾種元件:

  • 基礎元件;
  • 功能元件;
  • 業務元件;

下面依次來解釋幾種元件的定義和規則。

基礎元件

  • 基本配置
    • 常量;
    • 巨集定義;
  • 分類
    • 各種系統類的擴充套件;
  • 網路
    • 對 AFN 的封裝;
    • 對 SDWebImage 的封裝;
  • 工具類
    • 檔案處理;
    • 裝置資訊;
    • 時間日期處理;

基礎元件的含義就是最基礎的東西,每個業務元件都有可能會使用到,基礎元件需要抽取的應該是類似上面的程式碼,舉例來說,比如我們定義了一個常量,表示介面的根路徑:

let BASEMIRRORURL = "http://rest.mirror.xxxx.com/ios"複製程式碼

那麼這個常量在 Home,List,Detail 都有可能會被引用,因此我們將這種最底層的,最下一層的東西歸類到基礎元件。

又比如分類和擴充套件,我們給 UIView 的擴充套件定義一個計算屬性:

extension UIView {
  var height {
    set {
      self.frame.size.height = newValue
    }
    get {
      return self.frame.size.height
    }
  }
}複製程式碼

可以想到,也會有很多的業務元件會使用到這個擴充套件。

功能元件

  • 控制元件
    • 彈幕;
    • 輪播;
    • 選單;
    • 瀑布流;
  • 功能
    • 斷點續傳;
    • 音視訊處理;
    • GPUImage 封裝;

功能元件分為可見和不可見兩種,可見的是控制元件,不可見的是功能。功能元件的作用顧名思義,就是實現了一個功能。

業務元件

業務元件,也就是業務的具體實現了,比如一個 App 的骨架如下:

  • 首頁;
  • 發現;
  • 我的;

首頁下又分為這樣:

  • 側滑選單;
  • Banner;
  • 熱門;

這裡的每個部分,都可以稱為業務元件。

三種元件的關係

三種元件的關係
三種元件的關係

基礎元件規則

基礎元件和基礎元件之間不應該產生依賴,比如我們使用網路請求元件,希望根路徑是一個預設引數,但可以對外暴露和修改,像下面這樣:

class NetWork {
  func request(baseUrl: String = BASEMIRRORURL, path: String, param: [String:Any]) {

  }
}

NetWork.request(path: "/g/login.server", param: param)複製程式碼

這時,NetWork 就依賴了 常量 這個基礎元件,我們如果使用 NetWork 基礎元件,還需要匯入 常量 這個基礎元件,這是不應該的。

但為了程式碼的簡潔性,這樣的封裝又是必要的,那麼應該怎麼做呢?這個問題我們下面會講到。

功能元件規則

功能元件和基礎元件之間不應該產生依賴,比如我們做輪播圖,會用到 UIView 的擴充套件常量 ,像下面這樣:

imageView.width = SCREENWIDTH複製程式碼

其中 .widthSCREENWIDTH ,都在基礎元件中,但基礎元件中不僅僅是這些東西,如果依賴了基礎元件,就需要匯入基礎元件中其他無用的程式碼,而且其他人使用輪播圖元件,也需要匯入基礎元件。

因此,在功能元件中,不建議依賴基礎元件,上面的程式碼應該改成這樣:

imageView.frame.size.width = UIScreen.main.bounds.size.width複製程式碼

或者直接複製程式碼,將需要的基礎元件的功能,複製到功能元件當中。

同基礎元件一樣,功能元件和功能元件也不應該產生依賴,道理是一樣的,我們使用一個功能,不應該將另一個功能也匯入進來。

業務元件規則

基礎元件和功能元件都是為業務服務的,因此業務元件可以依賴於基礎元件和功能元件,快速的實現業務,但是業務元件和業務元件之間不應該產生依賴。

比如這樣一條業務線,我們要求 發現 這個業務元件,點選一條視訊,跳轉到 視訊播放器

func pushToPlayerVC(model: VideoModel) {
  let vc = PlayerVC(videoModel: model)
  navigationVC.push(vc)
}複製程式碼

這時 發現 就對 視訊播放器產生了依賴,如果將 發現 進行元件化進行剝離,能行嗎?不行。

其實這個問題和網路請求使用預設引數封裝一樣,是元件與元件之間的通訊問題,當然,這個問題我們下面會講到,現在再提一下是為了一會兒往下寫的時候忘了填坑 ...

每個元件存在的形式

  • 元件內部;
  • 元件外部;
  • 元件測試;

元件內部

元件的內部應該使用設計模式劃分資料夾的結構,例如 MVVM 結構:

---- PlayerView
  -- View
  -- Model
  -- ViewModel複製程式碼

元件外部

元件的外部應該是一個遠端私有 pod 庫,使用 CocoaPods 進行管理。

元件測試

單獨的測試工程。

怎樣整合各個元件

元件整合
元件整合

元件的整合應該像上面的圖一樣,基礎元件和功能元件互不依賴,製作遠端 pod 私有庫,業務元件依賴於這些 pod 私有庫開發,同樣製作成遠端 pod 私有庫,殼工程依賴於 CocoaPods 管理這些私有庫,完成整個專案。

當然還有另外的方式,比如將殼工程作為主工程,元件建立為子工程,這方式的缺點是子工程可以修改,缺少約束性,目錄結構也比較凌亂。

還有將元件製作為 FrameWork,殼工程中匯入一個個 FrameWork 庫,這種方式個人感覺比上一種好一些,但是在物理上,元件和殼還是沒能做到分離。

因此,我個人還是更傾向於 pod 庫的形式。

元件之間的通訊

  • 對外公開 API 介面;
  • 通過中介軟體的中轉;

上面我們有兩個遺留的問題,歸納為元件之間的通訊問題,下面就通過這兩個問題,討論一下元件之間的通訊。

網路請求預設引數

下面的思路就是暴露出 baseUrl 引數,通過中介軟體 NetWorkMWNetWork常量 兩個基礎元件組合,完成預設引數網路請求的封裝。

// 基礎元件 - 常量
let BASEMIRRORURL = "http://rest.mirror.xxxx.com/ios"

// 基礎元件 - 網路請求
class NetWork {
  func request(baseUrl: String, path: String, param: [String:Any]) {

  }
}

//殼工程 - 網路請求中介軟體
class NetWorkMW {
  func request(baseUrl: String = BASEMIRRORURL, path: String, param: [String:Any]) {
    NetWork.request(baseUrl: baseUrl, path: path, param: param)
  }
}

NetWorkMW.request(path: "/g/login.server", param: param)複製程式碼

發現跳轉視訊播放

這個思路是使用代理,對外暴露點選事件,通過中介軟體,匯入 視訊播放 業務元件,topVC 基礎元件,完成向 視訊播放 的跳轉:

// 業務元件 - 發現
func pushToPlayerVC(model: VideoModel) {
  delegate?.pushToPlayerVC?(videoModel: model)
}

// 中介軟體 - 發現
func pushToPlayerVC(model: VideoModel) {
  let vc = PlayerVC(videoModel: model)
  topVC.navigationVC.push(vc)
}複製程式碼

以上實際上是怎麼樣把多個元件組合使用起來,這種組合是確定的,還有一些是不確定的,例如有一個元件的狀態改變了,我要讓其他元件知道我的變化,但是我不知道都要告訴誰,怎麼辦?

眼珠一轉,對外暴露狀態變化,中介軟體在變化時傳送通知。但是同時我想附帶一個模型過去,通知的接收方怎樣正確的使用這個模型呢?如果要使用模型,勢必要和傳送通知的業務元件產生耦合,怎麼辦?

以後再辦,先埋個坑,這些場景我們會在以後再講到。

元件分離的難點

元件分離的重點和難點也就是解耦,比如我們現在負責一個專案,其中的一個業務或者功能,希望實現元件化,但是它依賴於專案中的其他公共功能,該如何處理呢?這裡提供兩種思路:

  1. 拷程式碼,簡單粗暴,擺脫依賴,對於一些不重要的工具方法,可以直接拷貝到內部來使用;
  2. 把元件依賴的程式碼先做一個 pod 庫,然後依賴這個 pod 庫;

上面講到的是程式碼方面的依賴,還有一種情況是功能方面的依賴,比如我們有一個選單,這個選單涉及到網路圖片的載入,那麼怎樣將這個選單進行元件化呢?

  1. 使用 Block 或者代理,將網路圖片載入這部分的職責交給外部控制;

舉例來說,像下面這樣:

// 業務元件 - 選單
self.imageView.sd_setImage(with: url, completed: completed)複製程式碼

那麼如果現在將它元件化,這個元件就要依賴於 SDWebImage,我們應該修改成這樣:

// 業務元件 - 選單
setImage?(for: imageView, completed: ImageLoadCompletedBlock)

// 中介軟體 - 選單
menu.setImage = { (imageView, completed) in 
  imageView.sd_setImage(with: url, completed: completed)
}複製程式碼

現在選單就擺脫了對 SDWebImage 的依賴。

附加問題

以上的環節掌握了,應該可以嘗試簡單的元件化了,但是問題沒完,還有哪些呢?

庫的升級維護

隨著專案的迭代,你負責的庫升級了,其他的小夥伴們還在用上個版本的庫,怎麼辦?

各種路徑資源問題

我們在自己的庫裡使用了 imageNamedmainBundle,但是小夥伴把我們的庫拖過去後,這些路徑和我們不是一個路徑,Assets.xcassets 跟我們也不是同一個 Assets.xcassets,怎麼辦?

這些問題你可以從這篇文章找到答案:你真的會用 CocoaPods 嗎?

相關文章