iOS 组件化 —— 路由设计思路分析
原文
前言
隨著用戶的需求越來(lái)越多,對(duì)App的用戶體驗(yàn)也變的要求越來(lái)越高。為了更好的應(yīng)對(duì)各種需求,開(kāi)發(fā)人員從軟件工程的角度,將App架構(gòu)由原來(lái)簡(jiǎn)單的MVC變成MVVM,VIPER等復(fù)雜架構(gòu)。更換適合業(yè)務(wù)的架構(gòu),是為了后期能更好的維護(hù)項(xiàng)目。
但是用戶依舊不滿意,繼續(xù)對(duì)開(kāi)發(fā)人員提出了更多更高的要求,不僅需要高質(zhì)量的用戶體驗(yàn),還要求快速迭代,最好一天出一個(gè)新功能,而且用戶還要求不更新就能體驗(yàn)到新功能。為了滿足用戶需求,于是開(kāi)發(fā)人員就用H5,ReactNative,Weex等技術(shù)對(duì)已有的項(xiàng)目進(jìn)行改造。項(xiàng)目架構(gòu)也變得更加的復(fù)雜,縱向的會(huì)進(jìn)行分層,網(wǎng)絡(luò)層,UI層,數(shù)據(jù)持久層。每一層橫向的也會(huì)根據(jù)業(yè)務(wù)進(jìn)行組件化。盡管這樣做了以后會(huì)讓開(kāi)發(fā)更加有效率,更加好維護(hù),但是如何解耦各層,解耦各個(gè)界面和各個(gè)組件,降低各個(gè)組件之間的耦合度,如何能讓整個(gè)系統(tǒng)不管多么復(fù)雜的情況下都能保持“高內(nèi)聚,低耦合”的特點(diǎn)?這一系列的問(wèn)題都擺在開(kāi)發(fā)人員面前,亟待解決。今天就來(lái)談?wù)劷鉀Q這個(gè)問(wèn)題的一些思路。
目錄
1.引子
2.App路由能解決哪些問(wèn)題
3.App之間跳轉(zhuǎn)實(shí)現(xiàn)
4.App內(nèi)組件間路由設(shè)計(jì)
5.各個(gè)方案優(yōu)缺點(diǎn)
6.最好的方案
一、引子
大前端發(fā)展這么多年了,相信也一定會(huì)遇到相似的問(wèn)題。近兩年SPA發(fā)展極其迅猛,React 和 Vue一直處于風(fēng)口浪尖,那我們就看看他們是如何處理好這一問(wèn)題的。
在SPA單頁(yè)面應(yīng)用,路由起到了很關(guān)鍵的作用。路由的作用主要是保證視圖和 URL 的同步。在前端的眼里看來(lái),視圖是被看成是資源的一種表現(xiàn)。當(dāng)用戶在頁(yè)面中進(jìn)行操作時(shí),應(yīng)用會(huì)在若干個(gè)交互狀態(tài)中切換,路由則可以記錄下某些重要的狀態(tài),比如用戶查看一個(gè)網(wǎng)站,用戶是否登錄、在訪問(wèn)網(wǎng)站的哪一個(gè)頁(yè)面。而這些變化同樣會(huì)被記錄在瀏覽器的歷史中,用戶可以通過(guò)瀏覽器的前進(jìn)、后退按鈕切換狀態(tài)。總的來(lái)說(shuō),用戶可以通過(guò)手動(dòng)輸入或者與頁(yè)面進(jìn)行交互來(lái)改變 URL,然后通過(guò)同步或者異步的方式向服務(wù)端發(fā)送請(qǐng)求獲取資源,成功后重新繪制 UI,原理如下圖所示:
react-router通過(guò)傳入的location到最終渲染新的UI,流程如下:
location的來(lái)源有2種,一種是瀏覽器的回退和前進(jìn),另外一種是直接點(diǎn)了一個(gè)鏈接。新的 location 對(duì)象后,路由內(nèi)部的 matchRoutes 方法會(huì)匹配出 Route 組件樹(shù)中與當(dāng)前 location 對(duì)象匹配的一個(gè)子集,并且得到了 nextState,在this.setState(nextState) 時(shí)就可以實(shí)現(xiàn)重新渲染 Router 組件。
大前端的做法大概是這樣的,我們可以把這些思想借鑒到iOS這邊來(lái)。上圖中的Back / Forward 在iOS這邊很多情況下都可以被UINavgation所管理。所以iOS的Router主要處理綠色的那一塊。
二、App路由能解決哪些問(wèn)題
既然前端能在SPA上解決URL和UI的同步問(wèn)題,那這種思想可以在App上解決哪些問(wèn)題呢?
思考如下的問(wèn)題,平時(shí)我們開(kāi)發(fā)中是如何優(yōu)雅的解決的:
1.3D-Touch功能或者點(diǎn)擊推送消息,要求外部跳轉(zhuǎn)到App內(nèi)部一個(gè)很深層次的一個(gè)界面。
比如微信的3D-Touch可以直接跳轉(zhuǎn)到“我的二維碼”。“我的二維碼”界面在我的里面的第三級(jí)界面。或者再極端一點(diǎn),產(chǎn)品需求給了更加變態(tài)的需求,要求跳轉(zhuǎn)到App內(nèi)部第十層的界面,怎么處理?
2.自家的一系列App之間如何相互跳轉(zhuǎn)?
如果自己App有幾個(gè),相互之間還想相互跳轉(zhuǎn),怎么處理?
3.如何解除App組件之間和App頁(yè)面之間的耦合性?
隨著項(xiàng)目越來(lái)越復(fù)雜,各個(gè)組件,各個(gè)頁(yè)面之間的跳轉(zhuǎn)邏輯關(guān)聯(lián)性越來(lái)越多,如何能優(yōu)雅的解除各個(gè)組件和頁(yè)面之間的耦合性?
4.如何能統(tǒng)一iOS和Android兩端的頁(yè)面跳轉(zhuǎn)邏輯?甚至如何能統(tǒng)一三端的請(qǐng)求資源的方式?
項(xiàng)目里面某些模塊會(huì)混合ReactNative,Weex,H5界面,這些界面還會(huì)調(diào)用Native的界面,以及Native的組件。那么,如何能統(tǒng)一Web端和Native端請(qǐng)求資源的方式?
5.如果使用了動(dòng)態(tài)下發(fā)配置文件來(lái)配置App的跳轉(zhuǎn)邏輯,那么如果做到iOS和Android兩邊只要共用一套配置文件?
6.如果App出現(xiàn)bug了,如何不用JSPatch,就能做到簡(jiǎn)單的熱修復(fù)功能?
比如App上線突然遇到了緊急bug,能否把頁(yè)面動(dòng)態(tài)降級(jí)成H5,ReactNative,Weex?或者是直接換成一個(gè)本地的錯(cuò)誤界面?
7.如何在每個(gè)組件間調(diào)用和頁(yè)面跳轉(zhuǎn)時(shí)都進(jìn)行埋點(diǎn)統(tǒng)計(jì)?每個(gè)跳轉(zhuǎn)的地方都手寫(xiě)代碼埋點(diǎn)?利用Runtime AOP ?
8.如何在每個(gè)組件間調(diào)用的過(guò)程中,加入調(diào)用的邏輯檢查,令牌機(jī)制,配合灰度進(jìn)行風(fēng)控邏輯?
9.如何在App任何界面都可以調(diào)用同一個(gè)界面或者同一個(gè)組件?只能在AppDelegate里面注冊(cè)單例來(lái)實(shí)現(xiàn)?
比如App出現(xiàn)問(wèn)題了,用戶可能在任何界面,如何隨時(shí)隨地的讓用戶強(qiáng)制登出?或者強(qiáng)制都跳轉(zhuǎn)到同一個(gè)本地的error界面?或者跳轉(zhuǎn)到相應(yīng)的H5,ReactNative,Weex界面?如何讓用戶在任何界面,隨時(shí)隨地的彈出一個(gè)View ?
以上這些問(wèn)題其實(shí)都可以通過(guò)在App端設(shè)計(jì)一個(gè)路由來(lái)解決。那么我們?cè)趺丛O(shè)計(jì)一個(gè)路由呢?
三、App之間跳轉(zhuǎn)實(shí)現(xiàn)
在談App內(nèi)部的路由之前,先來(lái)談?wù)勗趇OS系統(tǒng)間,不同App之間是怎么實(shí)現(xiàn)跳轉(zhuǎn)的。
1. URL Scheme方式
iOS系統(tǒng)是默認(rèn)支持URL Scheme的,具體見(jiàn)官方文檔。
比如說(shuō),在iPhone的Safari瀏覽器上面輸入如下的命令,會(huì)自動(dòng)打開(kāi)一些App:
在iOS 9 之前只要在App的info.plist里面添加URL types - URL Schemes,如下圖:
這里就添加了一個(gè)com.ios.Qhomer的Scheme。這樣就可以在iPhone的Safari瀏覽器上面輸入:
就可以直接打開(kāi)這個(gè)App了。
關(guān)于其他一些常見(jiàn)的App,可以從iTunes里面下載到它的ipa文件,解壓,顯示包內(nèi)容里面可以找到info.plist文件,打開(kāi)它,在里面就可以相應(yīng)的URL Scheme。
當(dāng)然了,某些App對(duì)于調(diào)用URL Scheme比較敏感,它們不希望其他的App隨意的就調(diào)用自己。
如果待調(diào)用的App已經(jīng)運(yùn)行了,那么它的生命周期如下:
如果待調(diào)用的App在后臺(tái),那么它的生命周期如下:
明白了上面的生命周期之后,我們就可以通過(guò)調(diào)用application:openURL:sourceApplication:annotation:這個(gè)方法,來(lái)阻止一些App的隨意調(diào)用。
如上圖,餓了么App允許通過(guò)URL Scheme調(diào)用,那么我們可以在Safari里面調(diào)用到餓了么App。手機(jī)QQ不允許調(diào)用,我們?cè)赟afari里面也就沒(méi)法跳轉(zhuǎn)過(guò)去。
關(guān)于App間的跳轉(zhuǎn)問(wèn)題,感興趣的可以查看官方文檔Inter-App Communication。
App也是可以直接跳轉(zhuǎn)到系統(tǒng)設(shè)置的。比如有些需求要求檢測(cè)用戶有沒(méi)有開(kāi)啟某些系統(tǒng)權(quán)限,如果沒(méi)有開(kāi)啟就彈框提示,點(diǎn)擊彈框的按鈕直接跳轉(zhuǎn)到系統(tǒng)設(shè)置里面對(duì)應(yīng)的設(shè)置界面。
-
iOS 10 支持通過(guò) URL Scheme 跳轉(zhuǎn)到系統(tǒng)設(shè)置
-
iOS10跳轉(zhuǎn)系統(tǒng)設(shè)置的正確姿勢(shì)
-
關(guān)于 iOS 系統(tǒng)功能的 URL 匯總列表
2. Universal Links方式
雖然在微信內(nèi)部開(kāi)網(wǎng)頁(yè)會(huì)禁止所有的Scheme,但是iOS 9.0新增加了一項(xiàng)功能是Universal Links,使用這個(gè)功能可以使我們的App通過(guò)HTTP鏈接來(lái)啟動(dòng)App。
1.如果安裝過(guò)App,不管在微信里面http鏈接還是在Safari瀏覽器,還是其他第三方瀏覽器,都可以打開(kāi)App。
2.如果沒(méi)有安裝過(guò)App,就會(huì)打開(kāi)網(wǎng)頁(yè)。
具體設(shè)置需要3步:
1.App需要開(kāi)啟Associated Domains服務(wù),并設(shè)置Domains,注意必須要applinks:開(kāi)頭。
2.域名必須要支持HTTPS。
3.上傳內(nèi)容是Json格式的文件,文件名為apple-app-site-association到自己域名的根目錄下,或者.well-known目錄下。iOS自動(dòng)會(huì)去讀取這個(gè)文件。具體的文件內(nèi)容請(qǐng)查看官方文檔。
如果App支持了Universal Links方式,那么可以在其他App里面直接跳轉(zhuǎn)到我們自己的App里面。如下圖,點(diǎn)擊鏈接,由于該鏈接會(huì)Matcher到我們?cè)O(shè)置的鏈接,所以菜單里面會(huì)顯示用我們的App打開(kāi)。
在瀏覽器里面也是一樣的效果,如果是支持了Universal Links方式,訪問(wèn)相應(yīng)的URL,會(huì)有不同的效果。如下圖:
以上就是iOS系統(tǒng)中App間跳轉(zhuǎn)的二種方式。
從iOS 系統(tǒng)里面支持的URL Scheme方式,我們可以看出,對(duì)于一個(gè)資源的訪問(wèn),蘋果也是用URI的方式來(lái)訪問(wèn)的。
統(tǒng)一資源標(biāo)識(shí)符(英語(yǔ):Uniform Resource Identifier,或URI)是一個(gè)用于標(biāo)識(shí)某一互聯(lián)網(wǎng)資源名稱的字符串。 該種標(biāo)識(shí)允許用戶對(duì)網(wǎng)絡(luò)中(一般指萬(wàn)維網(wǎng))的資源通過(guò)特定的協(xié)議進(jìn)行交互操作。URI的最常見(jiàn)的形式是統(tǒng)一資源定位符(URL)。
舉個(gè)例子:
這是一段URI,每一段都代表了對(duì)應(yīng)的含義。對(duì)方接收到了這樣一串字符串,按照規(guī)則解析出來(lái),就能獲取到所有的有用信息。
這個(gè)能給我們?cè)O(shè)計(jì)App組件間的路由帶來(lái)一些思路么?如果我們想要定義一個(gè)三端(iOS,Android,H5)的統(tǒng)一訪問(wèn)資源的方式,能用URI的這種方式實(shí)現(xiàn)么?
四、App內(nèi)組件間路由設(shè)計(jì)
上一章節(jié)中我們介紹了iOS系統(tǒng)中,系統(tǒng)是如何幫我們處理App間跳轉(zhuǎn)邏輯的。這一章節(jié)我們著重討論一下,App內(nèi)部,各個(gè)組件之間的路由應(yīng)該怎么設(shè)計(jì)。關(guān)于App內(nèi)部的路由設(shè)計(jì),主要需要解決2個(gè)問(wèn)題:
1.各個(gè)頁(yè)面和組件之間的跳轉(zhuǎn)問(wèn)題。
2.各個(gè)組件之間相互調(diào)用。
先來(lái)分析一下這兩個(gè)問(wèn)題。
1. 關(guān)于頁(yè)面跳轉(zhuǎn)
在iOS開(kāi)發(fā)的過(guò)程中,經(jīng)常會(huì)遇到以下的場(chǎng)景,點(diǎn)擊按鈕跳轉(zhuǎn)Push到另外一個(gè)界面,或者點(diǎn)擊一個(gè)cell Present一個(gè)新的ViewController。在MVC模式中,一般都是新建一個(gè)VC,然后Push / Present到下一個(gè)VC。但是在MVVM中,會(huì)有一些不合適的情況。
眾所周知,MVVM把MVC拆成了上圖演示的樣子,原來(lái)View對(duì)應(yīng)的與數(shù)據(jù)相關(guān)的代碼都移到ViewModel中,相應(yīng)的C也變瘦了,演變成了M-VM-C-V的結(jié)構(gòu)。這里的C里面的代碼可以只剩下頁(yè)面跳轉(zhuǎn)相關(guān)的邏輯。如果用代碼表示就是下面這樣子:
假設(shè)一個(gè)按鈕的執(zhí)行邏輯都封裝成了command。
上述的代碼本身沒(méi)啥問(wèn)題,但是可能會(huì)弱化MVVM框架的一個(gè)重要作用。
MVVM框架的目的除去解耦以外,還有2個(gè)很重要的目的:
-
代碼高復(fù)用率
-
方便進(jìn)行單元測(cè)試
如果需要測(cè)試一個(gè)業(yè)務(wù)是否正確,我們只要對(duì)ViewModel進(jìn)行單元測(cè)試即可。前提是假定我們使用ReactiveCocoa進(jìn)行UI綁定的過(guò)程是準(zhǔn)確無(wú)誤的。目前綁定是正確的。所以我們只需要單元測(cè)試到ViewModel即可完成業(yè)務(wù)邏輯的測(cè)試。
頁(yè)面跳轉(zhuǎn)也屬于業(yè)務(wù)邏輯,所以應(yīng)該放在ViewModel中一起單元測(cè)試,保證業(yè)務(wù)邏輯測(cè)試的覆蓋率。
把頁(yè)面跳轉(zhuǎn)放到ViewModel中,有2種做法,第一種就是用路由來(lái)實(shí)現(xiàn),第二種由于和路由沒(méi)有關(guān)系,所以這里就不多闡述,有興趣的可以看lpd-mvvm-kit這個(gè)庫(kù)關(guān)于頁(yè)面跳轉(zhuǎn)的具體實(shí)現(xiàn)。
頁(yè)面跳轉(zhuǎn)相互的耦合性也就體現(xiàn)出來(lái)了:
1.由于pushViewController或者presentViewController,后面都需要帶一個(gè)待操作的ViewController,那么就必須要引入該類,import頭文件也就引入了耦合性。
2.由于跳轉(zhuǎn)這里寫(xiě)死了跳轉(zhuǎn)操作,如果線上一旦出現(xiàn)了bug,這里是不受我們控制的。
3.推送消息或者是3D-Touch需求,要求直接跳轉(zhuǎn)到內(nèi)部第10級(jí)界面,那么就需要寫(xiě)一個(gè)入口跳轉(zhuǎn)到指定界面。
2. 關(guān)于組件間調(diào)用
關(guān)于組件間的調(diào)用,也需要解耦。隨著業(yè)務(wù)越來(lái)越復(fù)雜,我們封裝的組件越來(lái)越多,要是封裝的粒度拿捏不準(zhǔn),就會(huì)出現(xiàn)大量組件之間耦合度高的問(wèn)題。組件的粒度可以隨著業(yè)務(wù)的調(diào)整,不斷的調(diào)整組件職責(zé)的劃分。但是組件之間的調(diào)用依舊不可避免,相互調(diào)用對(duì)方組件暴露的接口。如何減少各個(gè)組件之間的耦合度,是一個(gè)設(shè)計(jì)優(yōu)秀的路由的職責(zé)所在。
3. 如何設(shè)計(jì)一個(gè)路由
如何設(shè)計(jì)一個(gè)能完美解決上述2個(gè)問(wèn)題的路由,讓我們先來(lái)看看GitHub上優(yōu)秀開(kāi)源庫(kù)的設(shè)計(jì)思路。以下是我從Github上面找的一些路由方案,按照Star從高到低排列。依次來(lái)分析一下它們各自的設(shè)計(jì)思路。
(1)JLRoutes?Star 3189
JLRoutes在整個(gè)Github上面Star最多,那就來(lái)從它來(lái)分析分析它的具體設(shè)計(jì)思路。
首先JLRoutes是受URL Scheme思路的影響。它把所有對(duì)資源的請(qǐng)求看成是一個(gè)URI。
首先來(lái)熟悉一下NSURLComponent的各個(gè)字段:
Note
The URLs employed by the NSURL
class are described in?RFC 1808,?RFC 1738, and?RFC 2732.
JLRoutes會(huì)傳入每個(gè)字符串,都按照上面的樣子進(jìn)行切分處理,分別根據(jù)RFC的標(biāo)準(zhǔn)定義,取到各個(gè)NSURLComponent。
JLRoutes全局會(huì)保存一個(gè)Map,這個(gè)Map會(huì)以scheme為Key,JLRoutes為Value。所以在routeControllerMap里面每個(gè)scheme都是唯一的。
至于為何有這么多條路由,筆者認(rèn)為,如果路由按照業(yè)務(wù)線進(jìn)行劃分的話,每個(gè)業(yè)務(wù)線可能會(huì)有不相同的邏輯,即使每個(gè)業(yè)務(wù)里面的組件名字可能相同,但是由于業(yè)務(wù)線不同,會(huì)有不同的路由規(guī)則。
舉個(gè)例子:如果滴滴按照每個(gè)城市的打車業(yè)務(wù)進(jìn)行組件化拆分,那么每個(gè)城市就對(duì)應(yīng)著這里的每個(gè)scheme。每個(gè)城市的打車業(yè)務(wù)都有叫車,付款……等業(yè)務(wù),但是由于每個(gè)城市的地方法規(guī)不相同,所以這些組件即使名字相同,但是里面的功能也許千差萬(wàn)別。所以這里劃分出了多個(gè)route,也可以理解為不同的命名空間。
在每個(gè)JLRoutes里面都保存了一個(gè)數(shù)組,這個(gè)數(shù)組里面保存了每個(gè)路由規(guī)則JLRRouteDefinition里面會(huì)保存外部傳進(jìn)來(lái)的block閉包,pattern,和拆分之后的pattern。
在每個(gè)JLRoutes的數(shù)組里面,會(huì)按照路由的優(yōu)先級(jí)進(jìn)行排列,優(yōu)先級(jí)高的排列在前面。
由于這個(gè)數(shù)組里面的路由是一個(gè)單調(diào)隊(duì)列,所以查找優(yōu)先級(jí)的時(shí)候只用從高往低遍歷即可。
具體查找路由的過(guò)程如下:
首先根據(jù)外部傳進(jìn)來(lái)的URL初始化一個(gè)JLRRouteRequest,然后用這個(gè)JLRRouteRequest在當(dāng)前的路由數(shù)組里面依次request,每個(gè)規(guī)則都會(huì)生成一個(gè)response,但是只有符合條件的response才會(huì)match,最后取出匹配的JLRRouteResponse拿出其字典parameters里面對(duì)應(yīng)的參數(shù)就可以了。查找和匹配過(guò)程中重要的代碼如下:
舉個(gè)例子:
我們先注冊(cè)一個(gè)Router,規(guī)則如下:
我們傳入一個(gè)URL,讓Router進(jìn)行處理。
匹配成功之后,我們會(huì)得到下面這樣一個(gè)字典:
把上述過(guò)程圖解出來(lái),見(jiàn)下圖:
JLRoutes還可以支持Optional的路由規(guī)則,假如定義一條路由規(guī)則:
JLRoutes 會(huì)幫我們默認(rèn)注冊(cè)如下4條路由規(guī)則:
(2)routable-ios?Star 1415
Routable路由是用在in-app native端的 URL router, 它可以用在iOS上也可以用在Android上。
UPRouter里面保存了2個(gè)字典。routes字典里面存儲(chǔ)的Key是路由規(guī)則,Value存儲(chǔ)的是UPRouterOptions。cachedRoutes里面存儲(chǔ)的Key是最終的URL,帶傳參的,Value存儲(chǔ)的是RouterParams。RouterParams里面會(huì)包含在routes匹配的到的UPRouterOptions,還有額外的打開(kāi)參數(shù)openParams和一些額外參數(shù)extraParams。
這一段代碼里面重點(diǎn)在干一件事情,遍歷routes字典,然后找到參數(shù)匹配的字符串,封裝成RouterParams返回。
上面這段函數(shù),第一個(gè)參數(shù)是外部傳進(jìn)來(lái)URL帶有各個(gè)入?yún)⒌姆指顢?shù)組。第二個(gè)參數(shù)是路由規(guī)則分割開(kāi)的數(shù)組。routerComponent由于規(guī)定:號(hào)后面才是參數(shù),所以routerComponent的第1個(gè)位置就是對(duì)應(yīng)的參數(shù)名。params字典里面以參數(shù)名為Key,參數(shù)為Value。
最后通過(guò)RouterParams的初始化方法,把路由規(guī)則對(duì)應(yīng)的UPRouterOptions,上一步封裝好的參數(shù)字典givenParams,還有
routerParamsForUrl: extraParams: 方法的第二個(gè)入?yún)?#xff0c;這3個(gè)參數(shù)作為初始化參數(shù),生成了一個(gè)RouterParams。
最后一步self.cachedRoutes的字典里面Key為帶參數(shù)的URL,Value是RouterParams。
最后將匹配封裝出來(lái)的RouterParams轉(zhuǎn)換成對(duì)應(yīng)的Controller。
如果Controller是一個(gè)類,那么就調(diào)用allocWithRouterParams:方法去初始化。如果Controller已經(jīng)是一個(gè)實(shí)例了,那么就調(diào)用initWithRouterParams:方法去初始化。
將Routable的大致流程圖解如下:
(3)HHRouter?Star 1277
這是布丁動(dòng)畫(huà)的一個(gè)Router,靈感來(lái)自于?ABRouter?和?Routable iOS。
先來(lái)看看HHRouter的Api。它提供的方法非常清晰。
ViewController提供了2個(gè)方法。map是用來(lái)設(shè)置路由規(guī)則,matchController是用來(lái)匹配路由規(guī)則的,匹配爭(zhēng)取之后返回對(duì)應(yīng)的UIViewController。
block閉包提供了三個(gè)方法,map也是設(shè)置路由規(guī)則,matchBlock:是用來(lái)匹配路由,找到指定的block,但是不會(huì)調(diào)用該block。callBlock:是找到指定的block,找到以后就立即調(diào)用。
matchBlock:和callBlock:的區(qū)別就在于前者不會(huì)自動(dòng)調(diào)用閉包。所以matchBlock:方法找到對(duì)應(yīng)的block之后,如果想調(diào)用,需要手動(dòng)調(diào)用一次。
除去上面這些方法,HHRouter還為我們提供了一個(gè)特殊的方法。
這個(gè)方法就是用來(lái)找到執(zhí)行路由規(guī)則對(duì)應(yīng)的RouteType,RouteType總共就3種:
再來(lái)看看HHRouter是如何管理路由規(guī)則的。整個(gè)HHRouter就是由一個(gè)NSMutableDictionary *routes控制的。
別看只有這一個(gè)看似“簡(jiǎn)單”的字典數(shù)據(jù)結(jié)構(gòu),但是HHRouter路由設(shè)計(jì)的還是很精妙的。
上面兩個(gè)方法分別是block閉包和ViewController設(shè)置路由規(guī)則調(diào)用的方法實(shí)體。不管是ViewController還是block閉包,設(shè)置規(guī)則的時(shí)候都會(huì)調(diào)用subRoutesToRoute:方法。
上面這段函數(shù)就是來(lái)構(gòu)造路由匹配規(guī)則的字典。
舉個(gè)例子:
設(shè)置3條規(guī)則以后,按照上面構(gòu)造路由匹配規(guī)則的字典的方法,該路由規(guī)則字典就會(huì)變成這個(gè)樣子:
路由規(guī)則字典生成之后,等到匹配的時(shí)候就會(huì)遍歷這個(gè)字典。
假設(shè)這時(shí)候有一條路由過(guò)來(lái):
HHRouter對(duì)這條路由的處理方式是先匹配前面的scheme,如果連scheme都不正確的話,會(huì)直接導(dǎo)致后面匹配失敗。
然后再進(jìn)行路由匹配,最后生成的參數(shù)字典如下:
具體的路由參數(shù)匹配的函數(shù)在
這個(gè)方法里面實(shí)現(xiàn)的。這個(gè)方法就是按照路由匹配規(guī)則,把傳進(jìn)來(lái)的URL的參數(shù)都一一解析出來(lái),帶?號(hào)的也都會(huì)解析成字典。這個(gè)方法沒(méi)什么難度,就不在贅述了。
ViewController 的字典里面默認(rèn)還會(huì)加上2項(xiàng):
route里面都會(huì)保存?zhèn)鬟^(guò)來(lái)的完整的URL。
如果傳進(jìn)來(lái)的路由后面帶訪問(wèn)字符串呢?那我們?cè)賮?lái)看看:
那么解析出所有的參數(shù)字典會(huì)是下面的樣子:
同理,如果是一個(gè)block閉包的情況呢?
還是先添加一條block閉包的路由規(guī)則:
這條規(guī)則對(duì)應(yīng)的會(huì)生成一個(gè)路由規(guī)則的字典。
注意”_”后面跟著是一個(gè)block。
匹配block閉包的方式有兩種。
匹配出來(lái)的參數(shù)字典是如下:
block的字典里面會(huì)默認(rèn)加上下面這2項(xiàng):
route里面都會(huì)保存?zhèn)鬟^(guò)來(lái)的完整的URL。
生成的參數(shù)字典最終會(huì)被綁定到ViewController的Associated Object關(guān)聯(lián)對(duì)象上。
這個(gè)綁定的過(guò)程是在match匹配完成的時(shí)候進(jìn)行的。
最終得到的ViewController也是我們想要的。相應(yīng)的參數(shù)都在它綁定的params屬性的字典里面。
將上述過(guò)程圖解出來(lái),如下:
(4)MGJRouter?Star 633
這是蘑菇街的一個(gè)路由的方法。
這個(gè)庫(kù)的由來(lái):
JLRoutes 的問(wèn)題主要在于查找 URL 的實(shí)現(xiàn)不夠高效,通過(guò)遍歷而不是匹配。還有就是功能偏多。
HHRouter 的 URL 查找是基于匹配,所以會(huì)更高效,MGJRouter 也是采用的這種方法,但它跟 ViewController 綁定地過(guò)于緊密,一定程度上降低了靈活性。
于是就有了 MGJRouter。
從數(shù)據(jù)結(jié)構(gòu)來(lái)看,MGJRouter還是和HHRouter一模一樣的。
那么我們就來(lái)看看它對(duì)HHRouter做了哪些優(yōu)化改進(jìn)。
1.MGJRouter支持openURL時(shí),可以傳一些 userinfo 過(guò)去
這個(gè)對(duì)比HHRouter,僅僅只是寫(xiě)法上的一個(gè)語(yǔ)法糖,在HHRouter中雖然不支持帶字典的參數(shù),但是在URL后面可以用URL Query Parameter來(lái)彌補(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ì)原來(lái)的HHRouter字典里面存儲(chǔ)的路由規(guī)則的結(jié)構(gòu)進(jìn)行了改造。
這3個(gè)key會(huì)分別保存一些信息:
MGJRouterParameterURL保存的傳進(jìn)來(lái)的完整的URL信息。
MGJRouterParameterCompletion保存的是completion閉包。
MGJRouterParameterUserInfo保存的是UserInfo字典。
舉個(gè)例子:
上面的URL會(huì)匹配成功,那么生成的參數(shù)字典結(jié)構(gòu)如下:
5.可以統(tǒng)一管理URL
這個(gè)功能非常有用。
URL 的處理一不小心,就容易散落在項(xiàng)目的各個(gè)角落,不容易管理。比如注冊(cè)時(shí)的 pattern 是 mgj://beauty/:id,然后 open 時(shí)就是 mgj://beauty/123,這樣到時(shí)候 url 有改動(dòng),處理起來(lái)就會(huì)很麻煩,不好統(tǒng)一管理。
所以 MGJRouter 提供了一個(gè)類方法來(lái)處理這個(gè)問(wèn)題。
generateURLWithPattern:函數(shù)會(huì)對(duì)我們定義的宏里面的所有的:進(jìn)行替換,替換成后面的字符串?dāng)?shù)組,依次賦值。
將上述過(guò)程圖解出來(lái),如下:
蘑菇街為了區(qū)分開(kāi)頁(yè)面間調(diào)用和組件間調(diào)用,于是想出了一種新的方法。用Protocol的方法來(lái)進(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都可以通過(guò)openURL:方法打開(kāi)一個(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é)議,拿來(lái)用就行。
這里需要有一個(gè)公共的地方來(lái)容納這些 public protocl,也就是圖中的 PublicProtocl.h。
我猜測(cè),大概實(shí)現(xiàn)可能是下面的樣子:
然后這個(gè)是一個(gè)單例,在里面注冊(cè)各個(gè)協(xié)議:
在ModuleProtocolManager中用一個(gè)字典保存每個(gè)注冊(cè)的protocol。現(xiàn)在再來(lái)猜猜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)一步的把這些接口都抽離出來(lái)。這些抽離出來(lái)的接口變成“元接口”,它們是可以足夠支撐起整個(gè)組件一層的。
(5)CTMediator?Star 803
再來(lái)說(shuō)說(shuō)@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解決解耦的問(wèn)題。
targetName就是調(diào)用接口的Object,actionName就是調(diào)用方法的SEL,params是參數(shù),shouldCacheTarget代表是否需要緩存,如果需要緩存就把target存起來(lái),Key是targetClassString,Value是target。
通過(guò)這種方式進(jìn)行改造的,外面調(diào)用的方法都很統(tǒng)一,都是調(diào)用performTarget: action: params: shouldCacheTarget:。第三個(gè)參數(shù)是一個(gè)字典,這個(gè)字典里面可以傳很多參數(shù),只要Key-Value寫(xiě)好就可以了。處理錯(cuò)誤的方式也統(tǒng)一在一個(gè)地方了,target沒(méi)有,或者是target無(wú)法響應(yīng)相應(yīng)的方法,都可以在Mediator這里進(jìn)行統(tǒng)一出錯(cuò)處理。
但是在實(shí)際開(kāi)發(fā)過(guò)程中,不管是界面調(diào)用,組件間調(diào)用,在Mediator中需要定義很多方法。于是做作者又想出了建議我們用Category的方法,對(duì)Mediator的所有方法進(jìn)行拆分,這樣就就可以不會(huì)導(dǎo)致Mediator這個(gè)類過(guò)于龐大了。
把這些具體的方法一個(gè)個(gè)的都寫(xiě)在Category里面就好了,調(diào)用的方式都非常的一致,都是調(diào)用performTarget: action: params: shouldCacheTarget:方法。
最終去掉了中間者M(jìn)ediator對(duì)組件的依賴,各個(gè)組件之間互相不再依賴,組件間調(diào)用只依賴中間者M(jìn)ediator,Mediator不依賴其他任何組件。
(6)一些并沒(méi)有開(kāi)源的方案
除了上面開(kāi)源的路由方案,還有一些并沒(méi)有開(kāi)源的設(shè)計(jì)精美的方案。這里可以和大家一起分析交流一下。
這個(gè)方案是Uber 騎手App的一個(gè)方案。
Uber在發(fā)現(xiàn)MVC的一些弊端之后:比如動(dòng)輒上萬(wàn)行巨胖無(wú)比的VC,無(wú)法進(jìn)行單元測(cè)試等缺點(diǎn)后,于是考慮把架構(gòu)換成VIPER。但是VIPER也有一定的弊端。因?yàn)樗膇OS特定的結(jié)構(gòu),意味著iOS必須為Android做出一些妥協(xié)的權(quán)衡。以視圖為驅(qū)動(dòng)的應(yīng)用程序邏輯,代表應(yīng)用程序狀態(tài)由視圖驅(qū)動(dòng),整個(gè)應(yīng)用程序都鎖定在視圖樹(shù)上。由操作應(yīng)用程序狀態(tài)所關(guān)聯(lián)的業(yè)務(wù)邏輯的改變,就必須經(jīng)過(guò)Presenter。因此會(huì)暴露業(yè)務(wù)邏輯。最終導(dǎo)致了視圖樹(shù)和業(yè)務(wù)樹(shù)進(jìn)行了緊緊的耦合。這樣想實(shí)現(xiàn)一個(gè)緊緊只有業(yè)務(wù)邏輯的Node節(jié)點(diǎn)或者緊緊只有視圖邏輯的Node節(jié)點(diǎn)就非常的困難了。
通過(guò)改進(jìn)VIPER架構(gòu),吸收其優(yōu)秀的特點(diǎn),改進(jìn)其缺點(diǎn),就形成了Uber 騎手App的全新架構(gòu)——Riblets(肋骨)。
在這個(gè)新的架構(gòu)中,即使是相似的邏輯也會(huì)被區(qū)分成很小很小,相互獨(dú)立,可以單獨(dú)進(jìn)行測(cè)試的組件。每個(gè)組件都有非常明確的用途。使用這些一小塊一小塊的Riblets(肋骨),最終把整個(gè)App拼接成一顆Riblets(肋骨)樹(shù)。
通過(guò)抽象,一個(gè)Riblets(肋骨)被定義成一下6個(gè)更小的組件,這些組件各自有各自的職責(zé)。通過(guò)一個(gè)Riblets(肋骨)進(jìn)一步的抽象業(yè)務(wù)邏輯和視圖邏輯。
一個(gè)Riblets(肋骨)被設(shè)計(jì)成這樣,那和之前的VIPER和MVC有什么區(qū)別呢?最大的區(qū)別在路由上面。
Riblets(肋骨)內(nèi)的Router不再是視圖邏輯驅(qū)動(dòng)的,現(xiàn)在變成了業(yè)務(wù)邏輯驅(qū)動(dòng)。這一重大改變就導(dǎo)致了整個(gè)App不再是由表現(xiàn)形式驅(qū)動(dòng),現(xiàn)在變成了由數(shù)據(jù)流驅(qū)動(dòng)。
每一個(gè)Riblet都是由一個(gè)路由Router,一個(gè)關(guān)聯(lián)器Interactor,一個(gè)構(gòu)造器Builder和它們相關(guān)的組件構(gòu)成的。所以它的命名(Router - Interactor - Builder,Rib)也由此得來(lái)。當(dāng)然還可以有可選的展示器Presenter和視圖View。路由Router和關(guān)聯(lián)器Interactor處理業(yè)務(wù)邏輯,展示器Presenter和視圖View處理視圖邏輯。
重點(diǎn)分析一下Riblet里面路由的職責(zé)。
1.路由的職責(zé)
在整個(gè)App的結(jié)構(gòu)樹(shù)中,路由的職責(zé)是用來(lái)關(guān)聯(lián)和取消關(guān)聯(lián)其他子Riblet的。至于決定是由關(guān)聯(lián)器Interactor傳遞過(guò)來(lái)的。在狀態(tài)轉(zhuǎn)換過(guò)程中,關(guān)聯(lián)和取消關(guān)聯(lián)子Riblet的時(shí)候,路由也會(huì)影響到關(guān)聯(lián)器Interactor的生命周期。路由只包含2個(gè)業(yè)務(wù)邏輯:
-
提供關(guān)聯(lián)和取消關(guān)聯(lián)其他路由的方法。
-
在多個(gè)孩子之間決定最終狀態(tài)的狀態(tài)轉(zhuǎn)換邏輯。
2.拼裝
每一個(gè)Riblets只有一對(duì)Router路由和Interactor關(guān)聯(lián)器。但是它們可以有多對(duì)視圖。Riblets只處理業(yè)務(wù)邏輯,不處理視圖相關(guān)的部分。Riblets可以擁有單一的視圖(一個(gè)Presenter展示器和一個(gè)View視圖),也可以擁有多個(gè)視圖(一個(gè)Presenter展示器和多個(gè)View視圖,或者多個(gè)Presenter展示器和多個(gè)View視圖),甚至也可以能沒(méi)有視圖(沒(méi)有Presenter展示器也沒(méi)有View視圖)。這種設(shè)計(jì)可以有助于業(yè)務(wù)邏輯樹(shù)的構(gòu)建,也可以和視圖樹(shù)做到很好的分離。
舉個(gè)例子,騎手的Riblet是一個(gè)沒(méi)有視圖的Riblet,它用來(lái)檢查當(dāng)前用戶是否有一個(gè)激活的路線。如果騎手確定了路線,那么這個(gè)Riblet就會(huì)關(guān)聯(lián)到路線的Riblet上面。路線的Riblet會(huì)在地圖上顯示出路線圖。如果沒(méi)有確定路線,騎手的Riblet就會(huì)被關(guān)聯(lián)到請(qǐng)求的Riblet上。請(qǐng)求的Riblet會(huì)在屏幕上顯示等待被呼叫。像騎手的Riblet這樣沒(méi)有任何視圖邏輯的Riblet,它分開(kāi)了業(yè)務(wù)邏輯,在驅(qū)動(dòng)App和支撐模塊化架構(gòu)起了重大作用。
3.Riblets是如何工作的
Riblet中的數(shù)據(jù)流
在這個(gè)新的架構(gòu)中,數(shù)據(jù)流動(dòng)是單向的。Data數(shù)據(jù)流從service服務(wù)流到Model Stream生成Model流。Model流再?gòu)腗odel Stream流動(dòng)到Interactor關(guān)聯(lián)器。Interactor關(guān)聯(lián)器,scheduler調(diào)度器,遠(yuǎn)程推送都可以想Service觸發(fā)變化來(lái)引起Model Stream的改動(dòng)。Model Stream生成不可改動(dòng)的models。這個(gè)強(qiáng)制的要求就導(dǎo)致關(guān)聯(lián)器只能通過(guò)Service層改變App的狀態(tài)。
舉兩個(gè)例子:
1.數(shù)據(jù)從后臺(tái)到視圖View上
一個(gè)狀態(tài)的改變,引起服務(wù)器后臺(tái)觸發(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ù)器后臺(tái)
當(dāng)用戶點(diǎn)擊了一個(gè)按鈕,比如登錄按鈕。視圖View就會(huì)觸發(fā)UI事件傳遞給展示器Presenter。展示器Presenter調(diào)用關(guān)聯(lián)器Interactor登錄方法。關(guān)聯(lián)器Interactor又會(huì)調(diào)用Service call的實(shí)際登錄方法。請(qǐng)求網(wǎng)絡(luò)之后會(huì)把數(shù)據(jù)pull到后臺(tái)服務(wù)器。
Riblet間的數(shù)據(jù)流
當(dāng)一個(gè)關(guān)聯(lián)器Interactor在處理業(yè)務(wù)邏輯的工程中,需要調(diào)用其他Riblet的事件的時(shí)候,關(guān)聯(lián)器Interactor需要和子關(guān)聯(lián)器Interactor進(jìn)行關(guān)聯(lián)。見(jiàn)上圖5個(gè)步驟。
如果調(diào)用方法是從子調(diào)用父類,父類的Interactor的接口通常被定義成監(jiān)聽(tīng)者listener。如果調(diào)用方法是從父類調(diào)用到子類,那么子類的接口通常是一個(gè)delegate,實(shí)現(xiàn)父類的一些Protocol。
在Riblet的方案中,路由Router僅僅只是用來(lái)維護(hù)一個(gè)樹(shù)型關(guān)系,而關(guān)聯(lián)器Interactor才擔(dān)當(dāng)?shù)氖怯脕?lái)決定觸發(fā)組件間的邏輯跳轉(zhuǎn)的角色。
五、各個(gè)方案優(yōu)缺點(diǎn)
經(jīng)過(guò)上面的分析,可以發(fā)現(xiàn),路由的設(shè)計(jì)思路是從URLRoute ->Protocol-class ->Target-Action一步步的深入的過(guò)程。這也是逐漸深入本質(zhì)的過(guò)程。
1. URLRoute注冊(cè)方案的優(yōu)缺點(diǎn)
首先URLRoute也許是借鑒前端Router和系統(tǒng)App內(nèi)跳轉(zhuǎn)的方式想出來(lái)的方法。它通過(guò)URL來(lái)請(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è)面出問(wèn)題之后的錯(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沒(méi)有做相關(guān)出現(xiàn)問(wèn)題后能替換的界面,H5開(kāi)發(fā)人員覺(jué)得這是給他們?cè)鎏碡?fù)擔(dān)。如果公司也沒(méi)有完成服務(wù)器動(dòng)態(tài)下發(fā)路由規(guī)則的那套系統(tǒng),那么公司可能就不會(huì)采用URLRoute的方式。因?yàn)閁RLRoute帶來(lái)的少量動(dòng)態(tài)性,公司是可以用JSPatch來(lái)做到。線上出現(xiàn)bug了,可以立即用JSPatch修掉,而不采用URLRoute去做。
所以選擇URLRoute這種方案,也要看公司的發(fā)展情況和人員分配,技術(shù)選型方面。
URLRoute方案也是存在一些缺點(diǎn)的,首先URL的map規(guī)則是需要注冊(cè)的,它們會(huì)在load方法里面寫(xiě)。寫(xiě)在load方法里面是會(huì)影響App啟動(dòng)速度的。
其次是大量的硬編碼。URL鏈接里面關(guān)于組件和頁(yè)面的名字都是硬編碼,參數(shù)也都是硬編碼。而且每個(gè)URL參數(shù)字段都必須要一個(gè)文檔進(jìn)行維護(hù),這個(gè)對(duì)于業(yè)務(wù)開(kāi)發(fā)人員也是一個(gè)負(fù)擔(dān)。而且URL短連接散落在整個(gè)App四處,維護(hù)起來(lái)實(shí)在有點(diǎn)麻煩,雖然蘑菇街想到了用宏統(tǒng)一管理這些鏈接,但是還是解決不了硬編碼的問(wèn)題。
真正一個(gè)好的路由是在無(wú)形當(dāng)中服務(wù)整個(gè)App的,是一個(gè)無(wú)感知的過(guò)程,從這一點(diǎn)來(lái)說(shuō),略有點(diǎn)缺失。
最后一個(gè)缺點(diǎn)是,對(duì)于傳遞NSObject的參數(shù),URL是不夠友好的,它最多是傳遞一個(gè)字典。
2. Protocol-Class注冊(cè)方案的優(yōu)缺點(diǎn)
Protocol-Class方案的優(yōu)點(diǎn),這個(gè)方案沒(méi)有硬編碼。
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)用是分散在各處的,沒(méi)有統(tǒng)一的入口,也就沒(méi)法做組件不存在時(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è)問(wèn)題其實(shí)應(yīng)該是在打算實(shí)施組件化之前就應(yīng)該考慮的問(wèn)題。為何還要放在這里說(shuō)呢?因?yàn)榻M件的拆分每個(gè)公司都有屬于自己的拆分方案,按照業(yè)務(wù)線拆?按照最細(xì)小的業(yè)務(wù)功能模塊拆?還是按照一個(gè)完成的功能進(jìn)行拆分?這個(gè)就牽扯到了拆分粗細(xì)度的問(wèn)題了。組件拆分的粗細(xì)度就會(huì)直接關(guān)系到未來(lái)路由需要解耦的程度。
假設(shè),把登錄的所有流程封裝成一個(gè)組件,由于登錄里面會(huì)涉及到多個(gè)頁(yè)面,那么這些頁(yè)面都會(huì)打包在一個(gè)組件里面。那么其他模塊需要調(diào)用登錄狀態(tài)的時(shí)候,這時(shí)候就需要用到登錄組件暴露在外面可以獲取登錄狀態(tài)的接口。那么這個(gè)時(shí)候就可以考慮把這些接口寫(xiě)到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ù)不管變化,之前劃分的組件間耦合性越來(lái)越大,于是就會(huì)考慮繼續(xù)把之前的組件再進(jìn)行拆分。也許有些業(yè)務(wù)砍掉了,之前一些小的組件也許還會(huì)被組合到一起。總之,在業(yè)務(wù)沒(méi)有完全固定下來(lái)之前,組件的劃分可能一直進(jìn)行時(shí)。
六、最好的方案
關(guān)于架構(gòu),我覺(jué)得拋開(kāi)業(yè)務(wù)談架構(gòu)是沒(méi)有意義的。因?yàn)榧軜?gòu)是為了業(yè)務(wù)服務(wù)的,空談架構(gòu)只是一種理想的狀態(tài)。所以沒(méi)有最好的方案,只有最適合的方案。
最適合自己公司業(yè)務(wù)的方案才是最好的方案。分而治之,針對(duì)不同業(yè)務(wù)選擇不同的方案才是最優(yōu)的解決方案。如果非要籠統(tǒng)的采用一種方案,不同業(yè)務(wù)之間需要同一種方案,需要妥協(xié)犧牲的東西太多就不好了。
希望本文能拋磚引玉,幫助大家選擇出最適合自家業(yè)務(wù)的路由方案。當(dāng)然肯定會(huì)有更加優(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
-
總結(jié)
以上是生活随笔為你收集整理的iOS 组件化 —— 路由设计思路分析的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: Python 3.5 socket OS
- 下一篇: golang工程打包不发布