一、大神博客研讀
隨著應(yīng)用需求逐步迭代,應(yīng)用的代碼體積將會越來越大,為了更好的管理應(yīng)用工程,我們開始借助CocoaPods版本管理工具對原有應(yīng)用工程進行拆分。但是僅僅完成代碼拆分還不足以解決業(yè)務(wù)之間的代碼耦合,為了更好的讓拆分出去的業(yè)務(wù)工程能夠獨立運行,必須進行組件拆分并且實現(xiàn)組件服務(wù)化。
下面是最近在行業(yè)內(nèi)幾個大神的博客辯論對戰(zhàn),具體資料如下:
- 2016.03.10 蘑菇街App的組件化之路
- 2016.03.13 iOS應(yīng)用架構(gòu)談 組件化方案
- 2016.03.14 蘑菇街App的組件化之路·續(xù)
- 2016.03.14 iOS應(yīng)用架構(gòu)談 組件化方案(補充)
- 2016.03.18 iOS 組件化方案探索
- 2016.03.21圍觀神仙打架,反革命工程師《iOS應(yīng)用架構(gòu)談 組件化方案》和蘑菇街Limboy的《蘑菇街 App 的組件化之路》的閱讀指導(dǎo)
最近在參考大神們的討論和之前的LDBusBundle方案基礎(chǔ)上上,提煉出了一個適合中小型應(yīng)用的LDBusMediator中間件,正逐漸在項目中使用。
博客介紹:http://www.itdecent.cn/p/196f66d31543
中間件Git開源地址:https://github.com/Lede-Inc/LDBusMediator.git
(1)蘑菇街的組件化方案
文章來源:
2016.03.10 蘑菇街App的組件化之路: http://limboy.me/ios/2016/03/10/mgj-components.html
為什么要組件化?
- 組件和組件之間沒有明確的約束;
- 組件單獨開發(fā)、單獨測試,不能揉入主項目中開發(fā),測試也可以針對性的測試;
如何管理短鏈?(url跳轉(zhuǎn))
[MGJRouter registerURLPattern:@"mgj://detail?id=:id" toHandler:^(NSDictionary *routerParameters) {
NSNumber *id = routerParameters[@"id"];
// create view controller with id
// push view controller
}];
[MGJRouter openURL:@"mgj://detail?id=404”]
短鏈如何管理?
- 后臺專門管理短鏈;平臺生成所需的文件,ios平臺生成h,m文件,android生成java文件,注入到項目中;
- 開發(fā)人員查看生成文件了解所有可用URL;
- 缺點:無法把參數(shù)傳遞也通過生成方式獲得;
同步的Action調(diào)用?(服務(wù)調(diào)用)
方法一:通過url的方式
[MGJRouter registerURLPattern:@"mgj://cart/ordercount" toObjectHandler:^id(NSDictionary *routerParamters){
// do some calculation
return @42;
}]
NSNumber *orderCount = [MGJRouter objectForURL:@"mgj://cart/ordercount”]
方法二:通過protocol-class對應(yīng)的方式
把公共協(xié)議文件統(tǒng)一放到PublicProtocolDomain.h中,所有業(yè)務(wù)組件只依賴這個文件;protocol只能通過類方法提供?
@protocol MGJCart <NSObject>
+ (NSInteger)orderCount;
@end
[ModuleManager registerClass:MGJCartImpl forProtocol:@protocol(MGJCart)]
[ModuleManager classForProtocol:@protocol(MGJCart)]
組件生命周期的管理:(組件管理)
啟動初始化時,實例APP中所有組件的module實例,讓每個組件的module實例執(zhí)行一遍didFinishLaunchingWithOptions方法:在這方法中每個組件注冊自己的URL,使用class注冊;每個組件可以自行監(jiān)控系統(tǒng)的通知,如UIApplicationDidBecomeActiveNotification, 對于沒有系統(tǒng)通知消息則將此方法寫入module的protocol中,依次執(zhí)行實例的這些protocol方法;
[[ModuleManager sharedInstance] loadModuleFromPlist:[[NSBundle mainBundle] pathForResource:@"modules" ofType:@"plist"]];
NSArray *modules = [[ModuleManager sharedInstance] allModules];
for (id<ModuleProtocol> module in modules) {
if ([module respondsToSelector:_cmd]) {
[module application:application didFinishLaunchingWithOptions:launchOptions];
}
}
組件化版本管理的問題
- 版本同步問題: API接口改動升級(舊接口不存在了,不向下兼容),版本的中位號發(fā)生改變;需要所有依賴其的調(diào)用都發(fā)生改變,才能保證殼工程和主工程能夠同步編譯通過;
- pod update之后編譯太長: 考慮通過framework的方式進行修改;
- 持續(xù)集成問題: 不能只是把podspec直接扔到private repo里完事,需要扔到主工程進行打包編譯,編譯通過允許提供版本升級,不通過扔回去進行處理;CI編譯檢查,通過之后再將版本號升級到private repo中,同時修改主工程中Podfile的版本依賴號; 但如果是其它工程呢,被多個業(yè)務(wù)工程所依賴,如何辦?
蘑菇街開源組件:
MGJRouter: https://github.com/mogujie/MGJRouter.git
- JLRoutes 的問題主要在于查找 URL 的實現(xiàn)不夠高效,通過遍歷而不是匹配。還有就是功能偏多。
- HHRouter 的 URL 查找是基于匹配,所以會更高效,MGJRouter 也是采用的這種方法,但它跟 ViewController 綁定地過于緊密,一定程度上降低了靈活性。
(2)反革命的組件化方案
文章來源:
- 2016.03.13 iOS應(yīng)用架構(gòu)談 組件化方案http://casatwy.com/iOS-Modulization.html
- 2016.03.14 iOS應(yīng)用架構(gòu)談 組件化方案(補充)
蘑菇街的方案為什么不好?
- url注冊對于實施組件化是完全沒有必要的,拓展性和可維護性都降低;
- 基于openURL的方案的話,有一個致命缺陷:非常規(guī)對象無法參與本地組件間調(diào)度;但是可以通過傳遞params來解決,但是這樣區(qū)分了遠程調(diào)用和本地調(diào)用的入口;
- 模塊內(nèi)部是否仍然需要使用URL去完成調(diào)度?是沒有必要的,為啥要復(fù)雜化?
反革命的組件化方案:
基于Mediator模式和Target-Action模式:
[CTMediator sharedInstance]
openUrl:url] //call from other app with url
parseUrl
performTarget:action:params //call form Native Module
runtime
[TargetA action1], [TargetA action2]
[TargetB action1], [TargetB action2]
反革命組件化方案的調(diào)用方式:
本地跨組件間調(diào)用:
[[CTMediator sharedInstance] performTarget:targetName action:actionName params:@{…}]
遠程應(yīng)用調(diào)用:
openUrl + parseUrl的方式; 針對請求的路由操作,直接將Target和Action的名字封裝到url中;
反革命組件化方案的好處:
- 將遠程調(diào)用和本地調(diào)用做了拆分,而且由本地應(yīng)用調(diào)用位遠程應(yīng)用調(diào)用提供服務(wù);
- 組件僅通過Action暴露可調(diào)用接口;
- 組件化方案必需去Model設(shè)計:只有調(diào)用方依賴Mediator,響應(yīng)方依賴是沒有必要的;
- 調(diào)用方如何知道接收方需要哪些Key的參數(shù),如何知道有哪些target可被調(diào)用?:在mediator中維護針對Mediator的Category,每個category對應(yīng)一個target,categroy中的方法對應(yīng)Action場景;
- category為組合模式,根據(jù)不同的分類提供不同的方法,每個組件對應(yīng)一個category分類;
- 參數(shù)驗證和補救入口;
- 輕松的請求轉(zhuǎn)發(fā);
- 統(tǒng)一了所有組件間調(diào)用入口;
- param的hardcode在整個app的作用域僅僅存在于category中,跟調(diào)用宏差不多;
- 安全保證,對url中進行native前綴驗證;
- 保證動態(tài)調(diào)度考慮;
反革命組件化方案開源Demo:
代碼Git地址:https://github.com/casatwy/CTMediator.git
二、實際項目中的組件化問題
(1) 為什么要組件化?
- 解決人多(更好的協(xié)作)、需求多(更好的功能模塊劃分)的問題;
- 解決項目模塊間的代碼耦合問題;(堅決抵制業(yè)務(wù)組件間代碼直接引用)
(2)如何拆分組件?(神仙們討論的主要是產(chǎn)品業(yè)務(wù)組件化的問題)
-
基礎(chǔ)功能組件:(類似于性能統(tǒng)計、Networking、Patch、網(wǎng)絡(luò)診斷等)
- 按功能分庫,不涉及產(chǎn)品業(yè)務(wù)需求,跟庫Library類似
- 通過良好的接口拱上層業(yè)務(wù)組件調(diào)用;
- 不寫入產(chǎn)品定制邏輯,通過擴展接口完成定制;
-
基礎(chǔ)UI組件:(例如下拉刷新組件、iCausel類似的組件)
- 產(chǎn)品內(nèi)通用UI組件;(各個業(yè)務(wù)模塊依賴使用,但需要保持好定制擴展的設(shè)計)
- 公共通用UI組件;(不涉及具體產(chǎn)品的視覺設(shè)計, 目前較少)
-
產(chǎn)品業(yè)務(wù)組件:(例如圈子、1元購、登錄、客服MM等)
- 業(yè)務(wù)功能間相對獨立,相互間沒有Model共享的依賴;
- 業(yè)務(wù)之間的頁面調(diào)用只能通過UIBus進行跳轉(zhuǎn);
- 業(yè)務(wù)之間的邏輯Action調(diào)用只能通過服務(wù)提供;
(3)組件化工程需要解決的問題?
-
組件化頁面跳轉(zhuǎn)(UIBus)方案要求:
- 能夠傳遞普通參數(shù)(系統(tǒng)基礎(chǔ)數(shù)據(jù)類型)和復(fù)雜參數(shù)(url無法負載的對象),不負責(zé)CustomModel的傳遞處理;
- 能夠獲取url對應(yīng)的controller進行TabController的動態(tài)配置;
- 能夠?qū)ontroller的present方式進行定制;
- url的注冊須用代碼完成,必需去中心化處理;
-
組件服務(wù)化(ServiceBus)方案要求:
- 能夠傳遞普通參數(shù)和復(fù)雜參數(shù),盡量不使用CustomModel的傳遞;
- 通過接口文件的統(tǒng)一基礎(chǔ)庫進行依賴;(業(yè)務(wù)開發(fā)方開發(fā)階段只需要依賴接口文件依賴庫,在主項目集成測試階段依賴所有業(yè)務(wù)組件進行測試)
- 接口和實現(xiàn)類的的對應(yīng)注冊須用代碼完成,由中間件去控制服務(wù)實現(xiàn)類的生成;
(通用問題:復(fù)雜參數(shù)傳遞 key值的硬編碼問題)
(4)組件維護問題?
-
組件服務(wù)接口的設(shè)計:
- 最小化原則;
- 命名規(guī)范;
-
版本發(fā)布:
- 版本號規(guī)范:http://semver.org/
- 持續(xù)集成,通過腳本完成:https://github.com/fastlane/fastlane
三、關(guān)于組件化的思考和總結(jié)
MGJRouter+ModuleManager方案 (蘑菇街方案)
CTMediator+Target-Action方案 (反革命方案)
(1)主要解決本地業(yè)務(wù)組件之間的通信問題
組件化主要還是解決本地業(yè)務(wù)組件間的調(diào)用,至于跨App或者Hybrid頁面通過openUrl方式調(diào)用頁面和服務(wù)的方式其實是可以拆分成兩個步驟的問題:特定模塊解析處理+中間件調(diào)用??鏏pp通過info.plist配置的scheme跳轉(zhuǎn)進入,hybrid頁面通過JSBridge框架跳轉(zhuǎn)進入,這部分都有特定的模塊去解析完成。在特定的模塊中是否要調(diào)用其它業(yè)務(wù)組件的頁面或者服務(wù)由特定模塊自行決定,這不是組件化中間件要去完成的事情。
(2)從工程代碼層面來說,組件化就是通過中間件解決組件間頭文件直接引用、依賴混亂的問題;
從實際開發(fā)來說,組件之間最大的需求就是頁面跳轉(zhuǎn),需要從組件A的pageA1頁面跳轉(zhuǎn)到組件B的pageB1頁面,避免對組件B頁面ViewController頭文件的直接依賴。其次就是服務(wù)的調(diào)用,服務(wù)調(diào)用模塊絕不是為了解決url跳轉(zhuǎn)的問題,只是服務(wù)調(diào)用方式可以用來解決頁面跳轉(zhuǎn)的需求,但是沒有url跳轉(zhuǎn)方案成本低。所以才有了蘑菇街方案的MGJRouter和ModuleManager的class-protocol方案的區(qū)別;而反革命的方案仍然用Target-Action方案來解決頁面跳轉(zhuǎn)問題,成本稍大;而且url跳轉(zhuǎn)和服務(wù)調(diào)用是兩種不同的組件間通信需求,用兩種不同的方式來完成更有區(qū)分度。
(3)純中間件只負責(zé)掛接節(jié)點的通信問題,不應(yīng)涉及掛接點具體業(yè)務(wù)的任何邏輯。
中間件如果涉及到具體的業(yè)務(wù)邏輯,勢必造成中間件對業(yè)務(wù)模塊的直接依賴,所以中間件只需要抽象出業(yè)務(wù)通信的基本職責(zé),規(guī)定好協(xié)議接口,完成調(diào)度功能即可。
而每個掛接節(jié)點(這里指業(yè)務(wù)組件)遵循中間件的協(xié)議完成掛接工作,當(dāng)然這會造成掛接節(jié)點對中間件的協(xié)議依賴;調(diào)用方同樣也必須通過掛接點提供的方法將調(diào)用操作push到中間件上,而不用管具體的調(diào)用過程,這樣也是掛接節(jié)點依賴中間件,業(yè)務(wù)邏輯并沒有直接依賴中間件。這就是之前阿里無線分享的bus總線的思路,通過這種思路即使切換或者去掉中間件,都只需要在掛接節(jié)點中進行修改就可以完成,避免了對業(yè)務(wù)邏輯代碼的直接調(diào)用修改。
至于去掉中間件,應(yīng)用仍然能夠跑的命題? 如果沒有任何代碼的修改,就相當(dāng)于把解藕的橋梁給拆除了,再牛逼的框架也不能滿足。
- 反革命框架的調(diào)用方直接依賴中間件提供的調(diào)用方法,拆除中間件,至少需要修改調(diào)用方法。
(4)中間件是否應(yīng)該解決組件對外披露url調(diào)用和服務(wù)接口信息?
中間件解決了組件間的通信解藕問題,勢必會將組件對外提供調(diào)用的信息隱藏起來,不然就不能達到解藕通信的目標(biāo)。
蘑菇街方案的披露方法:
- url短鏈在后臺管理,自動生成可查看的.{h,m,java}文件,開發(fā)人員通過這個文件進行查看,代碼文件跟文檔功能類似;無法解決參數(shù)key、類型的問題;
- 服務(wù)調(diào)用接口統(tǒng)一放到PublicProtocol.h文件上,其它所有業(yè)務(wù)組件均需要依賴這個文件;
(是否把url短鏈和publicProtocol文件統(tǒng)一放到一個repo里,其實就相當(dāng)于說明文檔的作用)
反革命方案的披露方法:
- 通過依賴中間件的category(target)方式,將業(yè)務(wù)組件的所有調(diào)用都通過category的方法暴露出來;
- 優(yōu)點:解決了url參數(shù)key、類型檢查的問題;通過Target-Action方式同時解決了url短鏈和服務(wù)調(diào)用的通信需求,而且更加適合程序猿的風(fēng)格來解決問題。
四、我們的組件化方案
之前聽阿里的組件化分享之后,自己做了一套有關(guān)Bus總線的方案,但是在具體的產(chǎn)品使用過程中用起來還是麻煩,在項目中推廣起來難度還是比較大。特別是關(guān)于組件對外披露信息的部分,到現(xiàn)在都沒有一個好的思路,雖然反革命的方案解決了披露的問題,但是我覺得擴展性和可維護性上還是比較差。
git開源地址:https://github.com/Lede-Inc/LDBusBundle_IOS.git
最近研讀幾個大神的博客和討論之后,有了一些新的思路,希望能夠繼續(xù)按照bus+category的思路上去專研一下,希望能夠一個真正適合在項目里推行起來的方案。
最近在參考大神們的討論和之前的LDBusBundle方案基礎(chǔ)上上,提煉出了一個適合中小型應(yīng)用的LDBusMediator中間件,正逐漸在項目中使用。
博客介紹:http://www.itdecent.cn/p/196f66d31543
中間件Git開源地址:https://github.com/Lede-Inc/LDBusMediator.git