iOS路由設計(三)帶你一步步構建iOS路由


http://www.jianshu.com/p/3a902f274a3d
正則表達式


接上一篇移動端路由層設計,這一篇是實戰篇,手把手的帶你編寫一個簡單的路由組件。有朋友說不少人都收藏之後就再也沒看過,其實這屬於時間管理問題,在你忙碌的工做和生活的時候,有時候須要你稍微停頓一下,思考一下,例如,你能夠把本篇文章收藏之後再在iPhone的提醒事項里加入到一個閱讀清單裏,不用設置提醒,只須要在你閒的時候抽出一兩個小時,看一下。想象一下你本身動手從發現問題到解決問題再到作出一個解決問題的組件的過程給你帶來的成就感和獲取的進階經驗,再稍微改變一下你對天天須要處理的繁瑣事物的管理方式,也許你的生活和工做就會豁然開朗。express

這個路由到底是什麼鬼?能解決什麼問題?

舉一些場景來看看

場景1:一個App項目中團隊人員比較多,不一樣的人負責不一樣的模塊開發,有的人直接使用資源文件設計的,有的人用代碼直接寫的,有的人負責登陸,有的人負責訂單,忽然有一天搞訂單的開發A找搞登陸的開發B說要調一下登陸,登陸成功之後你要再回調下我寫的模塊的方法告訴我成功登陸,我要刷新一下訂單頁面,B傻傻的就答應了,找B的人C、D、F....愈來愈多,B負責的代碼越寫越多,同時A也不怎麼開心,由於A發現調B寫的登陸要經過類實例化函數獲取模塊,調C寫的支付使用工廠方法,調D寫的計算器組件又是另一種寫法,結果A本身的代碼也愈來愈醜。後端

場景2:一個App裏面有不少內嵌的H5頁面,纏品A對猿B說,咱們的活動頁面要調用一下咱們的訂單頁面,用戶若是下了一個訂單成功之後H5要可以拿到反饋有歡迎語,猿B和H5的開發猿C通過好久好久的討論,肯定了H5若是調用App的訂單頁面,參數怎麼傳,訂單提交之後怎麼再調H5的接口,參數怎麼定義,各自把代碼寫到各自的項目裏,沒過多久纏品A說另外的H5要調用原生的界面,怎麼怎麼個流程,推送點擊要調用原生的某個頁面,點完要反饋給後臺統計,兄弟App要跳轉到咱們的App某個頁面跳轉完成某個動做之後要再跳轉回去......猿B往往接到這樣的需求就牢牢握住本身中箭的膝蓋,收拾了一下寫的那麼多代碼,深藏功與名......�.設計模式

出了什麼問題?

我想上面的兩個場景出現的問題你們或多或少都會碰見,總結一下就是:架構

  1. 由於不一樣人負責不一樣模塊,調用他人必須瞭解他人編寫的模塊如何調用,對象是啥,初始化方式是啥,這違背了面向對象的封裝原則
  2. 引入不一樣的模塊頭文件,多了之後,所依賴的外部發生一丁點變化你就要跟着變,邏輯變得愈來愈耦合,不利於維護
  3. 調用不一樣模塊要反覆與他人溝通傳參、回調流程、接口定義等等,溝通效率低下
  4. 產品提出各類需求,可是我寫的代碼都是差很少的,來一個頁面我須要寫一些相同邏輯的代碼,並且產品還抱怨每次加相同的東西就要改代碼發版,這顯然不能知足複用的要求。

總結:
依賴多、耦合高、複用低。
可咱們都知道有這麼句話啊:高內聚、低耦合,職責單一邏輯清晰。 app

路由就是解決上面的問題

咱們已經發現依賴比較大是由於要導入其餘模塊的頭文件,瞭解其餘模塊的邏輯和定義,若是多了,你的代碼中引入的頭文件或者導入的包名愈來愈多,改一下牽一髮而動全身啊。大概是這個樣子:框架


相互引用


依賴的問題很嚴重,要想破除這樣的依賴,咱們能想到的辦法就是找個調度中心去作這件事,其實各個業務模塊並不關心其餘模塊具體的業務邏輯是什麼,也不須要知道這個模塊如何獲取,我只關心怎麼調用和反饋的結果,而這個有了調度中心這個東西,每一個模塊不須要依賴其餘模塊,只須要調度中心關心每一個模塊的調度。
less


引入中介者


有了Route這個調度中心,每一個模塊就不用寫那麼多重複的耦合代碼了,也不須要在導入那麼多頭文件了和引入那麼多包名了,這些藍色的箭頭表明着調用方式,若是調用方式再統一一下,溝通效率就提高上去了,由於咱們能夠用一套約定好的數據協議來代替重複溝通,有時候咱們須要靠約定和協議來提升咱們的工做效率。函數

