iOS 組件化 —— 路由設計思路分析

原文html

前言前端

隨着用戶的需求愈來愈多,對App的用戶體驗也變的要求愈來愈高。爲了更好的應對各類需求,開發人員從軟件工程的角度,將App架構由原來簡單的MVC變成MVVM,VIPER等複雜架構。更換適合業務的架構,是爲了後期能更好的維護項目。react

可是用戶依舊不滿意,繼續對開發人員提出了更多更高的要求,不只須要高質量的用戶體驗,還要求快速迭代,最好一天出一個新功能,並且用戶還要求不更新就能體驗到新功能。爲了知足用戶需求,因而開發人員就用H5,ReactNative,Weex等技術對已有的項目進行改造。項目架構也變得更加的複雜,縱向的會進行分層,網絡層,UI層,數據持久層。每一層橫向的也會根據業務進行組件化。儘管這樣作了之後會讓開發更加有效率,更加好維護,可是如何解耦各層,解耦各個界面和各個組件,下降各個組件之間的耦合度,如何能讓整個系統無論多麼複雜的狀況下都能保持「高內聚,低耦合」的特色?這一系列的問題都擺在開發人員面前,亟待解決。今天就來談談解決這個問題的一些思路。android

目錄ios

1.引子git

2.App路由能解決哪些問題github

3.App之間跳轉實現api

4.App內組件間路由設計數組

5.各個方案優缺點瀏覽器

6.最好的方案

1、引子

大前端發展這麼多年了,相信也必定會遇到類似的問題。近兩年SPA發展極其迅猛,React 和 Vue一直處於風口浪尖,那咱們就看看他們是如何處理好這一問題的。

1194012-4fa5a120089e0580.png

在SPA單頁面應用,路由起到了很關鍵的做用。路由的做用主要是保證視圖和 URL 的同步。在前端的眼裏看來,視圖是被當作是資源的一種表現。當用戶在頁面中進行操做時,應用會在若干個交互狀態中切換,路由則能夠記錄下某些重要的狀態,好比用戶查看一個網站,用戶是否登陸、在訪問網站的哪個頁面。而這些變化一樣會被記錄在瀏覽器的歷史中,用戶能夠經過瀏覽器的前進、後退按鈕切換狀態。總的來講,用戶能夠經過手動輸入或者與頁面進行交互來改變 URL,而後經過同步或者異步的方式向服務端發送請求獲取資源,成功後從新繪製 UI,原理以下圖所示:

QQ截圖20170301105219.png

react-router經過傳入的location到最終渲染新的UI,流程以下:

QQ截圖20170301105242.png

location的來源有2種,一種是瀏覽器的回退和前進,另一種是直接點了一個連接。新的 location 對象後,路由內部的 matchRoutes 方法會匹配出 Route 組件樹中與當前 location 對象匹配的一個子集,而且獲得了 nextState,在this.setState(nextState) 時就能夠實現從新渲染 Router 組件。

大前端的作法大概是這樣的,咱們能夠把這些思想借鑑到iOS這邊來。上圖中的Back / Forward 在iOS這邊不少狀況下均可以被UINavgation所管理。因此iOS的Router主要處理綠色的那一塊。

2、App路由能解決哪些問題

QQ截圖20170301105304.png

既然前端能在SPA上解決URL和UI的同步問題,那這種思想能夠在App上解決哪些問題呢?

思考以下的問題,平時咱們開發中是如何優雅的解決的:

1.3D-Touch功能或者點擊推送消息,要求外部跳轉到App內部一個很深層次的一個界面。

好比微信的3D-Touch能夠直接跳轉到「個人二維碼」。「個人二維碼」界面在個人裏面的第三級界面。或者再極端一點,產品需求給了更加變態的需求,要求跳轉到App內部第十層的界面,怎麼處理?

2.自家的一系列App之間如何相互跳轉?

若是本身App有幾個,相互之間還想相互跳轉,怎麼處理?

3.如何解除App組件之間和App頁面之間的耦合性?

隨着項目愈來愈複雜,各個組件,各個頁面之間的跳轉邏輯關聯性愈來愈多,如何能優雅的解除各個組件和頁面之間的耦合性?

4.如何能統一iOS和Android兩端的頁面跳轉邏輯?甚至如何能統一三端的請求資源的方式?

項目裏面某些模塊會混合ReactNative,Weex,H5界面,這些界面還會調用Native的界面,以及Native的組件。那麼,如何能統一Web端和Native端請求資源的方式?

5.若是使用了動態下發配置文件來配置App的跳轉邏輯,那麼若是作到iOS和Android兩邊只要共用一套配置文件?

6.若是App出現bug了,如何不用JSPatch,就能作到簡單的熱修復功能?

好比App上線忽然遇到了緊急bug,可否把頁面動態降級成H5,ReactNative,Weex?或者是直接換成一個本地的錯誤界面?

7.如何在每一個組件間調用和頁面跳轉時都進行埋點統計?每一個跳轉的地方都手寫代碼埋點?利用Runtime AOP ?

