你好,歡迎來到IOS教程網

 Ios教程網 >> IOS編程開發 >> IOS開發基礎 >> iOS 組件化 —— 路由設計思路分析

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

編輯:IOS開發基礎

原文

前言

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

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

目錄

1.引子

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

3.App之間跳轉實現

4.App內組件間路由設計

5.各個方案優缺點

6.最好的方案

一、引子

大前端發展這麼多年了,相信也一定會遇到相似的問題。近兩年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主要處理綠色的那一塊。

二、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端設計一個路由來解決。那麼我們怎麼設計一個路由呢?

三、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也是可以直接跳轉到系統設置的。比如有些需求要求檢測用戶有沒有開啟某些系統權限,如果沒有開啟就彈框提示,點擊彈框的按鈕直接跳轉到系統設置裡面對應的設置界面。

  • iOS 10 支持通過 URL Scheme 跳轉到系統設置

  • iOS10跳轉系統設置的正確姿勢

  • 關於 iOS 系統功能的 URL 匯總列表

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的這種方式實現麼?

四、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 1808, RFC 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才擔當的是用來決定觸發組件間的邏輯跳轉的角色。

五、各個方案優缺點

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的方法。這種把一個功能全部都劃分成登錄組件的話,劃分粒度就稍微粗一點。

如果僅僅把登錄狀態的細小功能劃分成一個元組件,那麼外面想獲取登錄狀態就直接調用這個組件就好。這種劃分的粒度就非常細了。這樣就會導致組件個數巨多。

所以在進行拆分組件的時候,也許當時業務並不復雜的時候,拆分成組件,相互耦合也不大。但是隨著業務不管變化,之前劃分的組件間耦合性越來越大,於是就會考慮繼續把之前的組件再進行拆分。也許有些業務砍掉了,之前一些小的組件也許還會被組合到一起。總之,在業務沒有完全固定下來之前,組件的劃分可能一直進行時。

六、最好的方案

1194012-80d7e39d04c3a0b1.png

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

最適合自己公司業務的方案才是最好的方案。分而治之,針對不同業務選擇不同的方案才是最優的解決方案。如果非要籠統的采用一種方案,不同業務之間需要同一種方案,需要妥協犧牲的東西太多就不好了。

希望本文能拋磚引玉,幫助大家選擇出最適合自家業務的路由方案。當然肯定會有更加優秀的方案,希望大家能多多指點我。

References:

  • 在現有工程中實施基於CTMediator的組件化方案

  • iOS應用架構談 組件化方案

  • 蘑菇街 App 的組件化之路

  • 蘑菇街 App 的組件化之路·續

  • ENGINEERING THE ARCHITECTURE BEHIND UBER’S NEW RIDER APP

  1. 上一頁:
  2. 下一頁:
蘋果刷機越獄教程| IOS教程問題解答| IOS技巧綜合| IOS7技巧| IOS8教程
Copyright © Ios教程網 All Rights Reserved