Tips:
發現問題這個環節很重要,你在工做中常常要反覆作的,浪費時間的都是須要你去優化和花大力氣去解決的,做爲一個專業人士,不斷改進你的代碼,優化你的工做流程,帶動團隊向好的協做方式去轉型,這是專業人士的習慣,更應該成爲你的習慣。同時針對代碼存在的問題,也許你常常會隱隱約約感到有問題,就是不知道問題在什麼地方,那麼須要問問本身有沒有如下狀況:哪些代碼是常常寫且重複度很高的,是否是能夠抽象出來?哪些代碼須要反覆的變更,是否是能夠作成配置或者是定義一套數據格式來知足動態兼容?有沒有一些現成的設計模式能夠解決這些問題?比方說,調度中心則使用的是中介者模式。我見過 優化

爲啥要說iOS路由呢?

路由層其實在邏輯功能上的設計都是同樣的,不少人把App中的視圖切換當作是路由組件的功能職責,這點我持否認態度,從單一職責角度和MVC框架分析來看,視圖切換屬於View中的交互邏輯並不屬於消息傳遞或者是事件分發的範疇,但路由請求、視圖轉場的實現部分與Android平臺和iOS平臺上的導航機制有着很是緊密的關係,Android操做系統有着自然的架構優點,Intent機制能夠協助應用間的交互與通信,是對調用組件和數據傳遞的描述,自己這種機制就解除了代碼邏輯和界面之間的依賴關係,只有數據依賴。而iOS的界面導航和轉場機制則大部分依賴UI組件各自的實現,因此如何解決這個問題,iOS端路由的實現則比較有表明性。
其實說白一點,路由層解決的核心問題就是原來界面或者組件之間相互調用都必須相互依賴,須要導入目標的頭文件、須要清楚目標對象的邏輯,而如今所有都經過路由中轉,只依賴路由或者某種通信協議,或者依靠一些消息傳遞機制連路由都不依賴。其次,路由的核心邏輯就是目標匹配,對於外部調用的狀況來講,URL如何匹配Handler是最爲重要的,匹配就必然用到正則表達式。瞭解這些關鍵點之後就有了設計的目的性,let‘s do it~

總結一下這個路由都要有什麼?(需求分析)

咱們先根據上面的模糊的總結梳理一下:

  1. 路由須要可以實現被其餘模塊調度,從而調度另一個模塊
  2. 接入路由的模塊不須要知道目標模塊的實現
  3. 調度發起方須要有目標的響應回調,相似於http請求,有一個request就要有一個response,才能實現雙向的調用
  4. 調用方式須要統一,統一而鬆散的調用協議和數據協議能夠減小大量接入成本和溝通成本
    那一個完整的調度流程應該是這樣的:

Route流程


看到這個流程之後,能夠肯定如下幾件事:

  1. A模塊調用路由,爲表達本身須要調用的是B模塊,考慮到H五、推送以及其餘App的外部調用,可使用URL這種方式來定義目標,也就是說用URL來表示目標B
  2. 對一個URL的請求來講,路由須要有統一的回調處理,固然,若是不須要回調也是能夠的,回調是須要目標去觸發的
  3. 路由要有處理URL的功能,並調用其餘模塊的能力

根據以上粗略的定義一下路由的框架:


框架類圖


這裏面以供有4部分:
WLRRouter就是一個實體對象,用來提供給其餘模塊調用。
WLRRouteRequest是一個以URL爲基礎的實體對象,爲何不直接用URL字符串?由於考慮到若是路由在內部調用其餘模塊的時候須要傳入一些原生對象,而URL上只能攜帶類型單一的字符串鍵值對錶示參數,因此須要使用這麼一個對象進行包裝。
WLRRouteHandler是一個處理某一個WLRRouteRequest請求的對象,當路由接收一個WLRRouteRequest請求,轉發給一個WLRRouteHandler處理,處理完畢之後若是有回調,則回調給調用者。URL的請求與Handler的對應關係確定須要匹配的邏輯,爲了使得路由內部邏輯更加清晰單獨使用WLRRouteMatcher來處理匹配的邏輯。

深刻具體需求,細化功能實現(詳細設計)

有了粗略的需求分析接下來就是細化需求並給出詳細設計的階段了,其實編寫一個模塊要有系統性思惟,粗略的需求裏面包含了整個模塊要實現的主要核心功能,核心流程是什麼,要有哪幾個類才能實現這樣的流程,不要妄圖一會兒深刻到細枝末節上,讓細節左右宏觀上的邏輯架構,大腦不適合同時考慮宏觀和微觀的事情,尤爲是對經驗不太足的開發者來講,要逐漸學會大腦在不一樣的時期進行宏觀和微觀的無縫切換,這樣才能專一目標和結果,在實現過程當中再投入所有精力考慮細節,才能保證具體的實現是不偏離整體目標的。