8.如何在每一個組件間調用的過程當中,加入調用的邏輯檢查,令牌機制,配合灰度進行風控邏輯?

9.如何在App任何界面均可以調用同一個界面或者同一個組件?只能在AppDelegate裏面註冊單例來實現?

好比App出現問題了,用戶可能在任何界面,如何隨時隨地的讓用戶強制登出?或者強制都跳轉到同一個本地的error界面?或者跳轉到相應的H5,ReactNative,Weex界面?如何讓用戶在任何界面,隨時隨地的彈出一個View ?

以上這些問題其實均可以經過在App端設計一個路由來解決。那麼咱們怎麼設計一個路由呢?

3、App之間跳轉實現

在談App內部的路由以前,先來談談在iOS系統間,不一樣App之間是怎麼實現跳轉的。

1. URL Scheme方式

iOS系統是默認支持URL Scheme的,具體見官方文檔

好比說,在iPhone的Safari瀏覽器上面輸入以下的命令,會自動打開一些App:

QQ截圖20170301135111.png

在iOS 9 以前只要在App的info.plist裏面添加URL types - URL Schemes,以下圖:

QQ截圖20170301105834.png

這裏就添加了一個com.ios.Qhomer的Scheme。這樣就能夠在iPhone的Safari瀏覽器上面輸入:

11.png

就能夠直接打開這個App了。

關於其餘一些常見的App,能夠從iTunes裏面下載到它的ipa文件,解壓,顯示包內容裏面能夠找到info.plist文件,打開它,在裏面就能夠相應的URL Scheme。

12.png

1194012-7bf9d12f40e43505.png

固然了,某些App對於調用URL Scheme比較敏感,它們不但願其餘的App隨意的就調用本身。

13.png

若是待調用的App已經運行了,那麼它的生命週期以下:

1194012-a36c3d174d449288.png

若是待調用的App在後臺,那麼它的生命週期以下:

1194012-389be7fe4279db76.png

明白了上面的生命週期以後,咱們就能夠經過調用application:openURL:sourceApplication:annotation:這個方法,來阻止一些App的隨意調用。

QQ截圖20170301110135.png

如上圖,餓了麼App容許經過URL Scheme調用,那麼咱們能夠在Safari裏面調用到餓了麼App。手機QQ不容許調用,咱們在Safari裏面也就無法跳轉過去。

關於App間的跳轉問題,感興趣的能夠查看官方文檔Inter-App Communication

App也是能夠直接跳轉到系統設置的。好比有些需求要求檢測用戶有沒有開啓某些系統權限,若是沒有開啓就彈框提示,點擊彈框的按鈕直接跳轉到系統設置裏面對應的設置界面。

2. Universal Links方式

雖然在微信內部開網頁會禁止全部的Scheme,可是iOS 9.0新增長了一項功能是Universal Links,使用這個功能可使咱們的App經過HTTP連接來啓動App。
1.若是安裝過App,無論在微信裏面http連接仍是在Safari瀏覽器,仍是其餘第三方瀏覽器,均可以打開App。
2.若是沒有安裝過App,就會打開網頁。

具體設置須要3步:

1.App須要開啓Associated Domains服務,並設置Domains,注意必需要applinks:開頭。

QQ截圖20170301110248.png

2.域名必需要支持HTTPS。

3.上傳內容是Json格式的文件,文件名爲apple-app-site-association到本身域名的根目錄下,或者.well-known目錄下。iOS自動會去讀取這個文件。具體的文件內容請查看官方文檔

1194012-2d1b91f5fcb619cd.png

若是App支持了Universal Links方式,那麼能夠在其餘App裏面直接跳轉到咱們本身的App裏面。以下圖,點擊連接,因爲該連接會Matcher到咱們設置的連接,因此菜單裏面會顯示用咱們的App打開。

1194012-9e8a7004389c7a53.png

在瀏覽器裏面也是同樣的效果,若是是支持了Universal Links方式,訪問相應的URL,會有不一樣的效果。以下圖:

1194012-69233d229be05d24.png

以上就是iOS系統中App間跳轉的二種方式。

從iOS 系統裏面支持的URL Scheme方式,咱們能夠看出,對於一個資源的訪問,蘋果也是用URI的方式來訪問的。

統一資源標識符(英語:Uniform Resource Identifier,或URI)是一個用於標識某一互聯網資源名稱的字符串。 該種標識容許用戶對網絡中(通常指萬維網)的資源經過特定的協議進行交互操做。URI的最多見的形式是統一資源定位符(URL)。

舉個例子:

QQ截圖20170301110523.png

這是一段URI,每一段都表明了對應的含義。對方接收到了這樣一串字符串,按照規則解析出來,就能獲取到全部的有用信息。

這個能給咱們設計App組件間的路由帶來一些思路麼?若是咱們想要定義一個三端(iOS,Android,H5)的統一訪問資源的方式,能用URI的這種方式實現麼?

4、App內組件間路由設計

