元件化的應用背景和優勢在此不再贅述,下面我們將從實踐的角度,討論一下如何應用元件化的思想,下面將以我自己的理解逐步展開,拋磚引玉。
哪些內容需要元件化
在我的理解中,一個專案可以拆分為以下幾種元件:
- 基礎元件;
- 功能元件;
- 業務元件;
下面依次來解釋幾種元件的定義和規則。
基礎元件
- 基本配置
- 常量;
- 巨集定義;
- 分類
- 各種系統類的擴充套件;
- 網路
- 對 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複製程式碼
其中 .width
和 SCREENWIDTH
,都在基礎元件中,但基礎元件中不僅僅是這些東西,如果依賴了基礎元件,就需要匯入基礎元件中其他無用的程式碼,而且其他人使用輪播圖元件,也需要匯入基礎元件。
因此,在功能元件中,不建議依賴基礎元件,上面的程式碼應該改成這樣:
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
引數,通過中介軟體 NetWorkMW
將 NetWork
和 常量
兩個基礎元件組合,完成預設引數網路請求的封裝。
// 基礎元件 - 常量
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)
}複製程式碼
以上實際上是怎麼樣把多個元件組合使用起來,這種組合是確定的,還有一些是不確定的,例如有一個元件的狀態改變了,我要讓其他元件知道我的變化,但是我不知道都要告訴誰,怎麼辦?
眼珠一轉,對外暴露狀態變化,中介軟體在變化時傳送通知。但是同時我想附帶一個模型過去,通知的接收方怎樣正確的使用這個模型呢?如果要使用模型,勢必要和傳送通知的業務元件產生耦合,怎麼辦?
以後再辦,先埋個坑,這些場景我們會在以後再講到。
元件分離的難點
元件分離的重點和難點也就是解耦,比如我們現在負責一個專案,其中的一個業務或者功能,希望實現元件化,但是它依賴於專案中的其他公共功能,該如何處理呢?這裡提供兩種思路:
- 拷程式碼,簡單粗暴,擺脫依賴,對於一些不重要的工具方法,可以直接拷貝到內部來使用;
- 把元件依賴的程式碼先做一個
pod
庫,然後依賴這個pod
庫;
上面講到的是程式碼方面的依賴,還有一種情況是功能方面的依賴,比如我們有一個選單,這個選單涉及到網路圖片的載入,那麼怎樣將這個選單進行元件化呢?
- 使用 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
的依賴。
附加問題
以上的環節掌握了,應該可以嘗試簡單的元件化了,但是問題沒完,還有哪些呢?
庫的升級維護
隨著專案的迭代,你負責的庫升級了,其他的小夥伴們還在用上個版本的庫,怎麼辦?
各種路徑資源問題
我們在自己的庫裡使用了 imageNamed
、mainBundle
,但是小夥伴把我們的庫拖過去後,這些路徑和我們不是一個路徑,Assets.xcassets
跟我們也不是同一個 Assets.xcassets
,怎麼辦?
這些問題你可以從這篇文章找到答案:你真的會用 CocoaPods 嗎?