WLRRouteRequest設計

路由層的請求,不管是跨應用的外部調用(H5調用、其餘App調用)仍是內部調用(內部模塊相互調用),最後都要造成一個路由請求,一個以URL爲基礎的request對象,首先須要有攜帶URL,再一個要攜帶請求所須要的參數,參數有三種,一種是Url上的鍵值對參數,一種是RESTFul風格的Url上的路徑參數,一種是內部調用適用的原生參數,具體是:


WLRRouteRequest


這裏說一下路徑參數,不少有後端開發經驗的人都知道,一個url上傳遞參數,或者是匹配後端服務的service,Url的路徑對於表達轉發語義十分重要,比方說 :
http://aaaa.com/login
http://aaaa.com/userCenter
那Url中的login和userCenter能夠表明是哪一個後端服務,那路由就須要設置正則匹配表達式去匹配http://aaaa.com/ 這部分,截取login、userCenter部分,說回咱們的路由,App的路由須要經過設置Url的正則表達式來獲取路徑參數,同時咱們必須知道這些參數的值和名稱,那麼我能夠這樣定義Url匹配的表達式
scheme://host/path/:name([a-zA-Z_-]+)
熟悉正則表達式的孩子都知道分組模式,path後name是key,([a-zA-Z_-]+)是規定name對應的value應該是什麼格式的。那麼routeParameters就是存放路徑參數的

//url
@property (nonatomic, copy, readonly) NSURL *URL;
//url上?之後的鍵值對參數
@property (nonatomic, copy, readonly) NSDictionary *queryParameters;
//url上匹配的路徑參數
@property (nonatomic, copy, readonly) NSDictionary *routeParameters;
//原生參數,比方說要傳給目標UIImage對象,NSArray對象等等
@property (nonatomic, copy, readonly) NSDictionary *primitiveParams;
//目標預留的callBack block,當完成處理之後,回到此Block,完成調用者的回調
@property(nonatomic,copy)void(^targetCallBack)(NSError *error,id responseObject);
//是否消費掉,一個request只能處理一次,該字段反應request是否被處理過
@property(nonatomic)BOOL isConsumed;

WLRRouteHandler設計

handler對象要接收一個WLRRouteRequest對象來啓動處理流程,前面通過咱們的分析,這個handler應該擔負起經過url和參數獲取目標對象的職責,在通常的route處理中,目標每每是一個視圖控制器,先實現這樣一個經過url調用某一個視圖控制器的並跳轉處理的handler,那麼應該是以下的:


WLRRouteHandler


handler處理一個request請求是一個具備過程性的邏輯,WLRRouteHandler要做爲一個基類,咱們知道,這個handler在須要處理獲取目標視圖控制器->參數傳遞給目標視圖控制器->視圖控制器的轉場->完成回調,那麼咱們須要設計這樣的接口

//即將開始處理request請求,返回值決定是否要繼續相應request
- (BOOL)shouldHandleWithRequest:(WLRRouteRequest *)request;
//開始處理request請求
-(BOOL)handleRequest:(WLRRouteRequest *)request error:(NSError *__autoreleasing *)error;
// 根據request獲取目標控制器
-(UIViewController *)targetViewControllerWithRequest:(WLRRouteRequest *)request;
//轉場必定是從一個視圖控制器跳轉到另一個視圖控制器,該方法用以獲取轉場中的源視圖控制器
-(UIViewController *)sourceViewControllerForTransitionWithRequest:(WLRRouteRequest *)request;
//改方法內根據request、獲取的目標和源視圖控制器,完成轉場邏輯
-(BOOL)transitionWithWithRequest:(WLRRouteRequest *)request sourceViewController:(UIViewController *)sourceViewController targetViewController:(UIViewController *)targetViewController isPreferModal:(BOOL)isPreferModal error:(NSError *__autoreleasing *)error;
//根據request來返回是不是模態跳轉
- (BOOL)preferModalPresentationWithRequest:(WLRRouteRequest *)request;

WLRRouteMatcher設計

一個matcher應該具備根據url和參數判斷是否匹配某個url表達式的邏輯


WLRRouteMatcher


matcher對象必須擁有url的匹配表達式,相似於 scheme://host/path/:name([a-zA-Z_-]+) ,也有擁有該表達式真正的正則表達式,^scheme://host/path/([a-zA-Z_-]+)$ 

@interface WLRRouteMatcher : NSObject
//url匹配表達式
@property(nonatomic,copy)NSString * routeExpressionPattern;
//url匹配的正則表達式
@property(nonatomic,copy)NSString * originalRouteExpression;
+(instancetype)matcherWithRouteExpression:(NSString *)expression;
-(WLRRouteRequest *)createRequestWithURL:(NSURL *)URL primitiveParameters:(NSDictionary *)primitiveParameters targetCallBack:(void(^)(NSError *, id responseObject))targetCallBack;