上一章節中咱們介紹了iOS系統中,系統是如何幫咱們處理App間跳轉邏輯的。這一章節咱們着重討論一下,App內部,各個組件之間的路由應該怎麼設計。關於App內部的路由設計,主要須要解決2個問題:

1.各個頁面和組件之間的跳轉問題。
2.各個組件之間相互調用。

先來分析一下這兩個問題。

1. 關於頁面跳轉

QQ截圖20170301110545.png

在iOS開發的過程當中,常常會遇到如下的場景,點擊按鈕跳轉Push到另一個界面,或者點擊一個cell Present一個新的ViewController。在MVC模式中,通常都是新建一個VC,而後Push / Present到下一個VC。可是在MVVM中,會有一些不合適的狀況。

1194012-35db9020069ee57b.gif

衆所周知,MVVM把MVC拆成了上圖演示的樣子,原來View對應的與數據相關的代碼都移到ViewModel中,相應的C也變瘦了,演變成了M-VM-C-V的結構。這裏的C裏面的代碼能夠只剩下頁面跳轉相關的邏輯。若是用代碼表示就是下面這樣子:

假設一個按鈕的執行邏輯都封裝成了command。

14.png

上述的代碼自己沒啥問題,可是可能會弱化MVVM框架的一個重要做用。

MVVM框架的目的除去解耦之外,還有2個很重要的目的:

  • 代碼高複用率

  • 方便進行單元測試

若是須要測試一個業務是否正確,咱們只要對ViewModel進行單元測試便可。前提是假定咱們使用ReactiveCocoa進行UI綁定的過程是準確無誤的。目前綁定是正確的。因此咱們只須要單元測試到ViewModel便可完成業務邏輯的測試。

頁面跳轉也屬於業務邏輯,因此應該放在ViewModel中一塊兒單元測試,保證業務邏輯測試的覆蓋率。

把頁面跳轉放到ViewModel中,有2種作法,第一種就是用路由來實現,第二種因爲和路由沒有關係,因此這裏就很少闡述,有興趣的能夠看lpd-mvvm-kit這個庫關於頁面跳轉的具體實現。

頁面跳轉相互的耦合性也就體現出來了:

1.因爲pushViewController或者presentViewController,後面都須要帶一個待操做的ViewController,那麼就必需要引入該類,import頭文件也就引入了耦合性。
2.因爲跳轉這裏寫死了跳轉操做,若是線上一旦出現了bug,這裏是不受咱們控制的。
3.推送消息或者是3D-Touch需求,要求直接跳轉到內部第10級界面,那麼就須要寫一個入口跳轉到指定界面。

2. 關於組件間調用

QQ截圖20170301110719.png

關於組件間的調用,也須要解耦。隨着業務愈來愈複雜,咱們封裝的組件愈來愈多,要是封裝的粒度拿捏不許,就會出現大量組件之間耦合度高的問題。組件的粒度能夠隨着業務的調整,不斷的調整組件職責的劃分。可是組件之間的調用依舊不可避免,相互調用對方組件暴露的接口。如何減小各個組件之間的耦合度,是一個設計優秀的路由的職責所在。

3. 如何設計一個路由

如何設計一個能完美解決上述2個問題的路由,讓咱們先來看看GitHub上優秀開源庫的設計思路。如下是我從Github上面找的一些路由方案,按照Star從高到低排列。依次來分析一下它們各自的設計思路。

(1)JLRoutes Star 3189

JLRoutes在整個Github上面Star最多,那就來從它來分析分析它的具體設計思路。

首先JLRoutes是受URL Scheme思路的影響。它把全部對資源的請求當作是一個URI。

首先來熟悉一下NSURLComponent的各個字段:

36.png

Note
The URLs employed by the NSURL
class are described in RFC 1808RFC 1738, and RFC 2732.

JLRoutes會傳入每一個字符串,都按照上面的樣子進行切分處理,分別根據RFC的標準定義,取到各個NSURLComponent。

55.png

JLRoutes全局會保存一個Map,這個Map會以scheme爲Key,JLRoutes爲Value。因此在routeControllerMap裏面每一個scheme都是惟一的。

至於爲什麼有這麼多條路由,筆者認爲,若是路由按照業務線進行劃分的話,每一個業務線可能會有不相同的邏輯,即便每一個業務裏面的組件名字可能相同,可是因爲業務線不一樣,會有不一樣的路由規則。

舉個例子:若是滴滴按照每一個城市的打車業務進行組件化拆分,那麼每一個城市就對應着這裏的每一個scheme。每一個城市的打車業務都有叫車,付款……等業務,可是因爲每一個城市的地方法規不相同,因此這些組件即便名字相同,可是裏面的功能也許千差萬別。因此這裏劃分出了多個route,也能夠理解爲不一樣的命名空間。

在每一個JLRoutes裏面都保存了一個數組,這個數組裏面保存了每一個路由規則JLRRouteDefinition裏面會保存外部傳進來的block閉包,pattern,和拆分以後的pattern。

