github上的優(yōu)秀開源庫(kù)的設(shè)計(jì)思路,Star從高到底:
(1)JLRoutes
受URL Scheme思路的影響。它把所有對(duì)資源的請(qǐng)求看成是一個(gè)URI沮明。
(2)routable-ios
(3)HHRouter
這是布丁動(dòng)畫APP的一個(gè)Router湃窍,靈感來自于ABRouter 和 Routable iOS。
(4)MGJRouter
這是蘑菇街的一個(gè)路由的方法仙蛉,這個(gè)庫(kù)的由來:
1.JLRoutes 的問題主要在于查找 URL 的實(shí)現(xiàn)不夠高效,通過遍歷而不是匹配碱蒙。還有就是功能偏多荠瘪。
2.HHRouter 的 URL 查找是基于匹配,所以會(huì)更高效赛惩,MGJRouter 也是采用的這種方法哀墓,但它跟 ViewController 綁定地過于緊密,一定程度上降低了靈活性喷兼。
從數(shù)據(jù)結(jié)構(gòu)來看篮绰,MGJRouter還是和HHRouter一模一樣的。
對(duì)HHRouter所做的優(yōu)化:
1.**MGJRouter 支持 openURL 時(shí)季惯,可以傳一些 userinfo 過去
這個(gè)對(duì)比HHRouter吠各,僅僅只是寫法上的一個(gè)語(yǔ)法糖,在HHRouter中雖然不支持帶字典的參數(shù)星瘾,但是在URL后面可以用URL Query Parameter來彌補(bǔ)走孽。
MGJRouter對(duì)userInfo的處理是:直接把它封裝到Key = MGJRouterParameterUserInfo對(duì)應(yīng)的Value里面。
2.支持中文的URL琳状。
這里就是需要注意一下編碼磕瓷。
3.定義一個(gè)全局的 URL Pattern 作為 Fallback。
這一點(diǎn)是模仿的JLRoutes的匹配不到會(huì)自動(dòng)降級(jí)到global的思想。
parameters字典里面會(huì)先存儲(chǔ)下一個(gè)路由規(guī)則困食,存在block閉包中边翁,在匹配的時(shí)候會(huì)取出這個(gè)handler,降級(jí)匹配到這個(gè)閉包中硕盹,進(jìn)行最終的處理符匾。
4.當(dāng) OpenURL 結(jié)束時(shí),可以執(zhí)行 Completion Block瘩例。
在MGJRouter里面啊胶,作者對(duì)原來的HHRouter字典里面存儲(chǔ)的路由規(guī)則的結(jié)構(gòu)進(jìn)行了改造。
這3個(gè)key會(huì)分別保存一些信息:
MGJRouterParameterURL保存的傳進(jìn)來的完整的URL信息垛贤。
MGJRouterParameterCompletion保存的是completion閉包焰坪。
MGJRouterParameterUserInfo保存的是UserInfo字典。
舉個(gè)例子:
上面的URL會(huì)匹配成功聘惦,那么生成的參數(shù)字典結(jié)構(gòu)如下:
注意某饰,額外的age和name,一個(gè)是注冊(cè)時(shí)候的參數(shù)善绎,一個(gè)是調(diào)用時(shí)候url中黔漂?后面的。
5.可以統(tǒng)一管理URL
這個(gè)功能非常有用禀酱。
URL 的處理一不小心炬守,就容易散落在項(xiàng)目的各個(gè)角落,不容易管理剂跟。比如注冊(cè)時(shí)的 pattern 是 mgj://beauty/:id劳较,然后 open 時(shí)就是 mgj://beauty/123,這樣到時(shí)候 url 有改動(dòng)浩聋,處理起來就會(huì)很麻煩,不好統(tǒng)一管理臊恋。
所以 MGJRouter 提供了一個(gè)類方法來處理這個(gè)問題衣洁。
generateURLWithPattern:函數(shù)會(huì)對(duì)我們定義的宏里面的所有的:進(jìn)行替換,替換成后面的字符串?dāng)?shù)組抖仅,依次賦值坊夫。
將上述過程圖解出來,如下:
以下部分可以忽略了撤卢,貌似MGJRouter中沒有环凿。
--- MGJRouter中貌似沒有體現(xiàn)出要區(qū)分開頁(yè)面間調(diào)用和組件間調(diào)用 ---
--- 以下的應(yīng)該是用代理代替了block ---
蘑菇街為了區(qū)分開頁(yè)面間調(diào)用和組件間調(diào)用蔚舀,于是想出了一種新的方法正压。用Protocol的方法來進(jìn)行組件間的調(diào)用。
每個(gè)組件之間都有一個(gè) Entry怜珍,這個(gè) Entry,主要做了三件事:
- 注冊(cè)這個(gè)組件關(guān)心的 URL
- 注冊(cè)這個(gè)組件能夠被調(diào)用的方法/屬性
- 在 App 生命周期的不同階段做不同的響應(yīng)
頁(yè)面間的openURL調(diào)用就是如下的樣子:
每個(gè)組件間都會(huì)向MGJRouter注冊(cè)到推,組件間相互調(diào)用或者是其他的App都可以通過openURL:方法打開一個(gè)界面或者調(diào)用一個(gè)組件考赛。
在組件間的調(diào)用,蘑菇街采用了Protocol的方式莉测。
[ModuleManager registerClass:ClassA forProtocol:ProtocolA] 的結(jié)果就是在 MM 內(nèi)部維護(hù)的 dict 里新加了一個(gè)映射關(guān)系颜骤。
[ModuleManager classForProtocol:ProtocolA] 的返回結(jié)果就是之前在 MM 內(nèi)部 dict 里 protocol 對(duì)應(yīng)的 class,使用方不需要關(guān)心這個(gè) class 是個(gè)什么東東捣卤,反正實(shí)現(xiàn)了 ProtocolA 協(xié)議忍抽,拿來用就行。
這里需要有一個(gè)公共的地方來容納這些 public protocl董朝,也就是圖中的 PublicProtocl.h鸠项。
我猜測(cè),大概實(shí)現(xiàn)可能是下面的樣子:
然后這個(gè)是一個(gè)單例益涧,在里面注冊(cè)各個(gè)協(xié)議:
在ModuleProtocolManager中用一個(gè)字典保存每個(gè)注冊(cè)的protocol⌒獯福現(xiàn)在再來猜猜ModuleEntry的實(shí)現(xiàn)。
然后每個(gè)模塊內(nèi)都有一個(gè)和暴露到外面的協(xié)議相連接的“接頭”闲询。
在它的實(shí)現(xiàn)中久免,需要引入3個(gè)外部文件,一個(gè)是ModuleProtocolManager扭弧,一個(gè)是DetailModuleEntryProtocol阎姥,最后一個(gè)是所在模塊需要跳轉(zhuǎn)或者調(diào)用的組件或者頁(yè)面。
至此基于Protocol的方案就完成了鸽捻。如果需要調(diào)用某個(gè)組件或者跳轉(zhuǎn)某個(gè)頁(yè)面呼巴,只要先從ModuleProtocolManager的字典里面根據(jù)對(duì)應(yīng)的ModuleEntryProtocol找到對(duì)應(yīng)的DetailModuleEntry,找到了DetailModuleEntry就是找到了組件或者頁(yè)面的“入口”了御蒲。再把參數(shù)傳進(jìn)去即可衣赶。
這樣就可以調(diào)用到組件或者界面了。
如果組件之間有相同的接口厚满,那么還可以進(jìn)一步的把這些接口都抽離出來府瞄。這些抽離出來的接口變成“元接口”,它們是可以足夠支撐起整個(gè)組件一層的碘箍。
(5)CTMediator
說說casatwy的方案遵馆,這方案是基于Mediator的。
傳統(tǒng)的中間人Mediator的模式是這樣的:
這種模式每個(gè)頁(yè)面或者組件都會(huì)依賴中間者丰榴,各個(gè)組件之間互相不再依賴货邓,組件間調(diào)用只依賴中間者M(jìn)ediator,Mediator還是會(huì)依賴其他組件四濒。那么這是最終方案了么换况?
看看@casatwy是怎么繼續(xù)優(yōu)化的职辨。
主要思想是利用了Target-Action簡(jiǎn)單粗暴的思想,利用Runtime解決解耦的問題复隆。
targetName就是調(diào)用接口的Object拨匆,actionName就是調(diào)用方法的SEL,params是參數(shù)挽拂,shouldCacheTarget代表是否需要緩存惭每,如果需要緩存就把target存起來,Key是targetClassString亏栈,Value是target台腥。
通過這種方式進(jìn)行改造的,外面調(diào)用的方法都很統(tǒng)一绒北,都是調(diào)用performTarget: action: params: shouldCacheTarget:黎侈。第三個(gè)參數(shù)是一個(gè)字典,這個(gè)字典里面可以傳很多參數(shù)闷游,只要Key-Value寫好就可以了峻汉。處理錯(cuò)誤的方式也統(tǒng)一在一個(gè)地方了,target沒有脐往,或者是target無(wú)法響應(yīng)相應(yīng)的方法休吠,都可以在Mediator這里進(jìn)行統(tǒng)一出錯(cuò)處理。
但是在實(shí)際開發(fā)過程中业簿,不管是界面調(diào)用瘤礁,組件間調(diào)用,在Mediator中需要定義很多方法梅尤。于是做作者又想出了建議我們用Category的方法柜思,對(duì)Mediator的所有方法進(jìn)行拆分,這樣就就可以不會(huì)導(dǎo)致Mediator這個(gè)類過于龐大了巷燥。
把這些具體的方法一個(gè)個(gè)的都寫在Category里面就好了赡盘,調(diào)用的方式都非常的一致,都是調(diào)用performTarget: action: params: shouldCacheTarget:方法缰揪。
最終去掉了中間者M(jìn)ediator對(duì)組件的依賴亡脑,各個(gè)組件之間互相不再依賴,組件間調(diào)用只依賴中間者M(jìn)ediator邀跃,Mediator不依賴其他任何組件。
(6)一些并沒有開源的方案
Uber 騎手App的一個(gè)方案蛙紫。拍屑。。坑傅。
各個(gè)方案優(yōu)缺點(diǎn)
1. URLRoute注冊(cè)方案的優(yōu)缺點(diǎn)
首先URLRoute也許是借鑒前端Router和系統(tǒng)App內(nèi)跳轉(zhuǎn)的方式想出來的方法僵驰。它通過URL來請(qǐng)求資源。不管是H5,RN蒜茴,Weex星爪,iOS界面或者組件請(qǐng)求資源的方式就都統(tǒng)一了。URL里面也會(huì)帶上參數(shù)粉私,這樣調(diào)用什么界面或者組件都可以顽腾。所以這種方式是最容易,也是最先可以想到的诺核。
URLRoute的優(yōu)點(diǎn)很多抄肖,最大的優(yōu)點(diǎn)就是:服務(wù)器可以動(dòng)態(tài)的控制頁(yè)面跳轉(zhuǎn),可以統(tǒng)一處理頁(yè)面出問題之后的錯(cuò)誤處理窖杀,可以統(tǒng)一三端漓摩,iOS,Android入客,H5 / RN / Weex 的請(qǐng)求方式管毙。
但是這種方式也需要看不同公司的需求。如果公司里面已經(jīng)完成了服務(wù)器端動(dòng)態(tài)下發(fā)的腳手架工具桌硫,前端也完成了Native端如果出現(xiàn)錯(cuò)誤了夭咬,可以隨時(shí)替換相同業(yè)務(wù)界面的需求,那么這個(gè)時(shí)候可能選擇URLRoute的幾率會(huì)更大鞍泉。
但是如果公司里面H5沒有做相關(guān)出現(xiàn)問題后能替換的界面皱埠,H5開發(fā)人員覺得這是給他們?cè)鎏碡?fù)擔(dān)。如果公司也沒有完成服務(wù)器動(dòng)態(tài)下發(fā)路由規(guī)則的那套系統(tǒng)咖驮,那么公司可能就不會(huì)采用URLRoute的方式边器。因?yàn)閁RLRoute帶來的少量動(dòng)態(tài)性,公司是可以用JSPatch來做到托修。線上出現(xiàn)bug了忘巧,可以立即用JSPatch修掉,而不采用URLRoute去做睦刃。
所以選擇URLRoute這種方案砚嘴,也要看公司的發(fā)展情況和人員分配,技術(shù)選型方面涩拙。
URLRoute方案也是存在一些缺點(diǎn):
首先URL的map規(guī)則是需要注冊(cè)的际长,它們會(huì)在load方法里面寫。寫在load方法里面是會(huì)影響App啟動(dòng)速度的兴泥。
其次是大量的硬編碼工育。URL鏈接里面關(guān)于組件和頁(yè)面的名字都是硬編碼,參數(shù)也都是硬編碼搓彻。而且每個(gè)URL參數(shù)字段都必須要一個(gè)文檔進(jìn)行維護(hù)如绸,這個(gè)對(duì)于業(yè)務(wù)開發(fā)人員也是一個(gè)負(fù)擔(dān)嘱朽。而且URL短連接散落在整個(gè)App四處,維護(hù)起來實(shí)在有點(diǎn)麻煩怔接,雖然蘑菇街想到了用宏統(tǒng)一管理這些鏈接搪泳,但是還是解決不了硬編碼的問題。
最后一個(gè)缺點(diǎn)是扼脐,對(duì)于傳遞NSObject的參數(shù)岸军,URL是不夠友好的,它最多是傳遞一個(gè)字典谎势。
真正一個(gè)好的路由是在無(wú)形當(dāng)中服務(wù)整個(gè)App的凛膏,是一個(gè)無(wú)感知的過程,從這一點(diǎn)來說脏榆,略有點(diǎn)缺失猖毫。
2. Protocol-Class注冊(cè)方案的優(yōu)缺點(diǎn)
Protocol-Class方案的優(yōu)點(diǎn),這個(gè)方案沒有硬編碼须喂。
Protocol-Class方案也是存在一些缺點(diǎn)的吁断,每個(gè)Protocol都要向ModuleManager進(jìn)行注冊(cè)。
這種方案ModuleEntry是同時(shí)需要依賴ModuleManager和組件里面的頁(yè)面或者組件兩者的坞生。當(dāng)然ModuleEntry也是會(huì)依賴ModuleEntryProtocol的仔役,但是這個(gè)依賴是可以去掉的,比如用Runtime的方法NSProtocolFromString是己,加上硬編碼是可以去掉對(duì)Protocol的依賴的又兵。但是考慮到硬編碼的方式對(duì)出現(xiàn)bug,后期維護(hù)都是不友好的卒废,所以對(duì)Protocol的依賴還是不要去除沛厨。
最后一個(gè)缺點(diǎn)是組件方法的調(diào)用是分散在各處的,沒有統(tǒng)一的入口摔认,也就沒法做組件不存在時(shí)或者出現(xiàn)錯(cuò)誤時(shí)的統(tǒng)一處理逆皮。
3. Target-Action方案的優(yōu)缺點(diǎn)
Target-Action方案的優(yōu)點(diǎn),充分的利用Runtime的特性参袱,無(wú)需注冊(cè)這一步电谣。Target-Action方案只有存在組件依賴Mediator這一層依賴關(guān)系。在Mediator中維護(hù)針對(duì)Mediator的Category抹蚀,每個(gè)category對(duì)應(yīng)一個(gè)Target剿牺,Categroy中的方法對(duì)應(yīng)Action場(chǎng)景。Target-Action方案也統(tǒng)一了所有組件間調(diào)用入口环壤。
Target-Action方案也能有一定的安全保證晒来,它對(duì)url中進(jìn)行Native前綴進(jìn)行驗(yàn)證。
Target-Action方案的缺點(diǎn)镐捧,Target_Action在Category中將常規(guī)參數(shù)打包成字典潜索,在Target處再把字典拆包成常規(guī)參數(shù),這就造成了一部分的硬編碼懂酱。
4. 組件如何拆分竹习?
這個(gè)問題其實(shí)應(yīng)該是在打算實(shí)施組件化之前就應(yīng)該考慮的問題。為何還要放在這里說呢列牺?因?yàn)榻M件的拆分每個(gè)公司都有屬于自己的拆分方案整陌,按照業(yè)務(wù)線拆?按照最細(xì)小的業(yè)務(wù)功能模塊拆瞎领?還是按照一個(gè)完成的功能進(jìn)行拆分泌辫?這個(gè)就牽扯到了拆分粗細(xì)度的問題了。組件拆分的粗細(xì)度就會(huì)直接關(guān)系到未來路由需要解耦的程度九默。
假設(shè)震放,把登錄的所有流程封裝成一個(gè)組件,由于登錄里面會(huì)涉及到多個(gè)頁(yè)面驼修,那么這些頁(yè)面都會(huì)打包在一個(gè)組件里面殿遂。那么其他模塊需要調(diào)用登錄狀態(tài)的時(shí)候,這時(shí)候就需要用到登錄組件暴露在外面可以獲取登錄狀態(tài)的接口乙各。那么這個(gè)時(shí)候就可以考慮把這些接口寫到Protocol里面墨礁,暴露給外面使用《停或者用Target-Action的方法恩静。這種把一個(gè)功能全部都劃分成登錄組件的話,劃分粒度就稍微粗一點(diǎn)蹲坷。
如果僅僅把登錄狀態(tài)的細(xì)小功能劃分成一個(gè)元組件驶乾,那么外面想獲取登錄狀態(tài)就直接調(diào)用這個(gè)組件就好。這種劃分的粒度就非常細(xì)了冠句。這樣就會(huì)導(dǎo)致組件個(gè)數(shù)巨多轻掩。
所以在進(jìn)行拆分組件的時(shí)候,也許當(dāng)時(shí)業(yè)務(wù)并不復(fù)雜的時(shí)候懦底,拆分成組件唇牧,相互耦合也不大。但是隨著業(yè)務(wù)不管變化聚唐,之前劃分的組件間耦合性越來越大丐重,于是就會(huì)考慮繼續(xù)把之前的組件再進(jìn)行拆分。也許有些業(yè)務(wù)砍掉了杆查,之前一些小的組件也許還會(huì)被組合到一起扮惦。總之亲桦,在業(yè)務(wù)沒有完全固定下來之前崖蜜,組件的劃分可能一直進(jìn)行時(shí)浊仆。
最好的方案:
關(guān)于架構(gòu),我覺得拋開業(yè)務(wù)談架構(gòu)是沒有意義的豫领。因?yàn)榧軜?gòu)是為了業(yè)務(wù)服務(wù)的抡柿,空談架構(gòu)只是一種理想的狀態(tài)。所以沒有最好的方案等恐,只有最適合的方案洲劣。
最適合自己公司業(yè)務(wù)的方案才是最好的方案。分而治之课蔬,針對(duì)不同業(yè)務(wù)選擇不同的方案才是最優(yōu)的解決方案囱稽。如果非要籠統(tǒng)的采用一種方案,不同業(yè)務(wù)之間需要同一種方案二跋,需要妥協(xié)犧牲的東西太多就不好了战惊。
References:iOS 組件化 —— 路由設(shè)計(jì)思路分析
以下部分是最開始寫的。
中間件是一種設(shè)計(jì)思想同欠,兩個(gè)組件并不是直接交互样傍,而是通過一個(gè)第三平臺(tái).
蘑菇街:mgjroute(mgj路由器)
各個(gè)需要交互的組件和宿主工程都要依賴中間件mgjroute;
mgjroute的內(nèi)部在維護(hù)一張表(url-block铺遂,即一個(gè)url字符串對(duì)應(yīng)一個(gè)block)衫哥,這個(gè)表是各個(gè)組件共享的。
示例1:如果想給外界提供一個(gè)VC控制器襟锐,[registerURL:@"xiazai://VC" withBlock:{ reture VC }];寫在下載聽撤逢;url就是@"xiazai://VC",block就是{ reture VC }粮坞。
示例2:播放器想拿到下載聽的控制器VC蚊荣,[mgjroute openURL:@"xiazai://VC"],返回一個(gè)VC
弊端:
1、每個(gè)組件都要依賴mgjroute中間件莫杈;
2互例、url如果特別多,會(huì)越來越難維護(hù)筝闹;
3媳叨、傳一些參數(shù)還要硬解碼,把里面的url翻譯過來关顷,把參數(shù)取出來糊秆。
target-action
中間件用perform target action,target可以通過運(yùn)行時(shí)來獲取议双,action也可以通過運(yùn)行時(shí)來獲榷环;
各個(gè)組件無(wú)需依賴target-action
建議完成MainModuleAPI
無(wú)論選擇哪個(gè)方案,都建議做好統(tǒng)一披露API汞舱,即完成MainModuleAPI.m伍纫、MainModuleAPI.h。