前言
隨著用戶的需求越來(lái)越多情萤,對(duì)App的用戶體驗(yàn)也變的要求越來(lái)越高。為了更好的應(yīng)對(duì)各種需求睁宰,開發(fā)人員從軟件工程的角度,將App架構(gòu)由原來(lái)簡(jiǎn)單的MVC變成MVVM诅愚,VIPER等復(fù)雜架構(gòu)刹前。更換適合業(yè)務(wù)的架構(gòu)祖今,是為了后期能更好的維護(hù)項(xiàng)目耍目。
但是用戶依舊不滿意,繼續(xù)對(duì)開發(fā)人員提出了更多更高的要求毅访,不僅需要高質(zhì)量的用戶體驗(yàn),還要求快速迭代守呜,最好一天出一個(gè)新功能,而且用戶還要求不更新就能體驗(yàn)到新功能侣颂。為了滿足用戶需求,于是開發(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ì)讓開發(fā)更加有效率盏袄,更加好維護(hù),但是如何解耦各層刁愿,解耦各個(gè)界面和各個(gè)組件,降低各個(gè)組件之間的耦合度脑题,如何能讓整個(gè)系統(tǒng)不管多么復(fù)雜的情況下都能保持“高內(nèi)聚,低耦合”的特點(diǎn)已艰?這一系列的問題都擺在開發(fā)人員面前,亟待解決。今天就來(lái)談?wù)劷鉀Q這個(gè)問題的一些思路誊薄。
目錄
1.引子
2.App路由能解決哪些問題
3.App之間跳轉(zhuǎn)實(shí)現(xiàn)
4.App內(nèi)組件間路由設(shè)計(jì)
5.各個(gè)方案優(yōu)缺點(diǎn)
6.最好的方案
一、引子
大前端發(fā)展這么多年了片吊,相信也一定會(huì)遇到相似的問題。近兩年SPA發(fā)展極其迅猛爷贫,React 和 Vue一直處于風(fēng)口浪尖,那我們就看看他們是如何處理好這一問題的腾务。
在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ǎng)站的哪一個(gè)頁(yè)面。而這些變化同樣會(huì)被記錄在瀏覽器的歷史中朵锣,用戶可以通過瀏覽器的前進(jìn)、后退按鈕切換狀態(tài)诬烹。總的來(lái)說(shuō)家破,用戶可以通過手動(dòng)輸入或者與頁(yè)面進(jìn)行交互來(lái)改變 URL,然后通過同步或者異步的方式向服務(wù)端發(fā)送請(qǐng)求獲取資源烹困,成功后重新繪制 UI拟蜻,原理如下圖所示:
react-router通過傳入的location到最終渲染新的UI辜御,流程如下:
location的來(lái)源有2種袱巨,一種是瀏覽器的回退和前進(jìn),另外一種是直接點(diǎn)了一個(gè)鏈接嫉入。新的 location 對(duì)象后,路由內(nèi)部的 matchRoutes 方法會(huì)匹配出 Route 組件樹中與當(dāng)前 location 對(duì)象匹配的一個(gè)子集垫竞,并且得到了 nextState,在this.setState(nextState) 時(shí)就可以實(shí)現(xiàn)重新渲染 Router 組件遣鼓。
大前端的做法大概是這樣的,我們可以把這些思想借鑒到iOS這邊來(lái)曾我。上圖中的Back / Forward 在iOS這邊很多情況下都可以被UINavgation所管理。所以iOS的Router主要處理綠色的那一塊蛉谜。
二客燕、App路由能解決哪些問題
既然前端能在SPA上解決URL和UI的同步問題,那這種思想可以在App上解決哪些問題呢傍妒?
思考如下的問題,平時(shí)我們開發(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)的地方都手寫代碼埋點(diǎn)渡讼?利用Runtime AOP ?
8.如何在每個(gè)組件間調(diào)用的過程中蹬昌,加入調(diào)用的邏輯檢查皂贩,令牌機(jī)制,配合灰度進(jìn)行風(fēng)控邏輯昆汹?
9.如何在App任何界面都可以調(diào)用同一個(gè)界面或者同一個(gè)組件明刷?只能在AppDelegate里面注冊(cè)單例來(lái)實(shí)現(xiàn)?
比如App出現(xiàn)問題了满粗,用戶可能在任何界面辈末,如何隨時(shí)隨地的讓用戶強(qiáng)制登出轰枝?或者強(qiáng)制都跳轉(zhuǎn)到同一個(gè)本地的error界面?或者跳轉(zhuǎn)到相應(yīng)的H5艾杏,ReactNative,Weex界面阳惹?如何讓用戶在任何界面,隨時(shí)隨地的彈出一個(gè)View 兢孝?
以上這些問題其實(shí)都可以通過在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的伴找,具體見官方文檔。
比如說(shuō)塑悼,在iPhone的Safari瀏覽器上面輸入如下的命令巷屿,會(huì)自動(dòng)打開一些App:
在iOS 9 之前只要在App的info.plist里面添加URL types - URL Schemes,如下圖:
這里就添加了一個(gè)com.ios.Qhomer的Scheme。這樣就可以在iPhone的Safari瀏覽器上面輸入:
就可以直接打開這個(gè)App了卷谈。
關(guān)于其他一些常見的App,可以從iTunes里面下載到它的ipa文件甲抖,解壓,顯示包內(nèi)容里面可以找到info.plist文件,打開它响蓉,在里面就可以相應(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)日丹,那么它的生命周期如下:
明白了上面的生命周期之后该编,我們就可以通過調(diào)用application:openURL:sourceApplication:annotation:這個(gè)方法谒拴,來(lái)阻止一些App的隨意調(diào)用。
如上圖歉铝,餓了么App允許通過URL Scheme調(diào)用,那么我們可以在Safari里面調(diào)用到餓了么App。手機(jī)QQ不允許調(diào)用求厕,我們?cè)赟afari里面也就沒法跳轉(zhuǎn)過去。
關(guān)于App間的跳轉(zhuǎn)問題含长,感興趣的可以查看官方文檔Inter-App Communication强岸。
App也是可以直接跳轉(zhuǎn)到系統(tǒng)設(shè)置的。比如有些需求要求檢測(cè)用戶有沒有開啟某些系統(tǒng)權(quán)限,如果沒有開啟就彈框提示,點(diǎn)擊彈框的按鈕直接跳轉(zhuǎn)到系統(tǒng)設(shè)置里面對(duì)應(yīng)的設(shè)置界面。
iOS 10 支持通過 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)部開網(wǎng)頁(yè)會(huì)禁止所有的Scheme瑰煎,但是iOS 9.0新增加了一項(xiàng)功能是Universal Links铺然,使用這個(gè)功能可以使我們的App通過HTTP鏈接來(lái)啟動(dòng)App。
1.如果安裝過App酒甸,不管在微信里面http鏈接還是在Safari瀏覽器魄健,還是其他第三方瀏覽器,都可以打開App插勤。
2.如果沒有安裝過App沽瘦,就會(huì)打開網(wǎng)頁(yè)革骨。
具體設(shè)置需要3步:
1.App需要開啟Associated Domains服務(wù),并設(shè)置Domains其垄,注意必須要applinks:開頭苛蒲。
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打開瓦盛。
在瀏覽器里面也是一樣的效果,如果是支持了Universal Links方式外潜,訪問相應(yīng)的URL原环,會(huì)有不同的效果。如下圖:
以上就是iOS系統(tǒng)中App間跳轉(zhuǎn)的二種方式处窥。
從iOS 系統(tǒng)里面支持的URL Scheme方式嘱吗,我們可以看出,對(duì)于一個(gè)資源的訪問滔驾,蘋果也是用URI的方式來(lái)訪問的谒麦。
統(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))的資源通過特定的協(xié)議進(jìn)行交互操作绕德。URI的最常見的形式是統(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)一訪問資源的方式,能用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è)問題:
1.各個(gè)頁(yè)面和組件之間的跳轉(zhuǎn)問題发钝。
2.各個(gè)組件之間相互調(diào)用。
先來(lái)分析一下這兩個(gè)問題波闹。
1. 關(guān)于頁(yè)面跳轉(zhuǎn)
在iOS開發(fā)的過程中酝豪,經(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。
上述的代碼本身沒啥問題垮衷,但是可能會(huì)弱化MVVM框架的一個(gè)重要作用厅翔。
MVVM框架的目的除去解耦以外,還有2個(gè)很重要的目的:
代碼高復(fù)用率
方便進(jìn)行單元測(cè)試
如果需要測(cè)試一個(gè)業(yè)務(wù)是否正確搀突,我們只要對(duì)ViewModel進(jìn)行單元測(cè)試即可刀闷。前提是假定我們使用ReactiveCocoa進(jìn)行UI綁定的過程是準(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)翻默,第二種由于和路由沒有關(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)這里寫死了跳轉(zhuǎn)操作,如果線上一旦出現(xiàn)了bug仇箱,這里是不受我們控制的县恕。
3.推送消息或者是3D-Touch需求,要求直接跳轉(zhuǎn)到內(nèi)部第10級(jí)界面剂桥,那么就需要寫一個(gè)入口跳轉(zhuǎn)到指定界面忠烛。
2. 關(guān)于組件間調(diào)用
關(guān)于組件間的調(diào)用,也需要解耦权逗。隨著業(yè)務(wù)越來(lái)越復(fù)雜美尸,我們封裝的組件越來(lái)越多,要是封裝的粒度拿捏不準(zhǔn)斟薇,就會(huì)出現(xià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è)問題的路由,讓我們先來(lái)看看GitHub上優(yōu)秀開源庫(kù)的設(shè)計(jì)思路发笔。以下是我從Github上面找的一些路由方案盟萨,按照Star從高到低排列。依次來(lái)分析一下它們各自的設(shè)計(jì)思路了讨。
(1)JLRoutesStar 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 inRFC 1808,RFC 1738, andRFC 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í)候只用從高往低遍歷即可梆暮。
具體查找路由的過程如下:
首先根據(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ù)就可以了窘游。查找和匹配過程中重要的代碼如下:
舉個(gè)例子:
我們先注冊(cè)一個(gè)Router唠椭,規(guī)則如下:
我們傳入一個(gè)URL,讓Router進(jìn)行處理忍饰。
匹配成功之后贪嫂,我們會(huì)得到下面這樣一個(gè)字典:
把上述過程圖解出來(lái),見下圖:
JLRoutes還可以支持Optional的路由規(guī)則艾蓝,假如定義一條路由規(guī)則:
JLRoutes 會(huì)幫我們默認(rèn)注冊(cè)如下4條路由規(guī)則:
(2)routable-iosStar 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贞岭,還有額外的打開參數(shù)openParams和一些額外參數(shù)extraParams。
這一段代碼里面重點(diǎn)在干一件事情搓侄,遍歷routes字典瞄桨,然后找到參數(shù)匹配的字符串,封裝成RouterParams返回讶踪。
上面這段函數(shù)芯侥,第一個(gè)參數(shù)是外部傳進(jìn)來(lái)URL帶有各個(gè)入?yún)⒌姆指顢?shù)組。第二個(gè)參數(shù)是路由規(guī)則分割開的數(shù)組俊柔。routerComponent由于規(guī)定:號(hào)后面才是參數(shù)筹麸,所以routerComponent的第1個(gè)位置就是對(duì)應(yīng)的參數(shù)名。params字典里面以參數(shù)名為Key雏婶,參數(shù)為Value物赶。
最后通過RouterParams的初始化方法,把路由規(guī)則對(duì)應(yīng)的UPRouterOptions留晚,上一步封裝好的參數(shù)字典givenParams酵紫,還有
routerParamsForUrl: extraParams: 方法的第二個(gè)入?yún)ⅲ@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)HHRouterStar 1277
這是布丁動(dòng)畫的一個(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í)候有一條路由過來(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è)方法沒什么難度思杯,就不在贅述了。
ViewController 的字典里面默認(rèn)還會(huì)加上2項(xiàng):
route里面都會(huì)保存?zhèn)鬟^來(lái)的完整的URL。
如果傳進(jìn)來(lái)的路由后面帶訪問字符串呢色乾?那我們?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)鬟^來(lái)的完整的URL澎办。
生成的參數(shù)字典最終會(huì)被綁定到ViewController的Associated Object關(guān)聯(lián)對(duì)象上嘲碱。
這個(gè)綁定的過程是在match匹配完成的時(shí)候進(jìn)行的。
最終得到的ViewController也是我們想要的局蚀。相應(yīng)的參數(shù)都在它綁定的params屬性的字典里面麦锯。
將上述過程圖解出來(lái),如下:
(4)MGJRouterStar 633
這是蘑菇街的一個(gè)路由的方法琅绅。
這個(gè)庫(kù)的由來(lái):
JLRoutes 的問題主要在于查找 URL 的實(shí)現(xiàn)不夠高效扶欣,通過遍歷而不是匹配。還有就是功能偏多千扶。
HHRouter 的 URL 查找是基于匹配料祠,所以會(huì)更高效,MGJRouter 也是采用的這種方法澎羞,但它跟 ViewController 綁定地過于緊密髓绽,一定程度上降低了靈活性。
于是就有了 MGJRouter煤痕。
從數(shù)據(jù)結(jié)構(gòu)來(lái)看梧宫,MGJRouter還是和HHRouter一模一樣的。
那么我們就來(lái)看看它對(duì)HHRouter做了哪些優(yōu)化改進(jìn)摆碉。
1.MGJRouter支持openURL時(shí)塘匣,可以傳一些 userinfo 過去
這個(gè)對(duì)比HHRouter,僅僅只是寫法上的一個(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è)問題。
generateURLWithPattern:函數(shù)會(huì)對(duì)我們定義的宏里面的所有的:進(jìn)行替換正压,替換成后面的字符串?dāng)?shù)組欣福,依次賦值。
將上述過程圖解出來(lái)焦履,如下:
蘑菇街為了區(qū)分開頁(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都可以通過openURL:方法打開一個(gè)界面或者調(diào)用一個(gè)組件厢洞。
在組件間的調(diào)用,蘑菇街采用了Protocol的方式典奉。
[ModuleManager registerClass:ClassA forProtocol:ProtocolA] 的結(jié)果就是在 MM 內(nèi)部維護(hù)的 dict 里新加了一個(gè)映射關(guān)系躺翻。
[ModuleManager classForProtocol:ProtocolA] 的返回結(jié)果就是之前在 MM 內(nèi)部 dict 里 protocol 對(duì)應(yīng)的 class,使用方不需要關(guān)心這個(gè) class 是個(gè)什么東東卫玖,反正實(shí)現(xiàn)了 ProtocolA 協(xié)議公你,拿來(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)CTMediatorStar 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解決解耦的問題唱歧。
targetName就是調(diào)用接口的Object,actionName就是調(diào)用方法的SEL粒竖,params是參數(shù)颅崩,shouldCacheTarget代表是否需要緩存,如果需要緩存就把target存起來(lái)蕊苗,Key是targetClassString沿后,Value是target。
通過這種方式進(jìn)行改造的朽砰,外面調(diào)用的方法都很統(tǒng)一尖滚,都是調(diào)用performTarget: action: params: shouldCacheTarget:喉刘。第三個(gè)參數(shù)是一個(gè)字典,這個(gè)字典里面可以傳很多參數(shù)漆弄,只要Key-Value寫好就可以了睦裳。處理錯(cuò)誤的方式也統(tǒng)一在一個(gè)地方了,target沒有撼唾,或者是target無(wú)法響應(yīng)相應(yīng)的方法廉邑,都可以在Mediator這里進(jìn)行統(tǒng)一出錯(cuò)處理。
但是在實(shí)際開發(fā)過程中倒谷,不管是界面調(diào)用蛛蒙,組件間調(diào)用,在Mediator中需要定義很多方法渤愁。于是做作者又想出了建議我們用Category的方法牵祟,對(duì)Mediator的所有方法進(jìn)行拆分,這樣就就可以不會(huì)導(dǎo)致Mediator這個(gè)類過于龐大了抖格。
把這些具體的方法一個(gè)個(gè)的都寫在Category里面就好了诺苹,調(diào)用的方式都非常的一致汁果,都是調(diào)用performTarget: action: params: shouldCacheTarget:方法谓传。
最終去掉了中間者M(jìn)ediator對(duì)組件的依賴,各個(gè)組件之間互相不再依賴凡蜻,組件間調(diào)用只依賴中間者M(jìn)ediator办桨,Mediator不依賴其他任何組件筹淫。
(6)一些并沒有開源的方案
除了上面開源的路由方案,還有一些并沒有開源的設(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)用程序都鎖定在視圖樹上。由操作應(yīng)用程序狀態(tài)所關(guān)聯(lián)的業(yè)務(wù)邏輯的改變拦英,就必須經(jīng)過Presenter蜒什。因此會(huì)暴露業(yè)務(wù)邏輯。最終導(dǎo)致了視圖樹和業(yè)務(wù)樹進(jìn)行了緊緊的耦合疤估。這樣想實(shí)現(xiàn)一個(gè)緊緊只有業(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è)新的架構(gòu)中,即使是相似的邏輯也會(huì)被區(qū)分成很小很小雕什,相互獨(dú)立缠俺,可以單獨(dú)進(jìn)行測(cè)試的組件。每個(gè)組件都有非常明確的用途监徘。使用這些一小塊一小塊的Riblets(肋骨),最終把整個(gè)App拼接成一顆Riblets(肋骨)樹吧碾。
通過抽象凰盔,一個(gè)Riblets(肋骨)被定義成一下6個(gè)更小的組件,這些組件各自有各自的職責(zé)倦春。通過一個(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)樹中潦刃,路由的職責(zé)是用來(lái)關(guān)聯(lián)和取消關(guān)聯(lián)其他子Riblet的侮措。至于決定是由關(guān)聯(lián)器Interactor傳遞過來(lái)的。在狀態(tài)轉(zhuǎn)換過程中乖杠,關(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視圖)通今,甚至也可以能沒有視圖(沒有Presenter展示器也沒有View視圖)。這種設(shè)計(jì)可以有助于業(yè)務(wù)邏輯樹的構(gòu)建肛根,也可以和視圖樹做到很好的分離辫塌。
舉個(gè)例子,騎手的Riblet是一個(gè)沒有視圖的Riblet派哲,它用來(lái)檢查當(dāng)前用戶是否有一個(gè)激活的路線臼氨。如果騎手確定了路線,那么這個(gè)Riblet就會(huì)關(guān)聯(lián)到路線的Riblet上面芭届。路線的Riblet會(huì)在地圖上顯示出路線圖储矩。如果沒有確定路線,騎手的Riblet就會(huì)被關(guān)聯(lián)到請(qǐng)求的Riblet上褂乍。請(qǐng)求的Riblet會(huì)在屏幕上顯示等待被呼叫持隧。像騎手的Riblet這樣沒有任何視圖邏輯的Riblet,它分開了業(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)器只能通過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)弟灼。見上圖5個(gè)步驟。
如果調(diào)用方法是從子調(diào)用父類冒黑,父類的Interactor的接口通常被定義成監(jiān)聽者listener田绑。如果調(diào)用方法是從父類調(diào)用到子類,那么子類的接口通常是一個(gè)delegate抡爹,實(shí)現(xiàn)父類的一些Protocol掩驱。
在Riblet的方案中,路由Router僅僅只是用來(lái)維護(hù)一個(gè)樹型關(guān)系豁延,而關(guān)聯(lián)器Interactor才擔(dān)當(dāng)?shù)氖怯脕?lái)決定觸發(fā)組件間的邏輯跳轉(zhuǎn)的角色昙篙。
五腊状、各個(gè)方案優(yōu)缺點(diǎn)
經(jīng)過上面的分析诱咏,可以發(fā)現(xiàn),路由的設(shè)計(jì)思路是從URLRoute ->Protocol-class ->Target-Action一步步的深入的過程缴挖。這也是逐漸深入本質(zhì)的過程袋狞。
1. URLRoute注冊(cè)方案的優(yōu)缺點(diǎn)
首先URLRoute也許是借鑒前端Router和系統(tǒng)App內(nèi)跳轉(zhuǎn)的方式想出來(lái)的方法。它通過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è)面出問題之后的錯(cuò)誤處理,可以統(tǒng)一三端杖虾,iOS烂瘫,Android,H5 / RN / Weex 的請(qǐng)求方式奇适。
但是這種方式也需要看不同公司的需求坟比。如果公司里面已經(jīng)完成了服務(wù)器端動(dòng)態(tài)下發(fā)的腳手架工具芦鳍,前端也完成了Native端如果出現(xiàn)錯(cuò)誤了,可以隨時(shí)替換相同業(yè)務(wù)界面的需求温算,那么這個(gè)時(shí)候可能選擇URLRoute的幾率會(huì)更大怜校。
但是如果公司里面H5沒有做相關(guān)出現(xiàn)問題后能替換的界面,H5開發(fā)人員覺得這是給他們?cè)鎏碡?fù)擔(dān)注竿。如果公司也沒有完成服務(wù)器動(dòng)態(tài)下發(fā)路由規(guī)則的那套系統(tǒng)茄茁,那么公司可能就不會(huì)采用URLRoute的方式。因?yàn)閁RLRoute帶來(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方法里面寫勋锤。寫在load方法里面是會(huì)影響App啟動(dòng)速度的。
其次是大量的硬編碼侥祭。URL鏈接里面關(guān)于組件和頁(yè)面的名字都是硬編碼叁执,參數(shù)也都是硬編碼。而且每個(gè)URL參數(shù)字段都必須要一個(gè)文檔進(jìn)行維護(hù)矮冬,這個(gè)對(duì)于業(yè)務(wù)開發(fā)人員也是一個(gè)負(fù)擔(dān)谈宛。而且URL短連接散落在整個(gè)App四處,維護(hù)起來(lái)實(shí)在有點(diǎn)麻煩胎署,雖然蘑菇街想到了用宏統(tǒng)一管理這些鏈接吆录,但是還是解決不了硬編碼的問題。
真正一個(gè)好的路由是在無(wú)形當(dāng)中服務(wù)整個(gè)App的琼牧,是一個(gè)無(wú)感知的過程恢筝,從這一點(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è)方案沒有硬編碼。
Protocol-Class方案也是存在一些缺點(diǎn)的勋拟,每個(gè)Protocol都要向ModuleManager進(jìn)行注冊(cè)勋磕。
這種方案ModuleEntry是同時(shí)需要依賴ModuleManager和組件里面的頁(yè)面或者組件兩者的。當(dāng)然ModuleEntry也是會(huì)依賴ModuleEntryProtocol的敢靡,但是這個(gè)依賴是可以去掉的挂滓,比如用Runtime的方法NSProtocolFromString,加上硬編碼是可以去掉對(duì)Protocol的依賴的啸胧。但是考慮到硬編碼的方式對(duì)出現(xiàn)bug赶站,后期維護(hù)都是不友好的,所以對(duì)Protocol的依賴還是不要去除纺念。
最后一個(gè)缺點(diǎn)是組件方法的調(diào)用是分散在各處的贝椿,沒有統(tǒng)一的入口,也就沒法做組件不存在時(shí)或者出現(xiàn)錯(cuò)誤時(shí)的統(tǒng)一處理陷谱。
3. Target-Action方案的優(yōu)缺點(diǎn)
Target-Action方案的優(yōu)點(diǎn)烙博,充分的利用Runtime的特性,無(wú)需注冊(cè)這一步烟逊。Target-Action方案只有存在組件依賴Mediator這一層依賴關(guān)系渣窜。在Mediator中維護(hù)針對(duì)Mediator的Category,每個(gè)category對(duì)應(yīng)一個(gè)Target宪躯,Categroy中的方法對(duì)應(yīng)Action場(chǎng)景乔宿。Target-Action方案也統(tǒng)一了所有組件間調(diào)用入口。
Target-Action方案也能有一定的安全保證眷唉,它對(duì)url中進(jìn)行Native前綴進(jìn)行驗(yàn)證予颤。
Target-Action方案的缺點(diǎn)囤官,Target_Action在Category中將常規(guī)參數(shù)打包成字典冬阳,在Target處再把字典拆包成常規(guī)參數(shù),這就造成了一部分的硬編碼党饮。
4. 組件如何拆分肝陪?
這個(gè)問題其實(shí)應(yīng)該是在打算實(shí)施組件化之前就應(yīng)該考慮的問題。為何還要放在這里說(shuō)呢刑顺?因?yàn)榻M件的拆分每個(gè)公司都有屬于自己的拆分方案氯窍,按照業(yè)務(wù)線拆?按照最細(xì)小的業(yè)務(wù)功能模塊拆蹲堂?還是按照一個(gè)完成的功能進(jìn)行拆分狼讨?這個(gè)就牽扯到了拆分粗細(xì)度的問題了。組件拆分的粗細(xì)度就會(huì)直接關(guān)系到未來(lái)路由需要解耦的程度柒竞。
假設(shè)政供,把登錄的所有流程封裝成一個(gè)組件,由于登錄里面會(huì)涉及到多個(gè)頁(yè)面,那么這些頁(yè)面都會(huì)打包在一個(gè)組件里面布隔。那么其他模塊需要調(diào)用登錄狀態(tài)的時(shí)候离陶,這時(shí)候就需要用到登錄組件暴露在外面可以獲取登錄狀態(tài)的接口。那么這個(gè)時(shí)候就可以考慮把這些接口寫到Protocol里面衅檀,暴露給外面使用招刨。或者用Target-Action的方法哀军。這種把一個(gè)功能全部都劃分成登錄組件的話沉眶,劃分粒度就稍微粗一點(diǎn)。
如果僅僅把登錄狀態(tài)的細(xì)小功能劃分成一個(gè)元組件杉适,那么外面想獲取登錄狀態(tài)就直接調(diào)用這個(gè)組件就好沦寂。這種劃分的粒度就非常細(xì)了。這樣就會(huì)導(dǎo)致組件個(gè)數(shù)巨多淘衙。
所以在進(jìn)行拆分組件的時(shí)候传藏,也許當(dāng)時(shí)業(yè)務(wù)并不復(fù)雜的時(shí)候,拆分成組件彤守,相互耦合也不大毯侦。但是隨著業(yè)務(wù)不管變化,之前劃分的組件間耦合性越來(lái)越大具垫,于是就會(huì)考慮繼續(xù)把之前的組件再進(jìn)行拆分侈离。也許有些業(yè)務(wù)砍掉了,之前一些小的組件也許還會(huì)被組合到一起筝蚕∝阅耄總之,在業(yè)務(wù)沒有完全固定下來(lái)之前起宽,組件的劃分可能一直進(jìn)行時(shí)洲胖。
六、最好的方案
關(guān)于架構(gòu)坯沪,我覺得拋開業(yè)務(wù)談架構(gòu)是沒有意義的绿映。因?yàn)榧軜?gòu)是為了業(yè)務(wù)服務(wù)的,空談架構(gòu)只是一種理想的狀態(tài)腐晾。所以沒有最好的方案叉弦,只有最適合的方案。
最適合自己公司業(yè)務(wù)的方案才是最好的方案藻糖。分而治之淹冰,針對(duì)不同業(yè)務(wù)選擇不同的方案才是最優(yōu)的解決方案。如果非要籠統(tǒng)的采用一種方案巨柒,不同業(yè)務(wù)之間需要同一種方案樱拴,需要妥協(xié)犧牲的東西太多就不好了凝颇。
希望本文能拋磚引玉,幫助大家選擇出最適合自家業(yè)務(wù)的路由方案疹鳄。當(dāng)然肯定會(huì)有更加優(yōu)秀的方案拧略,希望大家能多多指點(diǎn)我。
References:
在現(xiàn)有工程中實(shí)施基于CTMediator的組件化方案
ENGINEERING THE ARCHITECTURE BEHIND UBER’S NEW RIDER APP
最怕你一生碌碌無(wú)為 還安慰自己平凡可貴