在每一個JLRoutes的數組裏面,會按照路由的優先級進行排列,優先級高的排列在前面。

15.png

因爲這個數組裏面的路由是一個單調隊列,因此查找優先級的時候只用從高往低遍歷便可。

具體查找路由的過程以下:

QQ截圖20170301110941.png

首先根據外部傳進來的URL初始化一個JLRRouteRequest,而後用這個JLRRouteRequest在當前的路由數組裏面依次request,每一個規則都會生成一個response,可是隻有符合條件的response纔會match,最後取出匹配的JLRRouteResponse拿出其字典parameters裏面對應的參數就能夠了。查找和匹配過程當中重要的代碼以下:

16.png

17.png

舉個例子:

咱們先註冊一個Router,規則以下:

18.png

咱們傳入一個URL,讓Router進行處理。

19.png

匹配成功以後,咱們會獲得下面這樣一個字典:

20.png

把上述過程圖解出來,見下圖:

QQ截圖20170301111611.png

JLRoutes還能夠支持Optional的路由規則,假如定義一條路由規則:

21.png

JLRoutes 會幫咱們默認註冊以下4條路由規則:

22.png

(2)routable-ios Star 1415

Routable路由是用在in-app native端的 URL router, 它能夠用在iOS上也能夠用在Android上。

1.png

UPRouter裏面保存了2個字典。routes字典裏面存儲的Key是路由規則,Value存儲的是UPRouterOptions。cachedRoutes裏面存儲的Key是最終的URL,帶傳參的,Value存儲的是RouterParams。RouterParams裏面會包含在routes匹配的到的UPRouterOptions,還有額外的打開參數openParams和一些額外參數extraParams。

23.png

24.png

這一段代碼裏面重點在幹一件事情,遍歷routes字典,而後找到參數匹配的字符串,封裝成RouterParams返回。

25.png

上面這段函數,第一個參數是外部傳進來URL帶有各個入參的分割數組。第二個參數是路由規則分割開的數組。routerComponent因爲規定:號後面纔是參數,因此routerComponent的第1個位置就是對應的參數名。params字典裏面以參數名爲Key,參數爲Value。

26.png

最後經過RouterParams的初始化方法,把路由規則對應的UPRouterOptions,上一步封裝好的參數字典givenParams,還有
routerParamsForUrl: extraParams: 方法的第二個入參,這3個參數做爲初始化參數,生成了一個RouterParams。

27.png

最後一步self.cachedRoutes的字典裏面Key爲帶參數的URL,Value是RouterParams。

QQ截圖20170301112727.png

最後將匹配封裝出來的RouterParams轉換成對應的Controller。

28.png

若是Controller是一個類,那麼就調用allocWithRouterParams:方法去初始化。若是Controller已是一個實例了,那麼就調用initWithRouterParams:方法去初始化。

將Routable的大體流程圖解以下:

22.png

(3)HHRouter Star 1277

這是布丁動畫的一個Router,靈感來自於 ABRouter 和 Routable iOS

先來看看HHRouter的Api。它提供的方法很是清晰。

ViewController提供了2個方法。map是用來設置路由規則,matchController是用來匹配路由規則的,匹配爭取以後返回對應的UIViewController。

29.png

block閉包提供了三個方法,map也是設置路由規則,matchBlock:是用來匹配路由,找到指定的block,可是不會調用該block。callBlock:是找到指定的block,找到之後就當即調用。

30.png

matchBlock:和callBlock:的區別就在於前者不會自動調用閉包。因此matchBlock:方法找到對應的block以後,若是想調用,須要手動調用一次。

除去上面這些方法,HHRouter還爲咱們提供了一個特殊的方法。

31.png

這個方法就是用來找到執行路由規則對應的RouteType,RouteType總共就3種:

32.png

再來看看HHRouter是如何管理路由規則的。整個HHRouter就是由一個NSMutableDictionary *routes控制的。

33.png

QQ截圖20170301130543.png

別看只有這一個看似「簡單」的字典數據結構,可是HHRouter路由設計的仍是很精妙的。

34.png

上面兩個方法分別是block閉包和ViewController設置路由規則調用的方法實體。無論是ViewController仍是block閉包,設置規則的時候都會調用subRoutesToRoute:方法。

37.png

上面這段函數就是來構造路由匹配規則的字典。

舉個例子:

38.png

設置3條規則之後,按照上面構造路由匹配規則的字典的方法,該路由規則字典就會變成這個樣子:

39.png

路由規則字典生成以後,等到匹配的時候就會遍歷這個字典。

假設這時候有一條路由過來:

40.png

HHRouter對這條路由的處理方式是先匹配前面的scheme,若是連scheme都不正確的話,會直接致使後面匹配失敗。

而後再進行路由匹配,最後生成的參數字典以下:

41.png

具體的路由參數匹配的函數在

42.png

這個方法裏面實現的。這個方法就是按照路由匹配規則,把傳進來的URL的參數都一一解析出來,帶?號的也都會解析成字典。這個方法沒什麼難度,就不在贅述了。

