組件化的應(yīng)用背景和優(yōu)勢(shì)在此不再贅述赞警,下面我們將從實(shí)踐的角度,討論一下如何應(yīng)用組件化的思想,下面將以我自己的理解逐步展開挺峡,拋磚引玉托慨。
哪些內(nèi)容需要組件化
在我的理解中鼻由,一個(gè)項(xiàng)目可以拆分為以下幾種組件:
基礎(chǔ)組件;
功能組件厚棵;
業(yè)務(wù)組件蕉世;
下面依次來(lái)解釋幾種組件的定義和規(guī)則。
基礎(chǔ)組件
基本配置
常量婆硬;
宏定義狠轻;
分類
各種系統(tǒng)類的擴(kuò)展;
網(wǎng)絡(luò)
對(duì) AFN 的封裝彬犯;
對(duì) SDWebImage 的封裝向楼;
工具類
文件處理;
設(shè)備信息谐区;
時(shí)間日期處理湖蜕;
基礎(chǔ)組件的含義就是最基礎(chǔ)的東西,每個(gè)業(yè)務(wù)組件都有可能會(huì)使用到宋列,基礎(chǔ)組件需要抽取的應(yīng)該是類似上面的代碼昭抒,舉例來(lái)說(shuō),比如我們定義了一個(gè)常量炼杖,表示接口的根路徑:
let BASEMIRRORURL = "http://rest.mirror.xxxx.com/ios"
那么這個(gè)常量在 Home灭返,List,Detail 都有可能會(huì)被引用坤邪,因此我們將這種最底層的熙含,最下一層的東西歸類到基礎(chǔ)組件。
又比如分類和擴(kuò)展罩扇,我們給 UIView
的擴(kuò)展定義一個(gè)計(jì)算屬性:
extension UIView {
var height {
set {
self.frame.size.height = newValue
}
get {
return self.frame.size.height
}
}
}
可以想到婆芦,也會(huì)有很多的業(yè)務(wù)組件會(huì)使用到這個(gè)擴(kuò)展。
功能組件
控件
彈幕喂饥;
輪播消约;
菜單;
瀑布流员帮;
功能
斷點(diǎn)續(xù)傳或粮;
音視頻處理;
CUPImage 封裝捞高;
功能組件分為可見(jiàn)和不可見(jiàn)兩種氯材,可見(jiàn)的是控件渣锦,不可見(jiàn)的是功能。功能組件的作用顧名思義氢哮,就是實(shí)現(xiàn)了一個(gè)功能袋毙。
業(yè)務(wù)組件
業(yè)務(wù)組件,也就是業(yè)務(wù)的具體實(shí)現(xiàn)了冗尤,比如一個(gè) App 的骨架如下:
首頁(yè)听盖;
發(fā)現(xiàn);
我的裂七;
首頁(yè)下又分為這樣:
側(cè)滑菜單皆看;
Banner;
熱門背零;
這里的每個(gè)部分腰吟,都可以稱為業(yè)務(wù)組件。
三種組件的關(guān)系
基礎(chǔ)組件規(guī)則
基礎(chǔ)組件和基礎(chǔ)組件之間不應(yīng)該產(chǎn)生依賴徙瓶,比如我們使用網(wǎng)絡(luò)請(qǐng)求組件毛雇,希望根路徑是一個(gè)默認(rèn)參數(shù),但可以對(duì)外暴露和修改倍啥,像下面這樣:
class NetWork {
func request(baseUrl: String = BASEMIRRORURL, path: String, param: [String:Any]) {
}
}
NetWork.request(path: "/g/login.server", param: param)
這時(shí)禾乘,NetWork
就依賴了 常量
這個(gè)基礎(chǔ)組件,我們?nèi)绻褂?NetWork
基礎(chǔ)組件虽缕,還需要導(dǎo)入 常量
這個(gè)基礎(chǔ)組件,這是不應(yīng)該的蒲稳。
但為了代碼的簡(jiǎn)潔性氮趋,這樣的封裝又是必要的,那么應(yīng)該怎么做呢江耀?這個(gè)問(wèn)題我們下面會(huì)講到剩胁。
功能組件規(guī)則
功能組件和基礎(chǔ)組件之間不應(yīng)該產(chǎn)生依賴,比如我們做輪播圖祥国,會(huì)用到 UIView 的擴(kuò)展
和 常量
昵观,像下面這樣:
imageView.width = SCREENWIDTH
其中 .width
和 SCREENWIDTH
,都在基礎(chǔ)組件中舌稀,但基礎(chǔ)組件中不僅僅是這些東西啊犬,如果依賴了基礎(chǔ)組件,就需要導(dǎo)入基礎(chǔ)組件中其他無(wú)用的代碼壁查,而且其他人使用輪播圖組件觉至,也需要導(dǎo)入基礎(chǔ)組件。
因此睡腿,在功能組件中语御,不建議依賴基礎(chǔ)組件峻贮,?上面的代碼應(yīng)該改成這樣:
imageView.frame.size.width = UIScreen.main.bounds.size.width
或者直接復(fù)制代碼,將需要的基礎(chǔ)組件的功能应闯,復(fù)制到功能組件當(dāng)中纤控。
同基礎(chǔ)組件一樣,功能組件和功能組件也不應(yīng)該產(chǎn)生依賴碉纺,道理是一樣的嚼黔,我們使用一個(gè)功能,不應(yīng)該將另一個(gè)功能也導(dǎo)入進(jìn)來(lái)惜辑。
業(yè)務(wù)組件規(guī)則
基礎(chǔ)組件和功能組件都是為業(yè)務(wù)服務(wù)的唬涧,因此業(yè)務(wù)組件可以依賴于基礎(chǔ)組件和功能組件,快速的實(shí)現(xiàn)業(yè)務(wù)盛撑,但是業(yè)務(wù)組件和業(yè)務(wù)組件之間不應(yīng)該產(chǎn)生依賴碎节。
比如這樣一條業(yè)務(wù)線,我們要求 發(fā)現(xiàn)
這個(gè)業(yè)務(wù)組件抵卫,點(diǎn)擊一條視頻狮荔,跳轉(zhuǎn)到 視頻播放器
:
func pushToPlayerVC(model: VideoModel) {
let vc = PlayerVC(videoModel: model)
navigationVC.push(vc)
}
這時(shí) 發(fā)現(xiàn)
就對(duì) 視頻播放器
產(chǎn)生了依賴,如果將 發(fā)現(xiàn)
進(jìn)行組件化進(jìn)行剝離介粘,能行嗎殖氏?不行。
其實(shí)這個(gè)問(wèn)題和網(wǎng)絡(luò)請(qǐng)求使用默認(rèn)參數(shù)封裝一樣姻采,是組件與組件之間的通訊問(wèn)題雅采,當(dāng)然,這個(gè)問(wèn)題我們下面會(huì)講到慨亲,現(xiàn)在再提一下是為了一會(huì)兒往下寫的時(shí)候忘了填坑 ...
每個(gè)組件存在的形式
組件內(nèi)部婚瓜;
組件外部;
組件測(cè)試刑棵;
組件內(nèi)部
組件的內(nèi)部應(yīng)該使用設(shè)計(jì)模式劃分文件夾的結(jié)構(gòu)巴刻,例如 MVVM 結(jié)構(gòu):
---- PlayerView
-- View
-- Model
-- ViewModel
組件外部
組件的外部應(yīng)該是一個(gè)遠(yuǎn)程私有 pod
庫(kù),使用 CocoaPods 進(jìn)行管理蛉签。
組件測(cè)試
單獨(dú)的測(cè)試工程胡陪。
怎樣集成各個(gè)組件
組件的集成應(yīng)該像上面的圖一樣,基礎(chǔ)組件和功能組件互不依賴碍舍,制作遠(yuǎn)程 pod
私有庫(kù)柠座,業(yè)務(wù)組件依賴于這些 pod
私有庫(kù)開發(fā),同樣制作成遠(yuǎn)程 pod
私有庫(kù)乒验,殼工程依賴于 CocoaPods 管理這些私有庫(kù)愚隧,完成整個(gè)項(xiàng)目。
當(dāng)然還有另外的方式,比如將殼工程作為主工程狂塘,組件創(chuàng)建為子工程录煤,這方式的缺點(diǎn)是子工程可以修改,缺少約束性荞胡,目錄結(jié)構(gòu)也比較凌亂妈踊。
還有將組件制作為 FrameWork
,殼工程中導(dǎo)入一個(gè)個(gè) FrameWork
庫(kù)泪漂,這種方式個(gè)人感覺(jué)比上一種好一些廊营,但是在物理上,組件和殼還是沒(méi)能做到分離萝勤。
因此露筒,我個(gè)人還是更傾向于 pod
庫(kù)的形式。
組件之間的通訊
對(duì)外公開 API 接口敌卓;
通過(guò)中間件的中轉(zhuǎn)慎式;
上面我們有兩個(gè)遺留的問(wèn)題,歸納為組件之間的通訊問(wèn)題趟径,下面就通過(guò)這兩個(gè)問(wèn)題瘪吏,討論一下組件之間的通訊。
網(wǎng)絡(luò)請(qǐng)求默認(rèn)參數(shù)
下面的思路就是暴露出 baseUrl
參數(shù)蜗巧,通過(guò)中間件 NetWorkMW
將 NetWork
和 常量
兩個(gè)基礎(chǔ)組件組合掌眠,完成默認(rèn)參數(shù)網(wǎng)絡(luò)請(qǐng)求的封裝。
// 基礎(chǔ)組件 - 常量
let BASEMIRRORURL = "http://rest.mirror.xxxx.com/ios"
// 基礎(chǔ)組件 - 網(wǎng)絡(luò)請(qǐng)求
class NetWork {
func request(baseUrl: String, path: String, param: [String:Any]) {
}
}
//殼工程 - 網(wǎng)絡(luò)請(qǐng)求中間件
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)
發(fā)現(xiàn)跳轉(zhuǎn)視頻播放
這個(gè)思路是使用代理幕屹,對(duì)外暴露點(diǎn)擊事件蓝丙,通過(guò)中間件,導(dǎo)入 視頻播放
業(yè)務(wù)組件香嗓,topVC
基礎(chǔ)組件迅腔,完成向 視頻播放
的跳轉(zhuǎn):
// 業(yè)務(wù)組件 - 發(fā)現(xiàn)
func pushToPlayerVC(model: VideoModel) {
delegate?.pushToPlayerVC?(videoModel: model)
}
// 中間件 - 發(fā)現(xiàn)
func pushToPlayerVC(model: VideoModel) {
let vc = PlayerVC(videoModel: model)
topVC.navigationVC.push(vc)
}
以上實(shí)際上是怎么樣把多個(gè)組件組合使用起來(lái),這種組合是確定的靠娱,還有一些是不確定的,例如有一個(gè)組件的狀態(tài)改變了掠兄,我要讓其他組件知道我的變化像云,但是我不知道都要告訴誰(shuí),怎么辦蚂夕?
眼珠一轉(zhuǎn)迅诬,對(duì)外暴露狀態(tài)變化,中間件在變化時(shí)發(fā)送通知婿牍。但是同時(shí)我想附帶一個(gè)模型過(guò)去侈贷,通知的接收方怎樣正確的使用這個(gè)模型呢?如果要使用模型等脂,勢(shì)必要和發(fā)送通知的業(yè)務(wù)組件產(chǎn)生耦合俏蛮,怎么辦撑蚌?
以后再辦,先埋個(gè)坑搏屑,這些場(chǎng)景我們會(huì)在以后再講到争涌。
組件分離的難點(diǎn)
組件分離的重點(diǎn)和難點(diǎn)也就是解耦,比如我們現(xiàn)在負(fù)責(zé)一個(gè)項(xiàng)目辣恋,其中的一個(gè)業(yè)務(wù)或者功能亮垫,希望實(shí)現(xiàn)組件化,但是它依賴于項(xiàng)目中的其他公共功能伟骨,該如何處理呢饮潦?這里提供兩種思路:
拷代碼,簡(jiǎn)單粗暴携狭,擺脫依賴继蜡,對(duì)于一些不重要的工具方法,可以直接拷貝到內(nèi)部來(lái)使用暑中;
把組件依賴的代碼先做一個(gè)
pod
庫(kù)壹瘟,然后依賴這個(gè)pod
庫(kù);
上面講到的是代碼方面的依賴鳄逾,還有一種情況是功能方面的依賴稻轨,比如我們有一個(gè)菜單,這個(gè)菜單涉及到網(wǎng)絡(luò)圖片的加載雕凹,那么怎樣將這個(gè)菜單進(jìn)行組件化呢殴俱?
- 使用 Block 或者代理,將網(wǎng)絡(luò)圖片加載這部分的職責(zé)交給外部控制枚抵;
舉例來(lái)說(shuō)线欲,像下面這樣:
// 業(yè)務(wù)組件 - 菜單
self.imageView.sd_setImage(with: url, completed: completed)
那么如果現(xiàn)在將它組件化,這個(gè)組件就要依賴于 SDWebImage
汽摹,我們應(yīng)該修改成這樣:
// 業(yè)務(wù)組件 - 菜單
setImage?(for: imageView, completed: ImageLoadCompletedBlock)
// 中間件 - 菜單
menu.setImage = { (imageView, completed) in
imageView.sd_setImage(with: url, completed: completed)
}
現(xiàn)在菜單就擺脫了對(duì) SDWebImage
的依賴李丰。
附加問(wèn)題
以上的環(huán)節(jié)掌握了,應(yīng)該可以嘗試簡(jiǎn)單的組件化了逼泣,但是問(wèn)題沒(méi)完趴泌,還有哪些呢?
庫(kù)的升級(jí)維護(hù)
隨著項(xiàng)目的迭代嗜憔,你負(fù)責(zé)的庫(kù)升級(jí)了吉捶,其他的小伙伴們還在用上個(gè)版本的庫(kù)呐舔,怎么辦?
各種路徑資源問(wèn)題
我們?cè)谧约旱膸?kù)里使用了 imageNamed
榄审、mainBundle
搁进,但是小伙伴把我們的庫(kù)拖過(guò)去后饼问,這些路徑和我們不是一個(gè)路徑揭斧,Assets.xcassets
跟我們也不是同一個(gè) Assets.xcassets
讹开,怎么辦旦万?
這些問(wèn)題你可以從這篇文章找到答案:你真的會(huì)用 CocoaPods 嗎?