原文鏈接:https://github.com/halfrost/Halfrost-Field/blob/master/contents/iOSRouter/iOS%20組件化%20——%20路由設(shè)計思路分析.md
前言
隨著用戶的需求越來越多,對App的用戶體驗也變的要求越來越高。為了更好的應(yīng)對各種需求来累,開發(fā)人員從軟件工程的角度,將App架構(gòu)由原來簡單的MVC變成MVVM渠羞,VIPER等復(fù)雜架構(gòu)。更換適合業(yè)務(wù)的架構(gòu),是為了后期能更好的維護(hù)項目。
但是用戶依舊不滿意铐望,繼續(xù)對開發(fā)人員提出了更多更高的要求,不僅需要高質(zhì)量的用戶體驗茂附,還要求快速迭代正蛙,最好一天出一個新功能,而且用戶還要求不更新就能體驗到新功能营曼。為了滿足用戶需求乒验,于是開發(fā)人員就用H5,ReactNative蒂阱,Weex等技術(shù)對已有的項目進(jìn)行改造锻全。項目架構(gòu)也變得更加的復(fù)雜,縱向的會進(jìn)行分層录煤,網(wǎng)絡(luò)層鳄厌,UI層,數(shù)據(jù)持久層妈踊。每一層橫向的也會根據(jù)業(yè)務(wù)進(jìn)行組件化了嚎。盡管這樣做了以后會讓開發(fā)更加有效率,更加好維護(hù)廊营,但是如何解耦各層歪泳,解耦各個界面和各個組件,降低各個組件之間的耦合度赘风,如何能讓整個系統(tǒng)不管多么復(fù)雜的情況下都能保持“高內(nèi)聚夹囚,低耦合”的特點(diǎn)?這一系列的問題都擺在開發(fā)人員面前邀窃,亟待解決荸哟。今天就來談?wù)劷鉀Q這個問題的一些思路。
目錄
- 1.引子
- 2.App路由能解決哪些問題
- 3.App之間跳轉(zhuǎn)實(shí)現(xiàn)
- 4.App內(nèi)組件間路由設(shè)計
- 5.各個方案優(yōu)缺點(diǎn)
- 6.最好的方案
一. 引子
大前端發(fā)展這么多年了瞬捕,相信也一定會遇到相似的問題鞍历。近兩年SPA發(fā)展極其迅猛,React 和 Vue一直處于風(fēng)口浪尖肪虎,那我們就看看他們是如何處理好這一問題的劣砍。
在SPA單頁面應(yīng)用,路由起到了很關(guān)鍵的作用扇救。路由的作用主要是保證視圖和 URL 的同步刑枝。在前端的眼里看來香嗓,視圖是被看成是資源的一種表現(xiàn)。當(dāng)用戶在頁面中進(jìn)行操作時装畅,應(yīng)用會在若干個交互狀態(tài)中切換靠娱,路由則可以記錄下某些重要的狀態(tài),比如用戶查看一個網(wǎng)站掠兄,用戶是否登錄像云、在訪問網(wǎng)站的哪一個頁面。而這些變化同樣會被記錄在瀏覽器的歷史中蚂夕,用戶可以通過瀏覽器的前進(jìn)迅诬、后退按鈕切換狀態(tài)⌒鲭梗總的來說侈贷,用戶可以通過手動輸入或者與頁面進(jìn)行交互來改變 URL,然后通過同步或者異步的方式向服務(wù)端發(fā)送請求獲取資源牍汹,成功后重新繪制 UI铐维,原理如下圖所示:
react-router通過傳入的location到最終渲染新的UI,流程如下:
location的來源有2種慎菲,一種是瀏覽器的回退和前進(jìn)嫁蛇,另外一種是直接點(diǎn)了一個鏈接。新的 location 對象后露该,路由內(nèi)部的 matchRoutes 方法會匹配出 Route 組件樹中與當(dāng)前 location 對象匹配的一個子集睬棚,并且得到了 nextState,在this.setState(nextState) 時就可以實(shí)現(xiàn)重新渲染 Router 組件解幼。
大前端的做法大概是這樣的抑党,我們可以把這些思想借鑒到iOS這邊來。上圖中的Back / Forward 在iOS這邊很多情況下都可以被UINavgation所管理撵摆。所以iOS的Router主要處理綠色的那一塊底靠。
二. App路由能解決哪些問題
既然前端能在SPA上解決URL和UI的同步問題,那這種思想可以在App上解決哪些問題呢特铝?
思考如下的問題暑中,平時我們開發(fā)中是如何優(yōu)雅的解決的:
1.3D-Touch功能或者點(diǎn)擊推送消息,要求外部跳轉(zhuǎn)到App內(nèi)部一個很深層次的一個界面鲫剿。
比如微信的3D-Touch可以直接跳轉(zhuǎn)到“我的二維碼”鳄逾。“我的二維碼”界面在我的里面的第三級界面灵莲〉癜迹或者再極端一點(diǎn),產(chǎn)品需求給了更加變態(tài)的需求,要求跳轉(zhuǎn)到App內(nèi)部第十層的界面枚抵,怎么處理线欲?
2.自家的一系列App之間如何相互跳轉(zhuǎn)?
如果自己App有幾個汽摹,相互之間還想相互跳轉(zhuǎn)询筏,怎么處理?
3.如何解除App組件之間和App頁面之間的耦合性竖慧?
隨著項目越來越復(fù)雜,各個組件逆屡,各個頁面之間的跳轉(zhuǎn)邏輯關(guān)聯(lián)性越來越多圾旨,如何能優(yōu)雅的解除各個組件和頁面之間的耦合性?
4.如何能統(tǒng)一iOS和Android兩端的頁面跳轉(zhuǎn)邏輯魏蔗?甚至如何能統(tǒng)一三端的請求資源的方式砍的?
項目里面某些模塊會混合ReactNative,Weex莺治,H5界面廓鞠,這些界面還會調(diào)用Native的界面,以及Native的組件谣旁。那么床佳,如何能統(tǒng)一Web端和Native端請求資源的方式?
5.如果使用了動態(tài)下發(fā)配置文件來配置App的跳轉(zhuǎn)邏輯榄审,那么如果做到iOS和Android兩邊只要共用一套配置文件砌们?
6.如果App出現(xiàn)bug了,如何不用JSPatch搁进,就能做到簡單的熱修復(fù)功能浪感?
比如App上線突然遇到了緊急bug,能否把頁面動態(tài)降級成H5饼问,ReactNative影兽,Weex?或者是直接換成一個本地的錯誤界面莱革?
7.如何在每個組件間調(diào)用和頁面跳轉(zhuǎn)時都進(jìn)行埋點(diǎn)統(tǒng)計峻堰?每個跳轉(zhuǎn)的地方都手寫代碼埋點(diǎn)?利用Runtime AOP 驮吱?
8.如何在每個組件間調(diào)用的過程中茧妒,加入調(diào)用的邏輯檢查,令牌機(jī)制左冬,配合灰度進(jìn)行風(fēng)控邏輯桐筏?
9.如何在App任何界面都可以調(diào)用同一個界面或者同一個組件?只能在AppDelegate里面注冊單例來實(shí)現(xiàn)拇砰?
比如App出現(xiàn)問題了梅忌,用戶可能在任何界面狰腌,如何隨時隨地的讓用戶強(qiáng)制登出?或者強(qiáng)制都跳轉(zhuǎn)到同一個本地的error界面牧氮?或者跳轉(zhuǎn)到相應(yīng)的H5琼腔,ReactNative,Weex界面踱葛?如何讓用戶在任何界面丹莲,隨時隨地的彈出一個View ?
以上這些問題其實(shí)都可以通過在App端設(shè)計一個路由來解決尸诽。那么我們怎么設(shè)計一個路由呢甥材?
三. App之間跳轉(zhuǎn)實(shí)現(xiàn)
在談App內(nèi)部的路由之前,先來談?wù)勗趇OS系統(tǒng)間性含,不同App之間是怎么實(shí)現(xiàn)跳轉(zhuǎn)的洲赵。
1. URL Scheme方式
iOS系統(tǒng)是默認(rèn)支持URL Scheme的,具體見官方文檔商蕴。
比如說叠萍,在iPhone的Safari瀏覽器上面輸入如下的命令,會自動打開一些App:
// 打開郵箱
mailto://
// 給110撥打電話
tel://110
在iOS 9 之前只要在App的info.plist里面添加URL types - URL Schemes绪商,如下圖:
這里就添加了一個com.ios.Qhomer的Scheme苛谷。這樣就可以在iPhone的Safari瀏覽器上面輸入:
com.ios.Qhomer://
就可以直接打開這個App了。
關(guān)于其他一些常見的App部宿,可以從iTunes里面下載到它的ipa文件抄腔,解壓,顯示包內(nèi)容里面可以找到info.plist文件理张,打開它赫蛇,在里面就可以相應(yīng)的URL Scheme。
// 手機(jī)QQ
mqq://
// 微信
weixin://
// 新浪微博
sinaweibo://
// 餓了么
eleme://
當(dāng)然了雾叭,某些App對于調(diào)用URL Scheme比較敏感悟耘,它們不希望其他的App隨意的就調(diào)用自己。
- (BOOL)application:(UIApplication *)application
openURL:(NSURL *)url
sourceApplication:(NSString *)sourceApplication
annotation:(id)annotation
{
NSLog(@"sourceApplication: %@", sourceApplication);
NSLog(@"URL scheme:%@", [url scheme]);
NSLog(@"URL query: %@", [url query]);
if ([sourceApplication isEqualToString:@"com.tencent.weixin"]){
// 允許打開
return YES;
}else{
return NO;
}
}
如果待調(diào)用的App已經(jīng)運(yùn)行了织狐,那么它的生命周期如下:
如果待調(diào)用的App在后臺暂幼,那么它的生命周期如下:
明白了上面的生命周期之后,我們就可以通過調(diào)用application:openURL:sourceApplication:annotation:這個方法移迫,來阻止一些App的隨意調(diào)用旺嬉。
如上圖,餓了么App允許通過URL Scheme調(diào)用厨埋,那么我們可以在Safari里面調(diào)用到餓了么App邪媳。手機(jī)QQ不允許調(diào)用,我們在Safari里面也就沒法跳轉(zhuǎn)過去。
關(guān)于App間的跳轉(zhuǎn)問題雨效,感興趣的可以查看官方文檔Inter-App Communication迅涮。
App也是可以直接跳轉(zhuǎn)到系統(tǒng)設(shè)置的。比如有些需求要求檢測用戶有沒有開啟某些系統(tǒng)權(quán)限徽龟,如果沒有開啟就彈框提示叮姑,點(diǎn)擊彈框的按鈕直接跳轉(zhuǎn)到系統(tǒng)設(shè)置里面對應(yīng)的設(shè)置界面。
iOS 10 支持通過 URL Scheme 跳轉(zhuǎn)到系統(tǒng)設(shè)置
iOS10跳轉(zhuǎn)系統(tǒng)設(shè)置的正確姿勢
關(guān)于 iOS 系統(tǒng)功能的 URL 匯總列表
2. Universal Links方式
雖然在微信內(nèi)部開網(wǎng)頁會禁止所有的Scheme极颓,但是iOS 9.0新增加了一項功能是Universal Links讼昆,使用這個功能可以使我們的App通過HTTP鏈接來啟動App。
1.如果安裝過App浸赫,不管在微信里面http鏈接還是在Safari瀏覽器,還是其他第三方瀏覽器运敢,都可以打開App稻扬。
2.如果沒有安裝過App盼砍,就會打開網(wǎng)頁。
具體設(shè)置需要3步:
1.App需要開啟Associated Domains服務(wù)近刘,并設(shè)置Domains,注意必須要applinks:開頭蜕猫。
2.域名必須要支持HTTPS漱挚。
3.上傳內(nèi)容是Json格式的文件蹬屹,文件名為apple-app-site-association到自己域名的根目錄下,或者.well-known目錄下。iOS自動會去讀取這個文件。具體的文件內(nèi)容請查看官方文檔霎箍。
如果App支持了Universal Links方式缀壤,那么可以在其他App里面直接跳轉(zhuǎn)到我們自己的App里面筋夏。如下圖,點(diǎn)擊鏈接鸿染,由于該鏈接會Matcher到我們設(shè)置的鏈接绽媒,所以菜單里面會顯示用我們的App打開囤热。
在瀏覽器里面也是一樣的效果疙教,如果是支持了Universal Links方式,訪問相應(yīng)的URL,會有不同的效果。如下圖:
以上就是iOS系統(tǒng)中App間跳轉(zhuǎn)的二種方式。
從iOS 系統(tǒng)里面支持的URL Scheme方式,我們可以看出,對于一個資源的訪問粤攒,蘋果也是用URI的方式來訪問的。
統(tǒng)一資源標(biāo)識符(英語:Uniform Resource Identifier,或URI)是一個用于標(biāo)識某一互聯(lián)網(wǎng)資源名稱的字符串。 該種標(biāo)識允許用戶對網(wǎng)絡(luò)中(一般指萬維網(wǎng))的資源通過特定的協(xié)議進(jìn)行交互操作旗国。URI的最常見的形式是統(tǒng)一資源定位符(URL)肿轨。
舉個例子:
這是一段URI,每一段都代表了對應(yīng)的含義。對方接收到了這樣一串字符串京办,按照規(guī)則解析出來叶雹,就能獲取到所有的有用信息佑力。
這個能給我們設(shè)計App組件間的路由帶來一些思路么?如果我們想要定義一個三端(iOS,Android,H5)的統(tǒng)一訪問資源的方式鲸沮,能用URI的這種方式實(shí)現(xiàn)么最易?
四. App內(nèi)組件間路由設(shè)計
上一章節(jié)中我們介紹了iOS系統(tǒng)中,系統(tǒng)是如何幫我們處理App間跳轉(zhuǎn)邏輯的。這一章節(jié)我們著重討論一下汪茧,App內(nèi)部仆百,各個組件之間的路由應(yīng)該怎么設(shè)計省古。關(guān)于App內(nèi)部的路由設(shè)計惜互,主要需要解決2個問題:
1.各個頁面和組件之間的跳轉(zhuǎn)問題。
2.各個組件之間相互調(diào)用。
先來分析一下這兩個問題。
1. 關(guān)于頁面跳轉(zhuǎn)
在iOS開發(fā)的過程中,經(jīng)常會遇到以下的場景蔼两,點(diǎn)擊按鈕跳轉(zhuǎn)Push到另外一個界面妙啃,或者點(diǎn)擊一個cell Present一個新的ViewController抑胎。在MVC模式中,一般都是新建一個VC,然后Push / Present到下一個VC。但是在MVVM中谜洽,會有一些不合適的情況晤郑。
眾所周知,MVVM把MVC拆成了上圖演示的樣子,原來View對應(yīng)的與數(shù)據(jù)相關(guān)的代碼都移到ViewModel中,相應(yīng)的C也變瘦了,演變成了M-VM-C-V的結(jié)構(gòu)。這里的C里面的代碼可以只剩下頁面跳轉(zhuǎn)相關(guān)的邏輯。如果用代碼表示就是下面這樣子:
假設(shè)一個按鈕的執(zhí)行邏輯都封裝成了command。
@weakify(self);
[[[_viewModel.someCommand executionSignals] flatten] subscribeNext:^(id x) {
@strongify(self);
// 跳轉(zhuǎn)邏輯
[self.navigationController pushViewController:targetViewController animated:YES];
}];
上述的代碼本身沒啥問題己单,但是可能會弱化MVVM框架的一個重要作用苟跪。
MVVM框架的目的除去解耦以外元暴,還有2個很重要的目的:
- 代碼高復(fù)用率
- 方便進(jìn)行單元測試
如果需要測試一個業(yè)務(wù)是否正確,我們只要對ViewModel進(jìn)行單元測試即可。前提是假定我們使用ReactiveCocoa進(jìn)行UI綁定的過程是準(zhǔn)確無誤的核蘸。目前綁定是正確的。所以我們只需要單元測試到ViewModel即可完成業(yè)務(wù)邏輯的測試。
頁面跳轉(zhuǎn)也屬于業(yè)務(wù)邏輯,所以應(yīng)該放在ViewModel中一起單元測試,保證業(yè)務(wù)邏輯測試的覆蓋率撤蟆。
把頁面跳轉(zhuǎn)放到ViewModel中,有2種做法式镐,第一種就是用路由來實(shí)現(xiàn)夕玩,第二種由于和路由沒有關(guān)系缤弦,所以這里就不多闡述,有興趣的可以看lpd-mvvm-kit這個庫關(guān)于頁面跳轉(zhuǎn)的具體實(shí)現(xiàn)。
頁面跳轉(zhuǎn)相互的耦合性也就體現(xiàn)出來了:
1.由于pushViewController或者presentViewController,后面都需要帶一個待操作的ViewController友题,那么就必須要引入該類,import頭文件也就引入了耦合性。
2.由于跳轉(zhuǎn)這里寫死了跳轉(zhuǎn)操作,如果線上一旦出現(xiàn)了bug崭捍,這里是不受我們控制的。
3.推送消息或者是3D-Touch需求粒梦,要求直接跳轉(zhuǎn)到內(nèi)部第10級界面亮航,那么就需要寫一個入口跳轉(zhuǎn)到指定界面。
2. 關(guān)于組件間調(diào)用
關(guān)于組件間的調(diào)用匀们,也需要解耦。隨著業(yè)務(wù)越來越復(fù)雜泄朴,我們封裝的組件越來越多重抖,要是封裝的粒度拿捏不準(zhǔn),就會出現(xiàn)大量組件之間耦合度高的問題祖灰。組件的粒度可以隨著業(yè)務(wù)的調(diào)整钟沛,不斷的調(diào)整組件職責(zé)的劃分。但是組件之間的調(diào)用依舊不可避免局扶,相互調(diào)用對方組件暴露的接口恨统。如何減少各個組件之間的耦合度,是一個設(shè)計優(yōu)秀的路由的職責(zé)所在三妈。
3. 如何設(shè)計一個路由
如何設(shè)計一個能完美解決上述2個問題的路由畜埋,讓我們先來看看GitHub上優(yōu)秀開源庫的設(shè)計思路。以下是我從Github上面找的一些路由方案畴蒲,按照Star從高到低排列由捎。依次來分析一下它們各自的設(shè)計思路。
(1)JLRoutes Star 3189
JLRoutes在整個Github上面Star最多饿凛,那就來從它來分析分析它的具體設(shè)計思路狞玛。
首先JLRoutes是受URL Scheme思路的影響。它把所有對資源的請求看成是一個URI涧窒。
首先來熟悉一下NSURLComponent的各個字段:
Note
The URLs employed by the NSURL
class are described in RFC 1808, RFC 1738, and RFC 2732.
JLRoutes會傳入每個字符串心肪,都按照上面的樣子進(jìn)行切分處理,分別根據(jù)RFC的標(biāo)準(zhǔn)定義纠吴,取到各個NSURLComponent硬鞍。
JLRoutes全局會保存一個Map,這個Map會以scheme為Key,JLRoutes為Value固该。所以在routeControllerMap里面每個scheme都是唯一的锅减。
至于為何有這么多條路由,筆者認(rèn)為伐坏,如果路由按照業(yè)務(wù)線進(jìn)行劃分的話怔匣,每個業(yè)務(wù)線可能會有不相同的邏輯,即使每個業(yè)務(wù)里面的組件名字可能相同桦沉,但是由于業(yè)務(wù)線不同每瞒,會有不同的路由規(guī)則。
舉個例子:如果滴滴按照每個城市的打車業(yè)務(wù)進(jìn)行組件化拆分纯露,那么每個城市就對應(yīng)著這里的每個scheme剿骨。每個城市的打車業(yè)務(wù)都有叫車,付款……等業(yè)務(wù)埠褪,但是由于每個城市的地方法規(guī)不相同浓利,所以這些組件即使名字相同,但是里面的功能也許千差萬別钞速。所以這里劃分出了多個route荞膘,也可以理解為不同的命名空間。
在每個JLRoutes里面都保存了一個數(shù)組玉工,這個數(shù)組里面保存了每個路由規(guī)則JLRRouteDefinition里面會保存外部傳進(jìn)來的block閉包羽资,pattern,和拆分之后的pattern遵班。
在每個JLRoutes的數(shù)組里面屠升,會按照路由的優(yōu)先級進(jìn)行排列,優(yōu)先級高的排列在前面狭郑。
- (void)_registerRoute:(NSString *)routePattern priority:(NSUInteger)priority handler:(BOOL (^)(NSDictionary *parameters))handlerBlock
{
JLRRouteDefinition *route = [[JLRRouteDefinition alloc] initWithScheme:self.scheme pattern:routePattern priority:priority handlerBlock:handlerBlock];
if (priority == 0 || self.routes.count == 0) {
[self.routes addObject:route];
} else {
NSUInteger index = 0;
BOOL addedRoute = NO;
// 找到當(dāng)前已經(jīng)存在的一條優(yōu)先級比當(dāng)前待插入的路由低的路由
for (JLRRouteDefinition *existingRoute in [self.routes copy]) {
if (existingRoute.priority < priority) {
// 如果找到腹暖,就插入數(shù)組
[self.routes insertObject:route atIndex:index];
addedRoute = YES;
break;
}
index++;
}
// 如果沒有找到任何一條路由比當(dāng)前待插入的路由低的路由,或者最后一條路由優(yōu)先級和當(dāng)前路由一樣翰萨,那么就只能插入到最后脏答。
if (!addedRoute) {
[self.routes addObject:route];
}
}
}
由于這個數(shù)組里面的路由是一個單調(diào)隊列,所以查找優(yōu)先級的時候只用從高往低遍歷即可亩鬼。
具體查找路由的過程如下:
首先根據(jù)外部傳進(jìn)來的URL初始化一個JLRRouteRequest殖告,然后用這個JLRRouteRequest在當(dāng)前的路由數(shù)組里面依次request,每個規(guī)則都會生成一個response雳锋,但是只有符合條件的response才會match黄绩,最后取出匹配的JLRRouteResponse拿出其字典parameters里面對應(yīng)的參數(shù)就可以了。查找和匹配過程中重要的代碼如下:
- (BOOL)_routeURL:(NSURL *)URL withParameters:(NSDictionary *)parameters executeRouteBlock:(BOOL)executeRouteBlock
{
if (!URL) {
return NO;
}
[self _verboseLog:@"Trying to route URL %@", URL];
BOOL didRoute = NO;
JLRRouteRequest *request = [[JLRRouteRequest alloc] initWithURL:URL];
for (JLRRouteDefinition *route in [self.routes copy]) {
// 檢查每一個route玷过,生成對應(yīng)的response
JLRRouteResponse *response = [route routeResponseForRequest:request decodePlusSymbols:shouldDecodePlusSymbols];
if (!response.isMatch) {
continue;
}
[self _verboseLog:@"Successfully matched %@", route];
if (!executeRouteBlock) {
// 如果我們被要求不允許執(zhí)行爽丹,但是又找了匹配的路由response筑煮。
return YES;
}
// 裝配最后的參數(shù)
NSMutableDictionary *finalParameters = [NSMutableDictionary dictionary];
[finalParameters addEntriesFromDictionary:response.parameters];
[finalParameters addEntriesFromDictionary:parameters];
[self _verboseLog:@"Final parameters are %@", finalParameters];
didRoute = [route callHandlerBlockWithParameters:finalParameters];
if (didRoute) {
// 調(diào)用Handler成功
break;
}
}
if (!didRoute) {
[self _verboseLog:@"Could not find a matching route"];
}
// 如果在當(dāng)前路由規(guī)則里面沒有找到匹配的路由,當(dāng)前路由不是global 的粤蝎,并且允許降級到global里面去查找真仲,那么我們繼續(xù)在global的路由規(guī)則里面去查找。
if (!didRoute && self.shouldFallbackToGlobalRoutes && ![self _isGlobalRoutesController]) {
[self _verboseLog:@"Falling back to global routes..."];
didRoute = [[JLRoutes globalRoutes] _routeURL:URL withParameters:parameters executeRouteBlock:executeRouteBlock];
}
// 最后初澎,依舊沒有找到任何能匹配的秸应,如果有unmatched URL handler,調(diào)用這個閉包進(jìn)行最后的處理谤狡。
if, after everything, we did not route anything and we have an unmatched URL handler, then call it
if (!didRoute && executeRouteBlock && self.unmatchedURLHandler) {
[self _verboseLog:@"Falling back to the unmatched URL handler"];
self.unmatchedURLHandler(self, URL, parameters);
}
return didRoute;
}
舉個例子:
我們先注冊一個Router,規(guī)則如下:
[[JLRoutes globalRoutes] addRoute:@"/:object/:action" handler:^BOOL(NSDictionary *parameters) {
NSString *object = parameters[@"object"];
NSString *action = parameters[@"action"];
// stuff
return YES;
}];
我們傳入一個URL卧檐,讓Router進(jìn)行處理墓懂。
NSURL *editPost = [NSURL URLWithString:@"ele://post/halfrost?debug=true&foo=bar"];
[[UIApplication sharedApplication] openURL:editPost];
匹配成功之后,我們會得到下面這樣一個字典:
{
"object": "post",
"action": "halfrost",
"debug": "true",
"foo": "bar",
"JLRouteURL": "ele://post/halfrost?debug=true&foo=bar",
"JLRoutePattern": "/:object/:action",
"JLRouteScheme": "JLRoutesGlobalRoutesScheme"
}
把上述過程圖解出來霉囚,見下圖:
JLRoutes還可以支持Optional的路由規(guī)則捕仔,假如定義一條路由規(guī)則:
/the(/foo/:a)(/bar/:b)
JLRoutes 會幫我們默認(rèn)注冊如下4條路由規(guī)則:
/the/foo/:a/bar/:b
/the/foo/:a
/the/bar/:b
/the
(2)routable-ios Star 1415
Routable路由是用在in-app native端的 URL router, 它可以用在iOS上也可以用在Android上。
UPRouter里面保存了2個字典盈罐。routes字典里面存儲的Key是路由規(guī)則榜跌,Value存儲的是UPRouterOptions。cachedRoutes里面存儲的Key是最終的URL盅粪,帶傳參的钓葫,Value存儲的是RouterParams。RouterParams里面會包含在routes匹配的到的UPRouterOptions票顾,還有額外的打開參數(shù)openParams和一些額外參數(shù)extraParams础浮。
- (RouterParams *)routerParamsForUrl:(NSString *)url extraParams: (NSDictionary *)extraParams {
if (!url) {
//if we wait, caching this as key would throw an exception
if (_ignoresExceptions) {
return nil;
}
@throw [NSException exceptionWithName:@"RouteNotFoundException"
reason:[NSString stringWithFormat:ROUTE_NOT_FOUND_FORMAT, url]
userInfo:nil];
}
if ([self.cachedRoutes objectForKey:url] && !extraParams) {
return [self.cachedRoutes objectForKey:url];
}
// 比對url通過/分割之后的參數(shù)個數(shù)和pathComponents的個數(shù)是否一樣
NSArray *givenParts = url.pathComponents;
NSArray *legacyParts = [url componentsSeparatedByString:@"/"];
if ([legacyParts count] != [givenParts count]) {
NSLog(@"Routable Warning - your URL %@ has empty path components - this will throw an error in an upcoming release", url);
givenParts = legacyParts;
}
__block RouterParams *openParams = nil;
[self.routes enumerateKeysAndObjectsUsingBlock:
^(NSString *routerUrl, UPRouterOptions *routerOptions, BOOL *stop) {
NSArray *routerParts = [routerUrl pathComponents];
if ([routerParts count] == [givenParts count]) {
NSDictionary *givenParams = [self paramsForUrlComponents:givenParts routerUrlComponents:routerParts];
if (givenParams) {
openParams = [[RouterParams alloc] initWithRouterOptions:routerOptions openParams:givenParams extraParams: extraParams];
*stop = YES;
}
}
}];
if (!openParams) {
if (_ignoresExceptions) {
return nil;
}
@throw [NSException exceptionWithName:@"RouteNotFoundException"
reason:[NSString stringWithFormat:ROUTE_NOT_FOUND_FORMAT, url]
userInfo:nil];
}
[self.cachedRoutes setObject:openParams forKey:url];
return openParams;
}
這一段代碼里面重點(diǎn)在干一件事情,遍歷routes字典奠骄,然后找到參數(shù)匹配的字符串豆同,封裝成RouterParams返回。
- (NSDictionary *)paramsForUrlComponents:(NSArray *)givenUrlComponents routerUrlComponents:(NSArray *)routerUrlComponents {
__block NSMutableDictionary *params = [NSMutableDictionary dictionary];
[routerUrlComponents enumerateObjectsUsingBlock:
^(NSString *routerComponent, NSUInteger idx, BOOL *stop) {
NSString *givenComponent = givenUrlComponents[idx];
if ([routerComponent hasPrefix:@":"]) {
NSString *key = [routerComponent substringFromIndex:1];
[params setObject:givenComponent forKey:key];
}
else if (![routerComponent isEqualToString:givenComponent]) {
params = nil;
*stop = YES;
}
}];
return params;
}
上面這段函數(shù)含鳞,第一個參數(shù)是外部傳進(jìn)來URL帶有各個入?yún)⒌姆指顢?shù)組影锈。第二個參數(shù)是路由規(guī)則分割開的數(shù)組。routerComponent由于規(guī)定:號后面才是參數(shù)蝉绷,所以routerComponent的第1個位置就是對應(yīng)的參數(shù)名鸭廷。params字典里面以參數(shù)名為Key,參數(shù)為Value熔吗。
NSDictionary *givenParams = [self paramsForUrlComponents:givenParts routerUrlComponents:routerParts];
if (givenParams) {
openParams = [[RouterParams alloc] initWithRouterOptions:routerOptions openParams:givenParams extraParams: extraParams];
*stop = YES;
}
最后通過RouterParams的初始化方法靴姿,把路由規(guī)則對應(yīng)的UPRouterOptions,上一步封裝好的參數(shù)字典givenParams磁滚,還有
routerParamsForUrl: extraParams: 方法的第二個入?yún)⒎鹣牛@3個參數(shù)作為初始化參數(shù)宵晚,生成了一個RouterParams。
[self.cachedRoutes setObject:openParams forKey:url];
最后一步self.cachedRoutes的字典里面Key為帶參數(shù)的URL维雇,Value是RouterParams淤刃。
最后將匹配封裝出來的RouterParams轉(zhuǎn)換成對應(yīng)的Controller。
- (UIViewController *)controllerForRouterParams:(RouterParams *)params {
SEL CONTROLLER_CLASS_SELECTOR = sel_registerName("allocWithRouterParams:");
SEL CONTROLLER_SELECTOR = sel_registerName("initWithRouterParams:");
UIViewController *controller = nil;
Class controllerClass = params.routerOptions.openClass;
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
if ([controllerClass respondsToSelector:CONTROLLER_CLASS_SELECTOR]) {
controller = [controllerClass performSelector:CONTROLLER_CLASS_SELECTOR withObject:[params controllerParams]];
}
else if ([params.routerOptions.openClass instancesRespondToSelector:CONTROLLER_SELECTOR]) {
controller = [[params.routerOptions.openClass alloc] performSelector:CONTROLLER_SELECTOR withObject:[params controllerParams]];
}
#pragma clang diagnostic pop
if (!controller) {
if (_ignoresExceptions) {
return controller;
}
@throw [NSException exceptionWithName:@"RoutableInitializerNotFound"
reason:[NSString stringWithFormat:INVALID_CONTROLLER_FORMAT, NSStringFromClass(controllerClass), NSStringFromSelector(CONTROLLER_CLASS_SELECTOR), NSStringFromSelector(CONTROLLER_SELECTOR)]
userInfo:nil];
}
controller.modalTransitionStyle = params.routerOptions.transitionStyle;
controller.modalPresentationStyle = params.routerOptions.presentationStyle;
return controller;
}
如果Controller是一個類吱型,那么就調(diào)用allocWithRouterParams:方法去初始化逸贾。如果Controller已經(jīng)是一個實(shí)例了,那么就調(diào)用initWithRouterParams:方法去初始化津滞。
將Routable的大致流程圖解如下:
(3)HHRouter Star 1277
這是布丁動畫的一個Router铝侵,靈感來自于 ABRouter 和 Routable iOS。
先來看看HHRouter的Api触徐。它提供的方法非常清晰咪鲜。
ViewController提供了2個方法。map是用來設(shè)置路由規(guī)則撞鹉,matchController是用來匹配路由規(guī)則的疟丙,匹配爭取之后返回對應(yīng)的UIViewController。
- (void)map:(NSString *)route toControllerClass:(Class)controllerClass;
- (UIViewController *)matchController:(NSString *)route;
block閉包提供了三個方法鸟雏,map也是設(shè)置路由規(guī)則享郊,matchBlock:是用來匹配路由,找到指定的block孝鹊,但是不會調(diào)用該block炊琉。callBlock:是找到指定的block,找到以后就立即調(diào)用又活。
- (void)map:(NSString *)route toBlock:(HHRouterBlock)block;
- (HHRouterBlock)matchBlock:(NSString *)route;
- (id)callBlock:(NSString *)route;
matchBlock:和callBlock:的區(qū)別就在于前者不會自動調(diào)用閉包温自。所以matchBlock:方法找到對應(yīng)的block之后,如果想調(diào)用皇钞,需要手動調(diào)用一次悼泌。
除去上面這些方法,HHRouter還為我們提供了一個特殊的方法夹界。
- (HHRouteType)canRoute:(NSString *)route;
這個方法就是用來找到執(zhí)行路由規(guī)則對應(yīng)的RouteType馆里,RouteType總共就3種:
typedef NS_ENUM (NSInteger, HHRouteType) {
HHRouteTypeNone = 0,
HHRouteTypeViewController = 1,
HHRouteTypeBlock = 2
};
再來看看HHRouter是如何管理路由規(guī)則的。整個HHRouter就是由一個NSMutableDictionary *routes控制的可柿。
@interface HHRouter ()
@property (strong, nonatomic) NSMutableDictionary *routes;
@end
別看只有這一個看似“簡單”的字典數(shù)據(jù)結(jié)構(gòu)鸠踪,但是HHRouter路由設(shè)計的還是很精妙的。
- (void)map:(NSString *)route toBlock:(HHRouterBlock)block
{
NSMutableDictionary *subRoutes = [self subRoutesToRoute:route];
subRoutes[@"_"] = [block copy];
}
- (void)map:(NSString *)route toControllerClass:(Class)controllerClass
{
NSMutableDictionary *subRoutes = [self subRoutesToRoute:route];
subRoutes[@"_"] = controllerClass;
}
上面兩個方法分別是block閉包和ViewController設(shè)置路由規(guī)則調(diào)用的方法實(shí)體复斥。不管是ViewController還是block閉包营密,設(shè)置規(guī)則的時候都會調(diào)用subRoutesToRoute:方法。
- (NSMutableDictionary *)subRoutesToRoute:(NSString *)route
{
NSArray *pathComponents = [self pathComponentsFromRoute:route];
NSInteger index = 0;
NSMutableDictionary *subRoutes = self.routes;
while (index < pathComponents.count) {
NSString *pathComponent = pathComponents[index];
if (![subRoutes objectForKey:pathComponent]) {
subRoutes[pathComponent] = [[NSMutableDictionary alloc] init];
}
subRoutes = subRoutes[pathComponent];
index++;
}
return subRoutes;
}
上面這段函數(shù)就是來構(gòu)造路由匹配規(guī)則的字典目锭。
舉個例子:
[[HHRouter shared] map:@"/user/:userId/"
toControllerClass:[UserViewController class]];
[[HHRouter shared] map:@"/story/:storyId/"
toControllerClass:[StoryViewController class]];
[[HHRouter shared] map:@"/user/:userId/story/?a=0"
toControllerClass:[StoryListViewController class]];
設(shè)置3條規(guī)則以后评汰,按照上面構(gòu)造路由匹配規(guī)則的字典的方法纷捞,該路由規(guī)則字典就會變成這個樣子:
{
story = {
":storyId" = {
"_" = StoryViewController;
};
};
user = {
":userId" = {
"_" = UserViewController;
story = {
"_" = StoryListViewController;
};
};
};
}
路由規(guī)則字典生成之后,等到匹配的時候就會遍歷這個字典被去。
假設(shè)這時候有一條路由過來:
[[[HHRouter shared] matchController:@"hhrouter20://user/1/"] class],
HHRouter對這條路由的處理方式是先匹配前面的scheme主儡,如果連scheme都不正確的話,會直接導(dǎo)致后面匹配失敗惨缆。
然后再進(jìn)行路由匹配糜值,最后生成的參數(shù)字典如下:
{
"controller_class" = UserViewController;
route = "/user/1/";
userId = 1;
}
具體的路由參數(shù)匹配的函數(shù)在
- (NSDictionary *)paramsInRoute:(NSString *)route
這個方法里面實(shí)現(xiàn)的。這個方法就是按照路由匹配規(guī)則坯墨,把傳進(jìn)來的URL的參數(shù)都一一解析出來寂汇,帶?號的也都會解析成字典捣染。這個方法沒什么難度骄瓣,就不在贅述了。
ViewController 的字典里面默認(rèn)還會加上2項:
"controller_class" =
route =
route里面都會保存?zhèn)鬟^來的完整的URL液斜。
如果傳進(jìn)來的路由后面帶訪問字符串呢累贤?那我們再來看看:
[[HHRouter shared] matchController:@"/user/1/?a=b&c=d"]
那么解析出所有的參數(shù)字典會是下面的樣子:
{
a = b;
c = d;
"controller_class" = UserViewController;
route = "/user/1/?a=b&c=d";
userId = 1;
}
同理叠穆,如果是一個block閉包的情況呢少漆?
還是先添加一條block閉包的路由規(guī)則:
[[HHRouter shared] map:@"/user/add/"
toBlock:^id(NSDictionary* params) {
}];
這條規(guī)則對應(yīng)的會生成一個路由規(guī)則的字典。
{
story = {
":storyId" = {
"_" = StoryViewController;
};
};
user = {
":userId" = {
"_" = UserViewController;
story = {
"_" = StoryListViewController;
};
};
add = {
"_" = "<__NSMallocBlock__: 0x600000240480>";
};
};
}
注意”_”后面跟著是一個block硼被。
匹配block閉包的方式有兩種示损。
// 1.第一種方式匹配到對應(yīng)的block之后,還需要手動調(diào)用一次閉包嚷硫。
HHRouterBlock block = [[HHRouter shared] matchBlock:@"/user/add/?a=1&b=2"];
block(nil);
// 2.第二種方式匹配block之后自動會調(diào)用改閉包检访。
[[HHRouter shared] callBlock:@"/user/add/?a=1&b=2"];
匹配出來的參數(shù)字典是如下:
{
a = 1;
b = 2;
block = "<__NSMallocBlock__: 0x600000056b90>";
route = "/user/add/?a=1&b=2";
}
block的字典里面會默認(rèn)加上下面這2項:
block =
route =
route里面都會保存?zhèn)鬟^來的完整的URL。
生成的參數(shù)字典最終會被綁定到ViewController的Associated Object關(guān)聯(lián)對象上仔掸。
- (void)setParams:(NSDictionary *)paramsDictionary
{
objc_setAssociatedObject(self, &kAssociatedParamsObjectKey, paramsDictionary, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (NSDictionary *)params
{
return objc_getAssociatedObject(self, &kAssociatedParamsObjectKey);
}
這個綁定的過程是在match匹配完成的時候進(jìn)行的脆贵。
- (UIViewController *)matchController:(NSString *)route
{
NSDictionary *params = [self paramsInRoute:route];
Class controllerClass = params[@"controller_class"];
UIViewController *viewController = [[controllerClass alloc] init];
if ([viewController respondsToSelector:@selector(setParams:)]) {
[viewController performSelector:@selector(setParams:)
withObject:[params copy]];
}
return viewController;
}
最終得到的ViewController也是我們想要的。相應(yīng)的參數(shù)都在它綁定的params屬性的字典里面起暮。
將上述過程圖解出來卖氨,如下:
(4)MGJRouter Star 633
這是蘑菇街的一個路由的方法。
這個庫的由來:
JLRoutes 的問題主要在于查找 URL 的實(shí)現(xiàn)不夠高效负懦,通過遍歷而不是匹配筒捺。還有就是功能偏多。
HHRouter 的 URL 查找是基于匹配纸厉,所以會更高效系吭,MGJRouter 也是采用的這種方法,但它跟 ViewController 綁定地過于緊密颗品,一定程度上降低了靈活性肯尺。
于是就有了 MGJRouter沃缘。
從數(shù)據(jù)結(jié)構(gòu)來看,MGJRouter還是和HHRouter一模一樣的蟆盹。
@interface MGJRouter ()
@property (nonatomic) NSMutableDictionary *routes;
@end
那么我們就來看看它對HHRouter做了哪些優(yōu)化改進(jìn)孩灯。
1.MGJRouter支持openURL時,可以傳一些 userinfo 過去
[MGJRouter openURL:@"mgj://category/travel" withUserInfo:@{@"user_id": @1900} completion:nil];
這個對比HHRouter逾滥,僅僅只是寫法上的一個語法糖峰档,在HHRouter中雖然不支持帶字典的參數(shù),但是在URL后面可以用URL Query Parameter來彌補(bǔ)寨昙。
if (parameters) {
MGJRouterHandler handler = parameters[@"block"];
if (completion) {
parameters[MGJRouterParameterCompletion] = completion;
}
if (userInfo) {
parameters[MGJRouterParameterUserInfo] = userInfo;
}
if (handler) {
[parameters removeObjectForKey:@"block"];
handler(parameters);
}
}
MGJRouter對userInfo的處理是直接把它封裝到Key = MGJRouterParameterUserInfo對應(yīng)的Value里面讥巡。
2.支持中文的URL。
[parameters enumerateKeysAndObjectsUsingBlock:^(id key, NSString *obj, BOOL *stop) {
if ([obj isKindOfClass:[NSString class]]) {
parameters[key] = [obj stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
}
}];
這里就是需要注意一下編碼舔哪。
3.定義一個全局的 URL Pattern 作為 Fallback欢顷。
這一點(diǎn)是模仿的JLRoutes的匹配不到會自動降級到global的思想。
if (parameters) {
MGJRouterHandler handler = parameters[@"block"];
if (handler) {
[parameters removeObjectForKey:@"block"];
handler(parameters);
}
}
parameters字典里面會先存儲下一個路由規(guī)則捉蚤,存在block閉包中抬驴,在匹配的時候會取出這個handler,降級匹配到這個閉包中缆巧,進(jìn)行最終的處理布持。
4.當(dāng) OpenURL 結(jié)束時,可以執(zhí)行 Completion Block陕悬。
在MGJRouter里面题暖,作者對原來的HHRouter字典里面存儲的路由規(guī)則的結(jié)構(gòu)進(jìn)行了改造。
NSString *const MGJRouterParameterURL = @"MGJRouterParameterURL";
NSString *const MGJRouterParameterCompletion = @"MGJRouterParameterCompletion";
NSString *const MGJRouterParameterUserInfo = @"MGJRouterParameterUserInfo";
這3個key會分別保存一些信息:
MGJRouterParameterURL保存的傳進(jìn)來的完整的URL信息捉超。
MGJRouterParameterCompletion保存的是completion閉包胧卤。
MGJRouterParameterUserInfo保存的是UserInfo字典。
舉個例子:
[MGJRouter registerURLPattern:@"ele://name/:name" toHandler:^(NSDictionary *routerParameters) {
void (^completion)(NSString *) = routerParameters[MGJRouterParameterCompletion];
if (completion) {
completion(@"完成了");
}
}];
[MGJRouter openURL:@"ele://name/halfrost/?age=20" withUserInfo:@{@"user_id": @1900} completion:^(id result) {
NSLog(@"result = %@",result);
}];
上面的URL會匹配成功拼岳,那么生成的參數(shù)字典結(jié)構(gòu)如下:
{
MGJRouterParameterCompletion = "<__NSGlobalBlock__: 0x107ffe680>";
MGJRouterParameterURL = "ele://name/halfrost/?age=20";
MGJRouterParameterUserInfo = {
"user_id" = 1900;
};
age = 20;
block = "<__NSMallocBlock__: 0x608000252120>";
name = halfrost;
}
5.可以統(tǒng)一管理URL
這個功能非常有用枝誊。
URL 的處理一不小心,就容易散落在項目的各個角落惜纸,不容易管理叶撒。比如注冊時的 pattern 是 mgj://beauty/:id,然后 open 時就是 mgj://beauty/123堪簿,這樣到時候 url 有改動痊乾,處理起來就會很麻煩,不好統(tǒng)一管理椭更。
所以 MGJRouter 提供了一個類方法來處理這個問題哪审。
#define TEMPLATE_URL @"qq://name/:name"
[MGJRouter registerURLPattern:TEMPLATE_URL toHandler:^(NSDictionary *routerParameters) {
NSLog(@"routerParameters[name]:%@", routerParameters[@"name"]); // halfrost
}];
[MGJRouter openURL:[MGJRouter generateURLWithPattern:TEMPLATE_URL parameters:@[@"halfrost"]]];
}
generateURLWithPattern:函數(shù)會對我們定義的宏里面的所有的:進(jìn)行替換,替換成后面的字符串?dāng)?shù)組虑瀑,依次賦值湿滓。
將上述過程圖解出來滴须,如下:
蘑菇街為了區(qū)分開頁面間調(diào)用和組件間調(diào)用,于是想出了一種新的方法叽奥。用Protocol的方法來進(jìn)行組件間的調(diào)用扔水。
每個組件之間都有一個 Entry,這個 Entry朝氓,主要做了三件事:
- 注冊這個組件關(guān)心的 URL
- 注冊這個組件能夠被調(diào)用的方法/屬性
- 在 App 生命周期的不同階段做不同的響應(yīng)
頁面間的openURL調(diào)用就是如下的樣子:
每個組件間都會向MGJRouter注冊魔市,組件間相互調(diào)用或者是其他的App都可以通過openURL:方法打開一個界面或者調(diào)用一個組件。
在組件間的調(diào)用赵哲,蘑菇街采用了Protocol的方式待德。
[ModuleManager registerClass:ClassA forProtocol:ProtocolA] 的結(jié)果就是在 MM 內(nèi)部維護(hù)的 dict 里新加了一個映射關(guān)系。
[ModuleManager classForProtocol:ProtocolA] 的返回結(jié)果就是之前在 MM 內(nèi)部 dict 里 protocol 對應(yīng)的 class枫夺,使用方不需要關(guān)心這個 class 是個什么東東将宪,反正實(shí)現(xiàn)了 ProtocolA 協(xié)議,拿來用就行橡庞。
這里需要有一個公共的地方來容納這些 public protocl较坛,也就是圖中的 PublicProtocl.h。
我猜測扒最,大概實(shí)現(xiàn)可能是下面的樣子:
@interface ModuleProtocolManager : NSObject
+ (void)registServiceProvide:(id)provide forProtocol:(Protocol*)protocol;
+ (id)serviceProvideForProtocol:(Protocol *)protocol;
@end
然后這個是一個單例丑勤,在里面注冊各個協(xié)議:
@interface ModuleProtocolManager ()
@property (nonatomic, strong) NSMutableDictionary *serviceProvideSource;
@end
@implementation ModuleProtocolManager
+ (ModuleProtocolManager *)sharedInstance
{
static ModuleProtocolManager * instance;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [[self alloc] init];
});
return instance;
}
- (instancetype)init
{
self = [super init];
if (self) {
_serviceProvideSource = [[NSMutableDictionary alloc] init];
}
return self;
}
+ (void)registServiceProvide:(id)provide forProtocol:(Protocol*)protocol
{
if (provide == nil || protocol == nil)
return;
[[self sharedInstance].serviceProvideSource setObject:provide forKey:NSStringFromProtocol(protocol)];
}
+ (id)serviceProvideForProtocol:(Protocol *)protocol
{
return [[self sharedInstance].serviceProvideSource objectForKey:NSStringFromProtocol(protocol)];
}
在ModuleProtocolManager中用一個字典保存每個注冊的protocol。現(xiàn)在再來猜猜ModuleEntry的實(shí)現(xiàn)扼倘。
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
@protocol DetailModuleEntryProtocol <NSObject>
@required;
- (UIViewController *)detailViewControllerWithId:(NSString*)Id Name:(NSString *)name;
@end
然后每個模塊內(nèi)都有一個和暴露到外面的協(xié)議相連接的“接頭”确封。
#import <Foundation/Foundation.h>
@interface DetailModuleEntry : NSObject
@end
在它的實(shí)現(xiàn)中除呵,需要引入3個外部文件再菊,一個是ModuleProtocolManager,一個是DetailModuleEntryProtocol颜曾,最后一個是所在模塊需要跳轉(zhuǎn)或者調(diào)用的組件或者頁面纠拔。
#import "DetailModuleEntry.h"
#import <DetailModuleEntryProtocol/DetailModuleEntryProtocol.h>
#import <ModuleProtocolManager/ModuleProtocolManager.h>
#import "DetailViewController.h"
@interface DetailModuleEntry()<DetailModuleEntryProtocol>
@end
@implementation DetailModuleEntry
+ (void)load
{
[ModuleProtocolManager registServiceProvide:[[self alloc] init] forProtocol:@protocol(DetailModuleEntryProtocol)];
}
- (UIViewController *)detailViewControllerWithId:(NSString*)Id Name:(NSString *)name
{
DetailViewController *detailVC = [[DetailViewController alloc] initWithId:id Name:name];
return detailVC;
}
@end
至此基于Protocol的方案就完成了。如果需要調(diào)用某個組件或者跳轉(zhuǎn)某個頁面泛豪,只要先從ModuleProtocolManager的字典里面根據(jù)對應(yīng)的ModuleEntryProtocol找到對應(yīng)的DetailModuleEntry稠诲,找到了DetailModuleEntry就是找到了組件或者頁面的“入口”了。再把參數(shù)傳進(jìn)去即可诡曙。
- (void)didClickDetailButton:(UIButton *)button
{
id< DetailModuleEntryProtocol > DetailModuleEntry = [ModuleProtocolManager serviceProvideForProtocol:@protocol(DetailModuleEntryProtocol)];
UIViewController *detailVC = [DetailModuleEntry detailViewControllerWithId:@“詳情界面” Name:@“我的購物車”];
[self.navigationController pushViewController:detailVC animated:YES];
}
這樣就可以調(diào)用到組件或者界面了臀叙。
如果組件之間有相同的接口,那么還可以進(jìn)一步的把這些接口都抽離出來价卤。這些抽離出來的接口變成“元接口”劝萤,它們是可以足夠支撐起整個組件一層的。
(5)CTMediator Star 803
再來說說@casatwy的方案慎璧,這方案是基于Mediator的床嫌。
傳統(tǒng)的中間人Mediator的模式是這樣的:
這種模式每個頁面或者組件都會依賴中間者跨释,各個組件之間互相不再依賴,組件間調(diào)用只依賴中間者M(jìn)ediator毫捣,Mediator還是會依賴其他組件线召。那么這是最終方案了么军俊?
看看@casatwy是怎么繼續(xù)優(yōu)化的。
主要思想是利用了Target-Action簡單粗暴的思想缆娃,利用Runtime解決解耦的問題。
- (id)performTarget:(NSString *)targetName action:(NSString *)actionName params:(NSDictionary *)params shouldCacheTarget:(BOOL)shouldCacheTarget
{
NSString *targetClassString = [NSString stringWithFormat:@"Target_%@", targetName];
NSString *actionString = [NSString stringWithFormat:@"Action_%@:", actionName];
Class targetClass;
NSObject *target = self.cachedTarget[targetClassString];
if (target == nil) {
targetClass = NSClassFromString(targetClassString);
target = [[targetClass alloc] init];
}
SEL action = NSSelectorFromString(actionString);
if (target == nil) {
// 這里是處理無響應(yīng)請求的地方之一瑰排,這個demo做得比較簡單龄恋,如果沒有可以響應(yīng)的target,就直接return了凶伙。實(shí)際開發(fā)過程中是可以事先給一個固定的target專門用于在這個時候頂上郭毕,然后處理這種請求的
return nil;
}
if (shouldCacheTarget) {
self.cachedTarget[targetClassString] = target;
}
if ([target respondsToSelector:action]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
return [target performSelector:action withObject:params];
#pragma clang diagnostic pop
} else {
// 有可能target是Swift對象
actionString = [NSString stringWithFormat:@"Action_%@WithParams:", actionName];
action = NSSelectorFromString(actionString);
if ([target respondsToSelector:action]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
return [target performSelector:action withObject:params];
#pragma clang diagnostic pop
} else {
// 這里是處理無響應(yīng)請求的地方,如果無響應(yīng)函荣,則嘗試調(diào)用對應(yīng)target的notFound方法統(tǒng)一處理
SEL action = NSSelectorFromString(@"notFound:");
if ([target respondsToSelector:action]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
return [target performSelector:action withObject:params];
#pragma clang diagnostic pop
} else {
// 這里也是處理無響應(yīng)請求的地方显押,在notFound都沒有的時候,這個demo是直接return了傻挂。實(shí)際開發(fā)過程中乘碑,可以用前面提到的固定的target頂上的。
[self.cachedTarget removeObjectForKey:targetClassString];
return nil;
}
}
}
}
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:症副。第三個參數(shù)是一個字典店雅,這個字典里面可以傳很多參數(shù),只要Key-Value寫好就可以了贞铣。處理錯誤的方式也統(tǒng)一在一個地方了闹啦,target沒有,或者是target無法響應(yīng)相應(yīng)的方法辕坝,都可以在Mediator這里進(jìn)行統(tǒng)一出錯處理窍奋。
但是在實(shí)際開發(fā)過程中,不管是界面調(diào)用,組件間調(diào)用费变,在Mediator中需要定義很多方法摧扇。于是作者又想出了建議我們用Category的方法,對Mediator的所有方法進(jìn)行拆分挚歧,這樣就就可以不會導(dǎo)致Mediator這個類過于龐大了扛稽。
- (UIViewController *)CTMediator_viewControllerForDetail
{
UIViewController *viewController = [self performTarget:kCTMediatorTargetA
action:kCTMediatorActionNativFetchDetailViewController
params:@{@"key":@"value"}
shouldCacheTarget:NO
];
if ([viewController isKindOfClass:[UIViewController class]]) {
// view controller 交付出去之后,可以由外界選擇是push還是present
return viewController;
} else {
// 這里處理異常場景滑负,具體如何處理取決于產(chǎn)品
return [[UIViewController alloc] init];
}
}
- (void)CTMediator_presentImage:(UIImage *)image
{
if (image) {
[self performTarget:kCTMediatorTargetA
action:kCTMediatorActionNativePresentImage
params:@{@"image":image}
shouldCacheTarget:NO];
} else {
// 這里處理image為nil的場景在张,如何處理取決于產(chǎn)品
[self performTarget:kCTMediatorTargetA
action:kCTMediatorActionNativeNoImage
params:@{@"image":[UIImage imageNamed:@"noImage"]}
shouldCacheTarget:NO];
}
}
把這些具體的方法一個個的都寫在Category里面就好了,調(diào)用的方式都非常的一致矮慕,都是調(diào)用performTarget: action: params: shouldCacheTarget:方法帮匾。
最終去掉了中間者M(jìn)ediator對組件的依賴,各個組件之間互相不再依賴痴鳄,組件間調(diào)用只依賴中間者M(jìn)ediator瘟斜,Mediator不依賴其他任何組件。
(6)一些并沒有開源的方案
除了上面開源的路由方案痪寻,還有一些并沒有開源的設(shè)計精美的方案螺句。這里可以和大家一起分析交流一下。
這個方案是Uber 騎手App的一個方案橡类。
Uber在發(fā)現(xiàn)MVC的一些弊端之后:比如動輒上萬行巨胖無比的VC蛇尚,無法進(jìn)行單元測試等缺點(diǎn)后,于是考慮把架構(gòu)換成VIPER顾画。但是VIPER也有一定的弊端取劫。因為它的iOS特定的結(jié)構(gòu),意味著iOS必須為Android做出一些妥協(xié)的權(quán)衡研侣。以視圖為驅(qū)動的應(yīng)用程序邏輯谱邪,代表應(yīng)用程序狀態(tài)由視圖驅(qū)動,整個應(yīng)用程序都鎖定在視圖樹上义辕。由操作應(yīng)用程序狀態(tài)所關(guān)聯(lián)的業(yè)務(wù)邏輯的改變虾标,就必須經(jīng)過Presenter寓盗。因此會暴露業(yè)務(wù)邏輯灌砖。最終導(dǎo)致了視圖樹和業(yè)務(wù)樹進(jìn)行了緊緊的耦合。這樣想實(shí)現(xiàn)一個緊緊只有業(yè)務(wù)邏輯的Node節(jié)點(diǎn)或者緊緊只有視圖邏輯的Node節(jié)點(diǎn)就非常的困難了傀蚌。
通過改進(jìn)VIPER架構(gòu)基显,吸收其優(yōu)秀的特點(diǎn),改進(jìn)其缺點(diǎn)善炫,就形成了Uber 騎手App的全新架構(gòu)——Riblets(肋骨)撩幽。
在這個新的架構(gòu)中,即使是相似的邏輯也會被區(qū)分成很小很小,相互獨(dú)立窜醉,可以單獨(dú)進(jìn)行測試的組件宪萄。每個組件都有非常明確的用途。使用這些一小塊一小塊的Riblets(肋骨)榨惰,最終把整個App拼接成一顆Riblets(肋骨)樹拜英。
通過抽象,一個Riblets(肋骨)被定義成一下6個更小的組件琅催,這些組件各自有各自的職責(zé)居凶。通過一個Riblets(肋骨)進(jìn)一步的抽象業(yè)務(wù)邏輯和視圖邏輯。
一個Riblets(肋骨)被設(shè)計成這樣藤抡,那和之前的VIPER和MVC有什么區(qū)別呢侠碧?最大的區(qū)別在路由上面。
Riblets(肋骨)內(nèi)的Router不再是視圖邏輯驅(qū)動的缠黍,現(xiàn)在變成了業(yè)務(wù)邏輯驅(qū)動弄兜。這一重大改變就導(dǎo)致了整個App不再是由表現(xiàn)形式驅(qū)動,現(xiàn)在變成了由數(shù)據(jù)流驅(qū)動瓷式。
每一個Riblet都是由一個路由Router挨队,一個關(guān)聯(lián)器Interactor,一個構(gòu)造器Builder和它們相關(guān)的組件構(gòu)成的蒿往。所以它的命名(Router - Interactor - Builder盛垦,Rib)也由此得來。當(dāng)然還可以有可選的展示器Presenter和視圖View瓤漏。路由Router和關(guān)聯(lián)器Interactor處理業(yè)務(wù)邏輯腾夯,展示器Presenter和視圖View處理視圖邏輯。
重點(diǎn)分析一下Riblet里面路由的職責(zé)蔬充。
1.路由的職責(zé)
在整個App的結(jié)構(gòu)樹中蝶俱,路由的職責(zé)是用來關(guān)聯(lián)和取消關(guān)聯(lián)其他子Riblet的。至于決定是由關(guān)聯(lián)器Interactor傳遞過來的饥漫。在狀態(tài)轉(zhuǎn)換過程中榨呆,關(guān)聯(lián)和取消關(guān)聯(lián)子Riblet的時候,路由也會影響到關(guān)聯(lián)器Interactor的生命周期庸队。路由只包含2個業(yè)務(wù)邏輯:
1.提供關(guān)聯(lián)和取消關(guān)聯(lián)其他路由的方法积蜻。
2.在多個孩子之間決定最終狀態(tài)的狀態(tài)轉(zhuǎn)換邏輯。
2.拼裝
每一個Riblets只有一對Router路由和Interactor關(guān)聯(lián)器彻消。但是它們可以有多對視圖竿拆。Riblets只處理業(yè)務(wù)邏輯,不處理視圖相關(guān)的部分宾尚。Riblets可以擁有單一的視圖(一個Presenter展示器和一個View視圖)丙笋,也可以擁有多個視圖(一個Presenter展示器和多個View視圖谢澈,或者多個Presenter展示器和多個View視圖),甚至也可以能沒有視圖(沒有Presenter展示器也沒有View視圖)御板。這種設(shè)計可以有助于業(yè)務(wù)邏輯樹的構(gòu)建锥忿,也可以和視圖樹做到很好的分離。
舉個例子怠肋,騎手的Riblet是一個沒有視圖的Riblet缎谷,它用來檢查當(dāng)前用戶是否有一個激活的路線。如果騎手確定了路線灶似,那么這個Riblet就會關(guān)聯(lián)到路線的Riblet上面列林。路線的Riblet會在地圖上顯示出路線圖。如果沒有確定路線酪惭,騎手的Riblet就會被關(guān)聯(lián)到請求的Riblet上希痴。請求的Riblet會在屏幕上顯示等待被呼叫。像騎手的Riblet這樣沒有任何視圖邏輯的Riblet春感,它分開了業(yè)務(wù)邏輯砌创,在驅(qū)動App和支撐模塊化架構(gòu)起了重大作用。
3.Riblets是如何工作的
Riblet中的數(shù)據(jù)流
在這個新的架構(gòu)中鲫懒,數(shù)據(jù)流動是單向的嫩实。Data數(shù)據(jù)流從service服務(wù)流到Model Stream生成Model流。Model流再從Model Stream流動到Interactor關(guān)聯(lián)器窥岩。Interactor關(guān)聯(lián)器甲献,scheduler調(diào)度器,遠(yuǎn)程推送都可以想Service觸發(fā)變化來引起Model Stream的改動颂翼。Model Stream生成不可改動的models晃洒。這個強(qiáng)制的要求就導(dǎo)致關(guān)聯(lián)器只能通過Service層改變App的狀態(tài)。
舉兩個例子:
數(shù)據(jù)從后臺到視圖View上
一個狀態(tài)的改變朦乏,引起服務(wù)器后臺觸發(fā)推送到App球及。數(shù)據(jù)就被Push到App,然后生成不可變的數(shù)據(jù)流呻疹。關(guān)聯(lián)器收到model之后吃引,把它傳遞給展示器Presenter。展示器Presenter把model轉(zhuǎn)換成view model傳遞給視圖View刽锤。數(shù)據(jù)從視圖到服務(wù)器后臺
當(dāng)用戶點(diǎn)擊了一個按鈕镊尺,比如登錄按鈕。視圖View就會觸發(fā)UI事件傳遞給展示器Presenter姑蓝。展示器Presenter調(diào)用關(guān)聯(lián)器Interactor登錄方法鹅心。關(guān)聯(lián)器Interactor又會調(diào)用Service call的實(shí)際登錄方法。請求網(wǎng)絡(luò)之后會把數(shù)據(jù)pull到后臺服務(wù)器纺荧。
Riblet間的數(shù)據(jù)流
當(dāng)一個關(guān)聯(lián)器Interactor在處理業(yè)務(wù)邏輯的工程中,需要調(diào)用其他Riblet的事件的時候,關(guān)聯(lián)器Interactor需要和子關(guān)聯(lián)器Interactor進(jìn)行關(guān)聯(lián)宙暇。見上圖5個步驟输枯。
如果調(diào)用方法是從子調(diào)用父類,父類的Interactor的接口通常被定義成監(jiān)聽者listener占贫。如果調(diào)用方法是從父類調(diào)用到子類桃熄,那么子類的接口通常是一個delegate,實(shí)現(xiàn)父類的一些Protocol型奥。
在Riblet的方案中瞳收,路由Router僅僅只是用來維護(hù)一個樹型關(guān)系,而關(guān)聯(lián)器Interactor才擔(dān)當(dāng)?shù)氖怯脕頉Q定觸發(fā)組件間的邏輯跳轉(zhuǎn)的角色厢汹。
五. 各個方案優(yōu)缺點(diǎn)
經(jīng)過上面的分析螟深,可以發(fā)現(xiàn),路由的設(shè)計思路是從URLRoute ->Protocol-class ->Target-Action一步步的深入的過程烫葬。這也是逐漸深入本質(zhì)的過程界弧。
1. URLRoute注冊方案的優(yōu)缺點(diǎn)
首先URLRoute也許是借鑒前端Router和系統(tǒng)App內(nèi)跳轉(zhuǎn)的方式想出來的方法。它通過URL來請求資源搭综。不管是H5垢箕,RN,Weex兑巾,iOS界面或者組件請求資源的方式就都統(tǒng)一了条获。URL里面也會帶上參數(shù),這樣調(diào)用什么界面或者組件都可以蒋歌。所以這種方式是最容易月匣,也是最先可以想到的。
URLRoute的優(yōu)點(diǎn)很多奋姿,最大的優(yōu)點(diǎn)就是服務(wù)器可以動態(tài)的控制頁面跳轉(zhuǎn)锄开,可以統(tǒng)一處理頁面出問題之后的錯誤處理,可以統(tǒng)一三端称诗,iOS萍悴,Android,H5 / RN / Weex 的請求方式寓免。
但是這種方式也需要看不同公司的需求癣诱。如果公司里面已經(jīng)完成了服務(wù)器端動態(tài)下發(fā)的腳手架工具,前端也完成了Native端如果出現(xiàn)錯誤了袜香,可以隨時替換相同業(yè)務(wù)界面的需求撕予,那么這個時候可能選擇URLRoute的幾率會更大。
但是如果公司里面H5沒有做相關(guān)出現(xiàn)問題后能替換的界面蜈首,H5開發(fā)人員覺得這是給他們增添負(fù)擔(dān)实抡。如果公司也沒有完成服務(wù)器動態(tài)下發(fā)路由規(guī)則的那套系統(tǒng)欠母,那么公司可能就不會采用URLRoute的方式。因為URLRoute帶來的少量動態(tài)性吆寨,公司是可以用JSPatch來做到赏淌。線上出現(xiàn)bug了,可以立即用JSPatch修掉啄清,而不采用URLRoute去做六水。
所以選擇URLRoute這種方案,也要看公司的發(fā)展情況和人員分配辣卒,技術(shù)選型方面掷贾。
URLRoute方案也是存在一些缺點(diǎn)的,首先URL的map規(guī)則是需要注冊的荣茫,它們會在load方法里面寫想帅。寫在load方法里面是會影響App啟動速度的。
其次是大量的硬編碼计露。URL鏈接里面關(guān)于組件和頁面的名字都是硬編碼博脑,參數(shù)也都是硬編碼。而且每個URL參數(shù)字段都必須要一個文檔進(jìn)行維護(hù)票罐,這個對于業(yè)務(wù)開發(fā)人員也是一個負(fù)擔(dān)叉趣。而且URL短連接散落在整個App四處,維護(hù)起來實(shí)在有點(diǎn)麻煩该押,雖然蘑菇街想到了用宏統(tǒng)一管理這些鏈接疗杉,但是還是解決不了硬編碼的問題。
真正一個好的路由是在無形當(dāng)中服務(wù)整個App的蚕礼,是一個無感知的過程烟具,從這一點(diǎn)來說,略有點(diǎn)缺失奠蹬。
最后一個缺點(diǎn)是朝聋,對于傳遞NSObject的參數(shù),URL是不夠友好的囤躁,它最多是傳遞一個字典冀痕。
2. Protocol-Class注冊方案的優(yōu)缺點(diǎn)
Protocol-Class方案的優(yōu)點(diǎn),這個方案沒有硬編碼狸演。
Protocol-Class方案也是存在一些缺點(diǎn)的言蛇,每個Protocol都要向ModuleManager進(jìn)行注冊。
這種方案ModuleEntry是同時需要依賴ModuleManager和組件里面的頁面或者組件兩者的宵距。當(dāng)然ModuleEntry也是會依賴ModuleEntryProtocol的腊尚,但是這個依賴是可以去掉的,比如用Runtime的方法NSProtocolFromString满哪,加上硬編碼是可以去掉對Protocol的依賴的婿斥。但是考慮到硬編碼的方式對出現(xiàn)bug劝篷,后期維護(hù)都是不友好的,所以對Protocol的依賴還是不要去除受扳。
最后一個缺點(diǎn)是組件方法的調(diào)用是分散在各處的携龟,沒有統(tǒng)一的入口兔跌,也就沒法做組件不存在時或者出現(xiàn)錯誤時的統(tǒng)一處理勘高。
3. Target-Action方案的優(yōu)缺點(diǎn)
Target-Action方案的優(yōu)點(diǎn),充分的利用Runtime的特性坟桅,無需注冊這一步华望。Target-Action方案只有存在組件依賴Mediator這一層依賴關(guān)系。在Mediator中維護(hù)針對Mediator的Category仅乓,每個category對應(yīng)一個Target赖舟,Categroy中的方法對應(yīng)Action場景。Target-Action方案也統(tǒng)一了所有組件間調(diào)用入口夸楣。
Target-Action方案也能有一定的安全保證宾抓,它對url中進(jìn)行Native前綴進(jìn)行驗證。
Target-Action方案的缺點(diǎn)豫喧,Target_Action在Category中將常規(guī)參數(shù)打包成字典石洗,在Target處再把字典拆包成常規(guī)參數(shù),這就造成了一部分的硬編碼紧显。
4. 組件如何拆分讲衫?
這個問題其實(shí)應(yīng)該是在打算實(shí)施組件化之前就應(yīng)該考慮的問題。為何還要放在這里說呢孵班?因為組件的拆分每個公司都有屬于自己的拆分方案涉兽,按照業(yè)務(wù)線拆?按照最細(xì)小的業(yè)務(wù)功能模塊拆篙程?還是按照一個完成的功能進(jìn)行拆分枷畏?這個就牽扯到了拆分粗細(xì)度的問題了。組件拆分的粗細(xì)度就會直接關(guān)系到未來路由需要解耦的程度虱饿。
假設(shè)拥诡,把登錄的所有流程封裝成一個組件,由于登錄里面會涉及到多個頁面郭厌,那么這些頁面都會打包在一個組件里面袋倔。那么其他模塊需要調(diào)用登錄狀態(tài)的時候,這時候就需要用到登錄組件暴露在外面可以獲取登錄狀態(tài)的接口折柠。那么這個時候就可以考慮把這些接口寫到Protocol里面宾娜,暴露給外面使用∩仁郏或者用Target-Action的方法前塔。這種把一個功能全部都劃分成登錄組件的話嚣艇,劃分粒度就稍微粗一點(diǎn)。
如果僅僅把登錄狀態(tài)的細(xì)小功能劃分成一個元組件华弓,那么外面想獲取登錄狀態(tài)就直接調(diào)用這個組件就好食零。這種劃分的粒度就非常細(xì)了。這樣就會導(dǎo)致組件個數(shù)巨多寂屏。
所以在進(jìn)行拆分組件的時候贰谣,也許當(dāng)時業(yè)務(wù)并不復(fù)雜的時候,拆分成組件迁霎,相互耦合也不大吱抚。但是隨著業(yè)務(wù)不管變化,之前劃分的組件間耦合性越來越大考廉,于是就會考慮繼續(xù)把之前的組件再進(jìn)行拆分秘豹。也許有些業(yè)務(wù)砍掉了,之前一些小的組件也許還會被組合到一起昌粤〖热疲總之,在業(yè)務(wù)沒有完全固定下來之前涮坐,組件的劃分可能一直進(jìn)行時凄贩。
六. 最好的方案
關(guān)于架構(gòu),我覺得拋開業(yè)務(wù)談架構(gòu)是沒有意義的膊升。因為架構(gòu)是為了業(yè)務(wù)服務(wù)的怎炊,空談架構(gòu)只是一種理想的狀態(tài)。所以沒有最好的方案廓译,只有最適合的方案评肆。
最適合自己公司業(yè)務(wù)的方案才是最好的方案。分而治之非区,針對不同業(yè)務(wù)選擇不同的方案才是最優(yōu)的解決方案瓜挽。如果非要籠統(tǒng)的采用一種方案,不同業(yè)務(wù)之間需要同一種方案征绸,需要妥協(xié)犧牲的東西太多就不好了久橙。
希望本文能拋磚引玉,幫助大家選擇出最適合自家業(yè)務(wù)的路由方案管怠。當(dāng)然肯定會有更加優(yōu)秀的方案淆衷,希望大家能多多指點(diǎn)我。
References:
在現(xiàn)有工程中實(shí)施基于CTMediator的組件化方案
iOS應(yīng)用架構(gòu)談 組件化方案
蘑菇街 App 的組件化之路
蘑菇街 App 的組件化之路·續(xù)
ENGINEERING THE ARCHITECTURE BEHIND UBER’S NEW RIDER APP
GitHub Repo:Halfrost-Field
Follow: halfrost · GitHub
Source: https://halfrost.com/ios_router/