ViewController 的字典裏面默認還會加上2項:

560.png

route裏面都會保存傳過來的完整的URL。

若是傳進來的路由後面帶訪問字符串呢?那咱們再來看看:

561.png

那麼解析出全部的參數字典會是下面的樣子:

562.png

同理,若是是一個block閉包的狀況呢?

仍是先添加一條block閉包的路由規則:

563.png

這條規則對應的會生成一個路由規則的字典。

564.png

注意」_」後面跟着是一個block。

匹配block閉包的方式有兩種。

565.png

匹配出來的參數字典是以下:

566.png

block的字典裏面會默認加上下面這2項:

567.png

route裏面都會保存傳過來的完整的URL。

生成的參數字典最終會被綁定到ViewController的Associated Object關聯對象上。

568.png

這個綁定的過程是在match匹配完成的時候進行的。

569.png

最終獲得的ViewController也是咱們想要的。相應的參數都在它綁定的params屬性的字典裏面。

將上述過程圖解出來,以下:

33.png

(4)MGJRouter Star 633

這是蘑菇街的一個路由的方法。

這個庫的由來:

JLRoutes 的問題主要在於查找 URL 的實現不夠高效,經過遍歷而不是匹配。還有就是功能偏多。

HHRouter 的 URL 查找是基於匹配,因此會更高效,MGJRouter 也是採用的這種方法,但它跟 ViewController 綁定地過於緊密,必定程度上下降了靈活性。

因而就有了 MGJRouter。

從數據結構來看,MGJRouter仍是和HHRouter如出一轍的。

570.png

34.png

那麼咱們就來看看它對HHRouter作了哪些優化改進。

1.MGJRouter支持openURL時,能夠傳一些 userinfo 過去

571.png

這個對比HHRouter,僅僅只是寫法上的一個語法糖,在HHRouter中雖然不支持帶字典的參數,可是在URL後面能夠用URL Query Parameter來彌補。

572.png

MGJRouter對userInfo的處理是直接把它封裝到Key = MGJRouterParameterUserInfo對應的Value裏面。

2.支持中文的URL。

573.png

這裏就是須要注意一下編碼。

3.定義一個全局的 URL Pattern 做爲 Fallback。

這一點是模仿的JLRoutes的匹配不到會自動降級到global的思想。

574.png

parameters字典裏面會先存儲下一個路由規則,存在block閉包中,在匹配的時候會取出這個handler,降級匹配到這個閉包中,進行最終的處理。

4.當 OpenURL 結束時,能夠執行 Completion Block。

在MGJRouter裏面,做者對原來的HHRouter字典裏面存儲的路由規則的結構進行了改造。

575.png

這3個key會分別保存一些信息:

MGJRouterParameterURL保存的傳進來的完整的URL信息。
MGJRouterParameterCompletion保存的是completion閉包。
MGJRouterParameterUserInfo保存的是UserInfo字典。

舉個例子:

576.png

上面的URL會匹配成功,那麼生成的參數字典結構以下:

577.png

5.能夠統一管理URL

這個功能很是有用。

URL 的處理一不當心,就容易散落在項目的各個角落,不容易管理。好比註冊時的 pattern 是 mgj://beauty/:id,而後 open 時就是 mgj://beauty/123,這樣到時候 url 有改動,處理起來就會很麻煩,很差統一管理。

因此 MGJRouter 提供了一個類方法來處理這個問題。

578.png

generateURLWithPattern:函數會對咱們定義的宏裏面的全部的:進行替換,替換成後面的字符串數組,依次賦值。

將上述過程圖解出來,以下:

35.png

蘑菇街爲了區分開頁面間調用和組件間調用,因而想出了一種新的方法。用Protocol的方法來進行組件間的調用。

每一個組件之間都有一個 Entry,這個 Entry,主要作了三件事:

  • 註冊這個組件關心的 URL

  • 註冊這個組件可以被調用的方法/屬性

  • 在 App 生命週期的不一樣階段作不一樣的響應

頁面間的openURL調用就是以下的樣子:

QQ截圖20170301131642.png

每一個組件間都會向MGJRouter註冊,組件間相互調用或者是其餘的App均可以經過openURL:方法打開一個界面或者調用一個組件。

在組件間的調用,蘑菇街採用了Protocol的方式。

32.png

[ModuleManager registerClass:ClassA forProtocol:ProtocolA] 的結果就是在 MM 內部維護的 dict 裏新加了一個映射關係。

[ModuleManager classForProtocol:ProtocolA] 的返回結果就是以前在 MM 內部 dict 裏 protocol 對應的 class,使用方不須要關心這個 class 是個什麼東東,反正實現了 ProtocolA 協議,拿來用就行。

這裏須要有一個公共的地方來容納這些 public protocl,也就是圖中的 PublicProtocl.h。

我猜想,大概實現多是下面的樣子:

580.png

而後這個是一個單例,在裏面註冊各個協議:

581.png

