iOS路由與組件化

原文鏈接: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個很重要的目的:

  1. 代碼高復(fù)用率
  2. 方便進(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铝侵,靈感來自于 ABRouterRoutable 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朝氓,主要做了三件事:

  1. 注冊這個組件關(guān)心的 URL
  2. 注冊這個組件能夠被調(diào)用的方法/屬性
  3. 在 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)。

舉兩個例子:

  1. 數(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刽锤。

  2. 數(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/

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末渤弛,一起剝皮案震驚了整個濱河市祝拯,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖佳头,帶你破解...
    沈念sama閱讀 206,723評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件鹰贵,死亡現(xiàn)場離奇詭異,居然都是意外死亡康嘉,警方通過查閱死者的電腦和手機(jī)碉输,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,485評論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來亭珍,“玉大人敷钾,你說我怎么就攤上這事】榘觯” “怎么了闰非?”我有些...
    開封第一講書人閱讀 152,998評論 0 344
  • 文/不壞的土叔 我叫張陵膘格,是天一觀的道長峭范。 經(jīng)常有香客問我,道長瘪贱,這世上最難降的妖魔是什么纱控? 我笑而不...
    開封第一講書人閱讀 55,323評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮菜秦,結(jié)果婚禮上甜害,老公的妹妹穿的比我還像新娘。我一直安慰自己球昨,他們只是感情好尔店,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,355評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著主慰,像睡著了一般嚣州。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上共螺,一...
    開封第一講書人閱讀 49,079評論 1 285
  • 那天该肴,我揣著相機(jī)與錄音,去河邊找鬼藐不。 笑死匀哄,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的雏蛮。 我是一名探鬼主播涎嚼,決...
    沈念sama閱讀 38,389評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼挑秉!你這毒婦竟也來了法梯?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,019評論 0 259
  • 序言:老撾萬榮一對情侶失蹤衷模,失蹤者是張志新(化名)和其女友劉穎鹊汛,沒想到半個月后蒲赂,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,519評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡刁憋,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,971評論 2 325
  • 正文 我和宋清朗相戀三年滥嘴,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片至耻。...
    茶點(diǎn)故事閱讀 38,100評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡若皱,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出尘颓,到底是詐尸還是另有隱情走触,我是刑警寧澤,帶...
    沈念sama閱讀 33,738評論 4 324
  • 正文 年R本政府宣布疤苹,位于F島的核電站互广,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏卧土。R本人自食惡果不足惜惫皱,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,293評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望尤莺。 院中可真熱鬧旅敷,春花似錦、人聲如沸颤霎。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,289評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽友酱。三九已至晴音,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間粹污,已是汗流浹背段多。 一陣腳步聲響...
    開封第一講書人閱讀 31,517評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留壮吩,地道東北人进苍。 一個月前我還...
    沈念sama閱讀 45,547評論 2 354
  • 正文 我出身青樓,卻偏偏與公主長得像鸭叙,于是被迫代替她去往敵國和親觉啊。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,834評論 2 345

推薦閱讀更多精彩內(nèi)容