設計-(WLRRouteRequest *)createRequestWithURL:(NSURL *)URL primitiveParameters:(NSDictionary *)primitiveParameters targetCallBack:(void(^)(NSError *, id responseObject))targetCallBack;這個方法,能夠經過傳入url和參數,檢查是否返回request請求,來表示該WLRRouteMatcher對象所擁有的匹配表達式與url是否可以匹配,這句話有點繞,看不懂的多看幾遍。

WLRRouter

WLRRouter是路由實體對象,後端開發者對於路由掛載的概念很是瞭解,其實這樣一個路由實體對象能夠完成對URL的攔截和處理並返回結果,事實上,根據前面的梳理和總結,WLRRouter對象內部應該保存了須要匹配攔截的URL表達式,而前面咱們知道Url的匹配表達式是存儲在WLRRouteMatcher對象中的,而且一個Url傳入檢查是否匹配也是Matcher對象提供的功能,對於匹配上的Url須要有對應的Handler處理,因此Router對象的內部存在Machter對象和Handler對象一一對應的關係,而且擁有註冊Url表達式對應到Handler的功能,也具備傳入Url和參數就能匹配到Handler的功能,還要有一個檢測Url是否能有對應Handler處理的功能,因此應該是:


WLRRouter


這裏有兩種註冊的方法,註冊handler的就不需再多描述,另一個是註冊Block的回調形式,由於有時候可能會須要一些簡單的Url攔截,去作一些事情,這裏面的Block須要返回一個request對象,這是由於,若是Block沒有對request的回調作處理,Router應該處理調用者的回調問題,不然就會出現調用者設置了回調的Block而沒有人調用回來,這樣就尷尬了。

/** 註冊一個route表達式並與一個block處理相關聯 @param routeHandlerBlock block用以處理匹配route表達式的url的請求 @param route url的路由表達式,支持正則表達式的分組,例如app://login/:phone({0,9+})是一個表達式,:phone表明該路徑值對應的key,能夠在WLRRouteRequest對象中的routeParameters中獲取 */
-(void)registerBlock:(WLRRouteRequest *(^)(WLRRouteRequest * request))routeHandlerBlock forRoute:(NSString *)route;
/** 註冊一個route表達式並與一個block處理相關聯 @param routeHandlerBlock handler對象用以處理匹配route表達式的url的請求 @param route url的路由表達式,支持正則表達式的分組,例如app://login/:phone({0,9+})是一個表達式,:phone表明該路徑值對應的key,能夠在WLRRouteRequest對象中的routeParameters中獲取 */
-(void)registerHandler:(WLRRouteHandler *)handler forRoute:(NSString *)route;

/** 檢測url是否可以被處理,不包含中間件的檢查 @param url 請求的url @return 是否能夠handle */
-(BOOL)canHandleWithURL:(NSURL *)url;
/** 處理url請求 @param URL 調用的url @param primitiveParameters 攜帶的原生對象 @param targetCallBack 傳給目標對象的回調block @param completionBlock 完成路由中轉的block @return 是否可以handle */
-(BOOL)handleURL:(NSURL *)URL primitiveParameters:(NSDictionary *)primitiveParameters targetCallBack:(void(^)(NSError *, id responseObject))targetCallBack withCompletionBlock:(void(^)(BOOL handled, NSError *error))completionBlock;

梳理總結:

從以上咱們規劃的幾個類的接口,咱們能夠清楚的看到Router工做的流程。

  1. 首先實例化Router對象
  2. 實例化Handler或者是Block,經過Router的註冊接口使得一個Url的匹配表達式對應一個Handler或者是一個block
  3. Router內部會將Url的表達式造成一個Matcher對象進行保存,對應的Handler或處理的Block會與Matcher一一對應,怎麼對應呢?應該使用路由表達式進行關聯
  4. Router經過handle方法,接收一個Url的請求,內部遍歷全部的Matcher對象,將Url和參數轉換爲Request對象,若是能轉換爲Request對象則說明能匹配,若是不能則說明該Url不能被路由實體處理
  5. 拿到Request對象之後,則根據Matcher對應的路由表達式找到對應的Handler或者是Block
  6. 根據Handler的幾個關鍵方法,傳入Request對象,按照順序完成處理邏輯的觸發,最後若是有request當中包含有目標的回調,則將處理結果經過回調的Block響應給調用方
  7. Handler完成處理後,Router完成本次路由請求

WLRRoute

Tips: 不少開發者把敏捷開發當作來了需求無論三七二十一,一把梭子就是幹,不斷寫不斷改。