在ModuleProtocolManager中用一個字典保存每一個註冊的protocol。如今再來猜猜ModuleEntry的實現。

582.png

而後每一個模塊內都有一個和暴露到外面的協議相鏈接的「接頭」。

QQ截圖20170301132857.png

在它的實現中,須要引入3個外部文件,一個是ModuleProtocolManager,一個是DetailModuleEntryProtocol,最後一個是所在模塊須要跳轉或者調用的組件或者頁面。

QQ截圖20170301132934.png

至此基於Protocol的方案就完成了。若是須要調用某個組件或者跳轉某個頁面,只要先從ModuleProtocolManager的字典裏面根據對應的ModuleEntryProtocol找到對應的DetailModuleEntry,找到了DetailModuleEntry就是找到了組件或者頁面的「入口」了。再把參數傳進去便可。

323.png

這樣就能夠調用到組件或者界面了。

若是組件之間有相同的接口,那麼還能夠進一步的把這些接口都抽離出來。這些抽離出來的接口變成「元接口」,它們是能夠足夠支撐起整個組件一層的。

1194012-122920349fc0ac08.png

(5)CTMediator Star 803

再來講說@casatwy的方案,這方案是基於Mediator的。

傳統的中間人Mediator的模式是這樣的:

321.png

這種模式每一個頁面或者組件都會依賴中間者,各個組件之間互相再也不依賴,組件間調用只依賴中間者Mediator,Mediator仍是會依賴其餘組件。那麼這是最終方案了麼?

看看@casatwy是怎麼繼續優化的。

主要思想是利用了Target-Action簡單粗暴的思想,利用Runtime解決解耦的問題。

50.png

51.png

targetName就是調用接口的Object,actionName就是調用方法的SEL,params是參數,shouldCacheTarget表明是否須要緩存,若是須要緩存就把target存起來,Key是targetClassString,Value是target。

經過這種方式進行改造的,外面調用的方法都很統一,都是調用performTarget: action: params: shouldCacheTarget:。第三個參數是一個字典,這個字典裏面能夠傳不少參數,只要Key-Value寫好就能夠了。處理錯誤的方式也統一在一個地方了,target沒有,或者是target沒法響應相應的方法,均可以在Mediator這裏進行統一出錯處理。

可是在實際開發過程當中,無論是界面調用,組件間調用,在Mediator中須要定義不少方法。因而作做者又想出了建議咱們用Category的方法,對Mediator的全部方法進行拆分,這樣就就能夠不會致使Mediator這個類過於龐大了。

52.png

把這些具體的方法一個個的都寫在Category裏面就行了,調用的方式都很是的一致,都是調用performTarget: action: params: shouldCacheTarget:方法。

最終去掉了中間者Mediator對組件的依賴,各個組件之間互相再也不依賴,組件間調用只依賴中間者Mediator,Mediator不依賴其餘任何組件。

369.png

(6)一些並無開源的方案

除了上面開源的路由方案,還有一些並無開源的設計精美的方案。這裏能夠和你們一塊兒分析交流一下。

1194012-5e8372009b87f2ef.jpeg

這個方案是Uber 騎手App的一個方案。

Uber在發現MVC的一些弊端以後:好比動輒上萬行巨胖無比的VC,沒法進行單元測試等缺點後,因而考慮把架構換成VIPER。可是VIPER也有必定的弊端。由於它的iOS特定的結構,意味着iOS必須爲Android作出一些妥協的權衡。以視圖爲驅動的應用程序邏輯,表明應用程序狀態由視圖驅動,整個應用程序都鎖定在視圖樹上。由操做應用程序狀態所關聯的業務邏輯的改變,就必須通過Presenter。所以會暴露業務邏輯。最終致使了視圖樹和業務樹進行了牢牢的耦合。這樣想實現一個牢牢只有業務邏輯的Node節點或者牢牢只有視圖邏輯的Node節點就很是的困難了。

經過改進VIPER架構,吸取其優秀的特色,改進其缺點,就造成了Uber 騎手App的全新架構——Riblets(肋骨)。

QQ截圖20170301133949.png

在這個新的架構中,即便是類似的邏輯也會被區分紅很小很小,相互獨立,能夠單獨進行測試的組件。每一個組件都有很是明確的用途。使用這些一小塊一小塊的Riblets(肋骨),最終把整個App拼接成一顆Riblets(肋骨)樹。

經過抽象,一個Riblets(肋骨)被定義成一下6個更小的組件,這些組件各自有各自的職責。經過一個Riblets(肋骨)進一步的抽象業務邏輯和視圖邏輯。

330.png

一個Riblets(肋骨)被設計成這樣,那和以前的VIPER和MVC有什麼區別呢?最大的區別在路由上面。

Riblets(肋骨)內的Router再也不是視圖邏輯驅動的,如今變成了業務邏輯驅動。這一重大改變就致使了整個App再也不是由表現形式驅動,如今變成了由數據流驅動。

每個Riblet都是由一個路由Router,一個關聯器Interactor,一個構造器Builder和它們相關的組件構成的。因此它的命名(Router - Interactor - Builder,Rib)也由此得來。固然還能夠有可選的展現器Presenter和視圖View。路由Router和關聯器Interactor處理業務邏輯,展現器Presenter和視圖View處理視圖邏輯。

重點分析一下Riblet裏面路由的職責。

1.路由的職責

在整個App的結構樹中,路由的職責是用來關聯和取消關聯其餘子Riblet的。至於決定是由關聯器Interactor傳遞過來的。在狀態轉換過程當中,關聯和取消關聯子Riblet的時候,路由也會影響到關聯器Interactor的生命週期。路由只包含2個業務邏輯:

  • 提供關聯和取消關聯其餘路由的方法。

  • 在多個孩子之間決定最終狀態的狀態轉換邏輯。

2.拼裝

每個Riblets只有一對Router路由和Interactor關聯器。可是它們能夠有多對視圖。Riblets只處理業務邏輯,不處理視圖相關的部分。Riblets能夠擁有單一的視圖(一個Presenter展現器和一個View視圖),也能夠擁有多個視圖(一個Presenter展現器和多個View視圖,或者多個Presenter展現器和多個View視圖),甚至也能夠能沒有視圖(沒有Presenter展現器也沒有View視圖)。這種設計能夠有助於業務邏輯樹的構建,也能夠和視圖樹作到很好的分離。

舉個例子,騎手的Riblet是一個沒有視圖的Riblet,它用來檢查當前用戶是否有一個激活的路線。若是騎手肯定了路線,那麼這個Riblet就會關聯到路線的Riblet上面。路線的Riblet會在地圖上顯示出路線圖。若是沒有肯定路線,騎手的Riblet就會被關聯到請求的Riblet上。請求的Riblet會在屏幕上顯示等待被呼叫。像騎手的Riblet這樣沒有任何視圖邏輯的Riblet,它分開了業務邏輯,在驅動App和支撐模塊化架構起了重大做用。

3.Riblets是如何工做的

Riblet中的數據流

QQ截圖20170301134121.png

在這個新的架構中,數據流動是單向的。Data數據流從service服務流到Model Stream生成Model流。Model流再從Model Stream流動到Interactor關聯器。Interactor關聯器,scheduler調度器,遠程推送均可以想Service觸發變化來引發Model Stream的改動。Model Stream生成不可改動的models。這個強制的要求就致使關聯器只能經過Service層改變App的狀態。

舉兩個例子:

1.數據從後臺到視圖View上
一個狀態的改變,引發服務器後臺觸發推送到App。數據就被Push到App,而後生成不可變的數據流。關聯器收到model以後,把它傳遞給展現器Presenter。展現器Presenter把model轉換成view model傳遞給視圖View。

2.數據從視圖到服務器後臺
當用戶點擊了一個按鈕,好比登陸按鈕。視圖View就會觸發UI事件傳遞給展現器Presenter。展現器Presenter調用關聯器Interactor登陸方法。關聯器Interactor又會調用Service call的實際登陸方法。請求網絡以後會把數據pull到後臺服務器。

Riblet間的數據流

325.png

當一個關聯器Interactor在處理業務邏輯的工程中,須要調用其餘Riblet的事件的時候,關聯器Interactor須要和子關聯器Interactor進行關聯。見上圖5個步驟。

若是調用方法是從子調用父類,父類的Interactor的接口一般被定義成監聽者listener。若是調用方法是從父類調用到子類,那麼子類的接口一般是一個delegate,實現父類的一些Protocol。

在Riblet的方案中,路由Router僅僅只是用來維護一個樹型關係,而關聯器Interactor才擔當的是用來決定觸發組件間的邏輯跳轉的角色。

5、各個方案優缺點

QQ截圖20170301134242.png

通過上面的分析,能夠發現,路由的設計思路是從URLRoute ->Protocol-class ->Target-Action一步步的深刻的過程。這也是逐漸深刻本質的過程。

1. URLRoute註冊方案的優缺點

首先URLRoute也許是借鑑前端Router和系統App內跳轉的方式想出來的方法。它經過URL來請求資源。無論是H5,RN,Weex,iOS界面或者組件請求資源的方式就都統一了。URL裏面也會帶上參數,這樣調用什麼界面或者組件均可以。因此這種方式是最容易,也是最早能夠想到的。

URLRoute的優勢不少,最大的優勢就是服務器能夠動態的控制頁面跳轉,能夠統一處理頁面出問題以後的錯誤處理,能夠統一三端,iOS,Android,H5 / RN / Weex 的請求方式。

可是這種方式也須要看不一樣公司的需求。若是公司裏面已經完成了服務器端動態下發的腳手架工具,前端也完成了Native端若是出現錯誤了,能夠隨時替換相同業務界面的需求,那麼這個時候可能選擇URLRoute的概率會更大。

可是若是公司裏面H5沒有作相關出現問題後能替換的界面,H5開發人員以爲這是給他們增添負擔。若是公司也沒有完成服務器動態下發路由規則的那套系統,那麼公司可能就不會採用URLRoute的方式。由於URLRoute帶來的少許動態性,公司是能夠用JSPatch來作到。線上出現bug了,能夠當即用JSPatch修掉,而不採用URLRoute去作。

因此選擇URLRoute這種方案,也要看公司的發展狀況和人員分配,技術選型方面。

URLRoute方案也是存在一些缺點的,首先URL的map規則是須要註冊的,它們會在load方法裏面寫。寫在load方法裏面是會影響App啓動速度的。

其次是大量的硬編碼。URL連接裏面關於組件和頁面的名字都是硬編碼,參數也都是硬編碼。並且每一個URL參數字段都必需要一個文檔進行維護,這個對於業務開發人員也是一個負擔。並且URL短鏈接散落在整個App四處,維護起來實在有點麻煩,雖然蘑菇街想到了用宏統一管理這些連接,可是仍是解決不了硬編碼的問題。

真正一個好的路由是在無形當中服務整個App的,是一個無感知的過程,從這一點來講,略有點缺失。

最後一個缺點是,對於傳遞NSObject的參數,URL是不夠友好的,它最可能是傳遞一個字典。

2. Protocol-Class註冊方案的優缺點

Protocol-Class方案的優勢,這個方案沒有硬編碼。

Protocol-Class方案也是存在一些缺點的,每一個Protocol都要向ModuleManager進行註冊。

這種方案ModuleEntry是同時須要依賴ModuleManager和組件裏面的頁面或者組件二者的。固然ModuleEntry也是會依賴ModuleEntryProtocol的,可是這個依賴是能夠去掉的,好比用Runtime的方法NSProtocolFromString,加上硬編碼是能夠去掉對Protocol的依賴的。可是考慮到硬編碼的方式對出現bug,後期維護都是不友好的,因此對Protocol的依賴仍是不要去除。

最後一個缺點是組件方法的調用是分散在各處的,沒有統一的入口,也就無法作組件不存在時或者出現錯誤時的統一處理。

3. Target-Action方案的優缺點

Target-Action方案的優勢,充分的利用Runtime的特性,無需註冊這一步。Target-Action方案只有存在組件依賴Mediator這一層依賴關係。在Mediator中維護針對Mediator的Category,每一個category對應一個Target,Categroy中的方法對應Action場景。Target-Action方案也統一了全部組件間調用入口。

Target-Action方案也能有必定的安全保證,它對url中進行Native前綴進行驗證。

Target-Action方案的缺點,Target_Action在Category中將常規參數打包成字典,在Target處再把字典拆包成常規參數,這就形成了一部分的硬編碼。

4. 組件如何拆分?

這個問題其實應該是在打算實施組件化以前就應該考慮的問題。爲什麼還要放在這裏說呢?由於組件的拆分每一個公司都有屬於本身的拆分方案,按照業務線拆?按照最細小的業務功能模塊拆?仍是按照一個完成的功能進行拆分?這個就牽扯到了拆分粗細度的問題了。組件拆分的粗細度就會直接關係到將來路由須要解耦的程度。

假設,把登陸的全部流程封裝成一個組件,因爲登陸裏面會涉及到多個頁面,那麼這些頁面都會打包在一個組件裏面。那麼其餘模塊須要調用登陸狀態的時候,這時候就須要用到登陸組件暴露在外面能夠獲取登陸狀態的接口。那麼這個時候就能夠考慮把這些接口寫到Protocol裏面,暴露給外面使用。或者用Target-Action的方法。這種把一個功能所有都劃分紅登陸組件的話,劃分粒度就稍微粗一點。

若是僅僅把登陸狀態的細小功能劃分紅一個元組件,那麼外面想獲取登陸狀態就直接調用這個組件就好。這種劃分的粒度就很是細了。這樣就會致使組件個數巨多。

因此在進行拆分組件的時候,也許當時業務並不複雜的時候,拆分紅組件,相互耦合也不大。可是隨着業務無論變化,以前劃分的組件間耦合性愈來愈大,因而就會考慮繼續把以前的組件再進行拆分。也許有些業務砍掉了,以前一些小的組件也許還會被組合到一塊兒。總之,在業務沒有徹底固定下來以前,組件的劃分可能一直進行時。

6、最好的方案

1194012-80d7e39d04c3a0b1.png

關於架構,我以爲拋開業務談架構是沒有意義的。由於架構是爲了業務服務的,空談架構只是一種理想的狀態。因此沒有最好的方案,只有最適合的方案。

最適合本身公司業務的方案纔是最好的方案。分而治之,針對不一樣業務選擇不一樣的方案纔是最優的解決方案。若是非要籠統的採用一種方案,不一樣業務之間須要同一種方案,須要妥協犧牲的東西太多就很差了。

但願本文能拋磚引玉,幫助你們選擇出最適合自家業務的路由方案。固然確定會有更加優秀的方案,但願你們能多多指點我。

References:

相關文章
相關標籤/搜索