什么是移動端路由層:
路由層的概念在服務端是指url請求的分層解析,將一個請求分發到對應的應用處理程序。移動端的路由層指的是將諸如App內頁面訪問、H5與App訪問的訪問請求和App間的訪問請求,進行分發處理的邏輯層。
移動端路由層需要解決的問題:
- 對外部提供遠程訪問的功能,實現跨應用調用響應,包括H5應用調用、其他App應用調用、系統訪問調用等
- 原生頁面、模塊、組件等定義,統稱為資源(Resource),在跨應用調用和路由層在不同端實現的業務表現需要一致的前提下,需要對資源進行定義,在路由提供內部請求分發的時候則可以提供不依賴對外進行資源定義的功能
- 外部調用如何使用統一標示(Uniform)進行表示資源
- 如何在移動端統一定義訪問請求的過程,從而達成移動端與web端的統一性
- 如何更好的兼容iOS、Android的系統訪問機制、App鏈接協議、web端路由機制與前端開發規范等
- 如何兼容各平台(Android、iOS)App頁面導航機制
- 如何解決安全訪問問題
- 移動端在客戶端進行動態配置
-
移動端路由所應用的場景:
- H5頁面與App原生頁面、模塊與組件的交互
- App與App之間的相互訪問
- App內部頁面跳轉、模塊調度與組件加載等
- 推送與通知系統解除硬編碼的邏輯,動態訪問原生資源,更好的支持通過通知和推送完成動態頁面訪問和邏輯執行
- Extension等動態調用主App的資源
- App實現更復雜的架構MVVM或者是VIPER架構,提供解除業務相互依賴的能力
- 以組件化為目的的工程改造,隔離各個業務,以制作單獨的組件
對外如何定義資源
在路由提供對外的資源請求轉發的時候,因為要照顧到其他應用的請求表達方式,比如H5應用或者是其他App的應用的訪問請求,定義單純依賴業務的資源定義就顯得有些必要了。
舉個例子,一個H5的商品詳情頁,被用戶分享,當其他用戶看到這個H5應用的頁面的時候,點擊,如果該用戶裝了有對應這個H5商品詳情頁的App的時候,應該跳轉到該App的原生商品詳情頁,如果沒有安裝則加載這個H5頁面,在這個過程中,H5的頁面是通過URL進行標識的,那這個URL的標識也應該對照到App的原生頁面,但是要只依賴業務標識而不能依賴App的代碼實現,比如說iOS端的App的商品詳情頁叫做DetailViewController,那這個URL是不能包含這個名字的,Android端可能叫DetailActivity,如果不單純依賴業務,那H5應用就要根據平台來重新發送不同的資源定義的URL,就造成了硬編碼問題,H5應用要依賴App的實現邏輯,如果有一天,原生App的頁面代碼實現變成了GoodDetailViewController,所有依賴DetailViewController這個資源標示的H5應用都要進行更改,就會出現問題。所以路由層的設計應該具備根據業務定義來映射App內的資源定義。
常常在設計路由層的時候,我們會更加關注通信行為的細節、如何改進特定通信機制的表現,常常忽略了一個事實,那就是改變應用程序的互動風格比改變協議對整體的表現有更大的影響。
所謂資源,就是一個應用程序提供的不可分割的服務,從這個層面上看,App的資源即是一種實體的存在,可以進行獲取和訪問,必須進行良好的表示,在有些必要的情況下,必須是獨一無二的識別符來表示一個應用程序所提供的服務是什么。表示資源我們更傾向於使用URI進行標示,因為移動端沒有一個橫跨iOS、Android、Web后端與H5應用的資源標示方式,而URI是web service模式的資源通用表示方式,包括后面將要提到的Android與iOS統一支持的universal link(通用鏈接)也是借用URI的概念,App路由層所涉及到的資源表示方法還是建議使用URI的標示方式,同時更應該借鑒RESTful風格來架構這一層,原因是App的頁面、組件或者說一整套功能性的服務是非常復雜的,相比於H5有更加多與復雜的交互,相比於后端存在更加苛刻的網絡環境與多設備多平台的技術考量,所以URI在標示橫跨多平台多版本的資源的情況下,能夠更好的表示某一個資源實體而不是資源的表現形式。
在Android與iOS系統中,均支持URL Scheme,所以資源的標示通常會是這個樣子:
AppScheme://path //例如qq app: mqq:// //支付寶: 支付寶alipay://
如果協議是Http或者是Https標示的是Web應用或者是H5應用,你的App也是一個與WebService相同級別的應用,那么URL的協議部分應該是App的唯一標示符,這個主機部分和路徑部分則需要我們使用RESTful的風格進行重新設計。
重點是如何標示資源,例如表示App中的登錄服務,那可以表示為:
AppScheme://host/login
host為主機部分,在一般的WebService上,在業務表現形式上一般是比較大的業務條線的標示,比方說 https://news.sina.com.cn ,主機部分是news.sina.com.cn,則標示新浪新聞這條業務線,在App內你的業務條線也應該是清晰的,假如移動App的主UI框架是Tab分欄,那么每個Tab分欄就是你的業務條線的分割,這點跟WebService應用的導航欄類似,App的資源大多是頁面或者是可交互的組件,與UI關系比較大,假如你的Tab有四個:分別叫首頁、商品、發現、我的,那么我們可以這樣定義:
AppScheme://index/ AppScheme://goods/ AppScheme://discover/ AppScheme://user/
當然,也可以有額外的定義,比方說App有Api服務,Api提供實現一個純數據同步的服務標示,那么這個URL可以設計為:
AppScheme://api-asycn/collections?action='insert'&value='***'&&userUoken='*******'&&source="https//***.***.com/collection.html"
由於RESTful風格強調URL的資源標示而不是行為表示,所以”AppScheme://api-asycn/collections” 是一個良好的資源標示,表示了一個收藏功能的實體,而”?”后面的GET方式的參數實際上是不得已為之,因為實際上沒有Web的http request的實體,所以只能勉強借助GET參數來替代RESTful風格中強調的Accept和Content-Type字段來標示表現層的行為描述。
當然action與value這樣的描述可以根據業務划分,但是重點是要用參數表現形式。
iOS與Android的系統訪問機制、統一的鏈接協議
蘋果的URL Scheme由來已久: Apple URLScheme,Android平台同樣也實現了該功能,使得App能夠在沙盒機制的前提下,能夠相互調用聲明過的服務。由於URL Scheme天生沒有返回的callBack機制,著名的App Drafts的作者聯合Marco Arment、Justin Williams 等人開發了x-callback-URL來做出統一跳轉的協議: x-callback-url,在此不過多表述。
利用URL-Scheme的機制,可以定義如下的統一鏈接協議:
- 協議部分來標示App應用
- 主機Host部分用於標示業務線或者是應用提供的划分好的服務實體,比方說index、discover是業務條線,api-asycn是對外提供的api,pushService是App內部的推送服務等。
- 路徑部分則可以是細分的頁面、組件或者服務的標示
- 參數定義有一些是必要的,比如說action來標示動作,比方說可以使用get標示獲取、insert增加,userToken表示安全的用戶令牌,source表示來源,當然像是userToken與source這些都是路由層需要進行解析和驗證的,而action則是業務相關的參數,這一點在路由曾設計的時候需要進行詳細區分
統一訪問請求過程

整個統一的訪問請求過程如圖,關於最后的response返回有一些說明:
在WebService的工作棧中,http的request與response是有標准協議規范的,而App的路由層只是套用的URI的資源標示和RESTFul風格的交互,沒有標准的request和response結構,這部分實現在App內部,response對外部調用系統而言關心的有三個重要元素,資源狀態碼、返回值與錯誤,在路由層在響應外部調用的時候需要返回這三種元素
路由層邏輯結構

路由層安全
路由層的安全包含兩個方面:
- 跨應用時,需要注意注入攻擊,做到敏感參數加密防篡改,同時需要注意路由層應提供能夠實現風控的機制
- 跨業務系統的時候,需要開啟會話訪問機制,通過令牌或者是session會話等來實現路由層身份認證
路由層實現
敬請期待下一篇文章:《一步步構建iOS路由》番外:App孤島、API經濟與App開放性討論
什么叫App孤島
移動操作系統中的App一般都采用沙盒機制來嚴格限制訪問權限,App與App之間是不通的,用戶往往會安裝大量的App,比方說找吃飯的地方是大眾點評,聊天是微信,地圖是高德等等,那么我們想象一下沒有URL Scheme的世界,你在大眾點評上找到了一個好吃的地方,然后需要切換到高德去找找在哪,然后腦子記錄下來地址然后在微信上發給你的朋友,這么一個過程中,眾多App之間是不能傳遞信息和相互協作的,那一個個App就成了信息孤島,給用戶帶來極大的不便,而實現了URL Scheme的App一般都是大廠,用戶過億,給上億人帶來了方便。打破App孤島
本質上URL Scheme是操作系統支持的,也就是說,打破App孤島,必須過操作系統這一關,而無論是第三方開發者還是Apple與Google都在努力打破信息孤島。
Apple與Google分別在iOS9與Android M支持了universal link以打通H5應用和原生應用的屏障。
Apple則在iOS操作系統中通過Spotlight應用內搜索、AppGroups、AppExtension、ShareExtension與SiriKit等打破原生應用之間的信息屏障。
Google則通過PWA希望替代原生應用來實現大一統。
第三方開發者們也積極推動着這一趨勢。比如說:
前面提到的著名的App Drafts的作者聯合Marco Arment、Justin Williams 等人開發了x-callback-URL來做出統一跳轉的協議: x-callback-url,希望大部分App開發者能夠響應號召,更好的進行開發。
國內的一些深度鏈接的開發者平台 DeepShare - Share your App with the world
錘子手機開源的onestep等等。
作為一名開發者,構建安全高效而開放的路由實際上不僅僅滿足技術架構的需求更能為打破App孤島,更好的發展移動端生態做出貢獻。什么叫做API經濟
API經濟是基於API所產生的經濟活動的總和,在當今發展階段主要包括API業務,以及通過API進行的業務功能、性能等方面的商業交易。API經濟是當今各行業(零售、金融、物聯網、醫療等)中驅動數字變革的主要力量。 ———百度百科
為什么這里需要談到API經濟呢?我們都知道經濟學的第一要務是效率優先原則,就像上面我們聊到的App孤島,在日益便利的移動化時代,實際上降低了信息共享的效率,而增加了用戶的操作成本,則會阻礙這個平台上用戶的活躍度,那上層利用移動平台的可能性就會被限制。比如,二維碼和NFC解決了pos終端、商家與支付App之間的信息共享問題,就導致了繁盛的線下支付經濟,同樣的道理,各系統之間無論是App、WebServices或者是其他應用能夠開放API則會形成平台或者產業上的信息共享的規模效應,則會形成良性發展。
作為App開發者,你需要實現路由這一層,才能夠支持跨應用之間的調用,才能放開你想開發的API。
如果一個App的后端Services能夠和App一起開放API,那則更加具有優勢。比方說微信,如果開放了收藏的WebService API接口,同時微信App也開放URLScheme的收藏接口,那么無論在瀏覽器、手機中都能無縫實現隨時隨地的收藏一切內容,極大的方便用戶。
App開放性討論
這個環節主要是討論開放的時候要注意哪些:
- App類型(決定要不要開放)
- 路由安全(決定開放程度)
- 開放時機
未完,希望大家多多評論,一起討論。
接上一篇移動端路由層設計
為啥要說iOS路由呢?
路由層其實在邏輯上的設計都是一樣的,關於對界面跳轉的實現部分卻與Android平台和iOS平台上的導航機制有着非常緊密的關系,Android操作系統有着天然的架構優勢,Intent機制可以協助應用間的交互與通訊,是對調用組件和數據傳遞的描述,本身這種機制就解除了代碼邏輯和界面之間的依賴關系,只有數據依賴。而iOS的界面導航和轉場機制則大部分依賴UI組件各自的實現,所以如何解決這個問題,iOS端路由的實現則比較有代表性。
其實說白一點,路由層解決的核心問題就是原來界面或者組件之間相互調用都必須相互依賴,需要導入目標的頭文件、需要清楚目標對象的邏輯,而現在全部都通過路由中轉,只依賴路由,或者依靠一些消息傳遞機制連路由都不依賴。其次,路由的核心邏輯就是目標匹配,對於外部調用的情況來說,URL如何匹配Handler是最為重要的,匹配就必然用到正則表達式。了解這些關鍵點以后就有了設計的目的性,let‘s do it~
設計類圖:

這里面有如下幾個類:
- WLRRouteRequest,路由層的請求,無論是跨應用的外部調用還是內部調用,最后都形成一個路由請求,該請求包含了URL上的queryparameters和路徑參數,還有內部調用時直接傳入的原生參數,還有請求發起者對目標預留的回調block
- WLRRouteHandler,路由層的handler處理,handler接收一個WLRRouteRequest對象,來完成是否是界面跳轉,還是組件加載,還是內部邏輯
- WLRRouter,路由核心對象,內部持有注冊的Handler,比方說負責界面跳轉的Handler,負責組件加載的Handler,負責API的Handler等等,路由的作用就是將外部調用傳入的URL或者是內部調用傳入的target,在內部匹配上對應的handler,然后調用生命周期方法,完成處理過程,當然,圖中還有route的中間件,實際上是預留AOP的口子,方面后期擴展
- WLRRouteMatcher,用以處理外部調用的URL是否能與預設的正則表達式匹配,在WLRRouter中,每一次注冊一個handler,都會用一個URL匹配的表達式生成一個WLRRouteMatcher
- WLRRegularExpression,繼承NSRegularExpression,用以匹配URL,WLRRouteMatcher內部有一個WLRRegularExpression對象,WLRRouteMatcher接受一個URL,會使用WLRRegularExpression生成一個WLRMatchResult對象,來確定是否匹配成功,如果匹配成果則將URL上的路徑參數給取出來
- WLRMatchResult,用以描述WLRRegularExpression的匹配結果,包含路徑參數
工作流程:
- App啟動實例化WLRRouter對象
- 實例化WLRRouteHandler對象
- WLRRouter對象掛載WLRRouteHandler實例與URL的表達式相對應,WLRRouter內部生成一個WLRRouteMatcher對象,與URL的表達式相對應
- 外部調用的URL和callback傳入WLRRouter對象
- WLRRouter對象遍歷內部持有的URL的匹配表達式,並找到每一個WLRRouteMatcher對象,將URL傳入看是否能返回WLRRouteRequest對象
- 將WLRRouteRequest對象傳入對應的WLRRouteHandler對象
- WLRRouteHandler對象根據WLRRouteRequest尋找到TargetViewController和SourceViewController,在生命周期函數里,完成參數傳遞與視圖轉場
WLRRouteRequest:
了解了以上,我們從WLRRouteRequest入手。
其實WLRRouteRequest跟NSURLRequest差不多,不過WLRRouteRequest繼承NSObject,實現NSCopying協議,大概如下:
#import <Foundation/Foundation.h> @interface WLRRouteRequest : NSObject<NSCopying> //外部調用的URL @property (nonatomic, copy, readonly) NSURL *URL; //URL表達式,比方說調用登錄界面的表達式可以為:AppScheme://user/login/138********,那URL的匹配表達式可以是:/login/:phone([0-9]+),路徑必須以/login開頭,后面接0-9的電話號碼數字,當然你也可以直接把電話號碼的正則匹配寫全 @property(nonatomic,copy)NSString * routeExpression; //如果URL是AppScheme://user/login/138********?/callBack="",那么這個callBack就出現在這 @property (nonatomic, copy, readonly) NSDictionary *queryParameters; //這里面會出現{@"phone":@"138********"} @property (nonatomic, copy, readonly) NSDictionary *routeParameters; //這里面存放的是內部調用傳遞的原生參數 @property (nonatomic, copy, readonly) NSDictionary *primitiveParams; //自動檢測竊取回調的callBack 的Url @property (nonatomic, strong) NSURL *callbackURL; //目標的viewcontrolller或者是組件可以通過這個 @property(nonatomic,copy)void(^targetCallBack)(NSError *error,id responseObject); //用以表明該request是否被消費 @property(nonatomic)BOOL isConsumed; //簡便方法,用以下標法取參數 - (id)objectForKeyedSubscript:(NSString *)key; //初始化方法 -(instancetype)initWithURL:(NSURL *)URL routeExpression:(NSString *)routeExpression routeParameters:(NSDictionary *)routeParameters primitiveParameters:(NSDictionary *)primitiveParameters targetCallBack:(void(^)(NSError * error,id responseObject))targetCallBack; -(instancetype)initWithURL:(NSURL *)URL; //默認完成目標的回調 -(void)defaultFinishTargetCallBack; @end
NSURLRequest其實應該是個值類型的對象,所以實現拷貝協議,該對象的實現部分沒有什么可講的,對照源代碼查閱即可。
WLRRouteHandler
#import <Foundation/Foundation.h> @class WLRRouteRequest; @interface WLRRouteHandler : NSObject //即將handle某一個請求 - (BOOL)shouldHandleWithRequest:(WLRRouteRequest *)request; //根據request取出調用的目標視圖控制器 -(UIViewController *)targetViewControllerWithRequest:(WLRRouteRequest *)request; //根據request取出來源的視圖控制器 -(UIViewController *)sourceViewControllerForTransitionWithRequest:(WLRRouteRequest *)request; //開始進行轉場 -(BOOL)transitionWithRequest:(WLRRouteRequest *)request error:(NSError *__autoreleasing *)error; @end
當WLRRouter對象完成了URL的匹配生成Request,並尋找到Handler的時候,首先會調用- (BOOL)shouldHandleWithRequest:(WLRRouteRequest *)request;
,來確定handler是否願意處理,如果願意,則調用-(BOOL)transitionWithRequest:(WLRRouteRequest *)request error:(NSError *__autoreleasing *)error;
,內部則通過便利方法獲取targetViewController與SourceViewController,然后進行轉場,核心方法的實現為:
-(BOOL)transitionWithRequest:(WLRRouteRequest *)request error:(NSError *__autoreleasing *)error{ UIViewController * sourceViewController = [self sourceViewControllerForTransitionWithRequest:request]; UIViewController * targetViewController = [self targetViewControllerWithRequest:request]; if ((![sourceViewController isKindOfClass:[UIViewController class]])||(![targetViewController isKindOfClass:[UIViewController class]])) { *error = [NSError WLRTransitionError]; return NO; } if (targetViewController != nil) { targetViewController.wlr_request = request; } if ([self preferModalPresentationWithRequest:request]||![sourceViewController isKindOfClass:[UINavigationController class]]) { [sourceViewController presentViewController:targetViewController animated:YES completion:nil]; } else if ([sourceViewController isKindOfClass:[UINavigationController class]]){ UINavigationController * nav = (UINavigationController *)sourceViewController; [nav pushViewController:targetViewController animated:YES]; } return YES; } - (BOOL)preferModalPresentationWithRequest:(WLRRouteRequest *)request;{ return NO; }
這里根據SourceController的類型進行判斷,其實request對象的信息足夠可以判斷目標視圖應該如何打開,從本質上來講,URL的匹配表達式是跟業務強關聯的也是跟UI交互邏輯強關聯的,transitionWithRequest方法實現里,你大可以繼承一下,然后重寫轉場過程,甚至你可以在這自己設置iOS7自定義的轉場,提供動畫控制器和實現轉場協議的對象,進而可以整體的控制Appp內部的實現。
WLRRegularExpression
該類繼承NSRegularExpression
#import <Foundation/Foundation.h> @class WLRMatchResult; @interface WLRRegularExpression : NSRegularExpression //傳入一個URL返回一個匹配結果 -(WLRMatchResult *)matchResultForString:(NSString *)string; //根據一個URL的表達式創建一個WLRRegularExpression實例 +(WLRRegularExpression *)expressionWithPattern:(NSString *)pattern; @end
該對象主要的功能是將一個URL傳入查看是否匹配,並且將表達式上聲明的路徑參數從URL上取下來。
比說,我們設置的URL匹配的表達式是: login/:phone([0-9]+),那AppScheme://user/login/138** 這樣的URL應該是匹配,並且將138的手機號取出來,對應到phone上,這個過程必須用到正則表達式的分組提取子串的功能,:phone是約定好的提取子串的值對應的key的名字,其實這個url的正則表達式應該是: /login/([0-9]+)$,那么WLRRegularExpression對象需要知道需要提取所有子串的key還有將URL匹配的表達式轉換為真正的正則表達式。
-(instancetype)initWithPattern:(NSString *)pattern options:(NSRegularExpressionOptions)options error:(NSError * _Nullable __autoreleasing *)error{ //初始化方法中將URL匹配的表達式pattern轉換為真正的正則表達式 NSString *transformedPattern = [WLRRegularExpression transfromFromPattern:pattern]; //用轉化后的結果初始化父類 if (self = [super initWithPattern:transformedPattern options:options error:error]) { //同時將需要提取的子串的值的Key保存到數組中 self.routerParamNamesArr = [[self class] routeParamNamesFromPattern:pattern]; } return self; } //轉換為正則表達式 +(NSString*)transfromFromPattern:(NSString *)pattern{ //將pattern拷貝 NSString * transfromedPattern = [NSString stringWithString:pattern]; //利用:[a-zA-Z0-9-_][^/]+這個正則表達式,將URL匹配的表達式的子串key提取出來,也就是像 /login/:phone([0-9]+)/:name[a-zA-Z-_]這樣的pattern,需要將:phone([0-9]+)和:name[a-zA-Z-_]提取出來 NSArray * paramPatternStrings = [self paramPatternStringsFromPattern:pattern]; NSError * err; //再根據:[a-zA-Z0-9-_]+這個正則表達式,將帶有提取子串的key全部去除,比如將:phone([0-9]+)去除:phone改成([0-9]+) NSRegularExpression * paramNamePatternEx = [NSRegularExpression regularExpressionWithPattern:WLRRouteParamNamePattern options:NSRegularExpressionCaseInsensitive error:&err]; for (NSString * paramPatternString in paramPatternStrings) { NSString * replaceParamPatternString = [paramPatternString copy]; NSTextCheckingResult * foundParamNamePatternResult =[paramNamePatternEx matchesInString:paramPatternString options:NSMatchingReportProgress range:NSMakeRange(0, paramPatternString.length)].firstObject; if (foundParamNamePatternResult) { NSString *paramNamePatternString =[paramPatternString substringWithRange: foundParamNamePatternResult.range]; replaceParamPatternString = [replaceParamPatternString stringByReplacingOccurrencesOfString:paramNamePatternString withString:@""]; } if (replaceParamPatternString.length == 0) { replaceParamPatternString = WLPRouteParamMatchPattern; } transfromedPattern = [transfromedPattern stringByReplacingOccurrencesOfString:paramPatternString withString:replaceParamPatternString]; } if (transfromedPattern.length && !([transfromedPattern characterAtIndex:0] == '/')) { transfromedPattern = [@"^" stringByAppendingString:transfromedPattern]; } //最后結尾要用$符號 transfromedPattern = [transfromedPattern stringByAppendingString:@"$"]; //最后會將/login/:phone([0-9]+)轉換為login/([0-9]+)$ return transfromedPattern; }
在Matcher對象匹配一個URL的時候
-(WLRMatchResult *)matchResultForString:(NSString *)string{ //首先通過自身方法將URL進行匹配得出NSTextCheckingResult結果的數組 NSArray * array = [self matchesInString:string options:0 range:NSMakeRange(0, string.length)]; WLRMatchResult * result = [[WLRMatchResult alloc]init]; if (array.count == 0) { return result; } result.match = YES; NSMutableDictionary * paramDict = [NSMutableDictionary dictionary]; //遍歷NSTextCheckingResult結果 for (NSTextCheckingResult * paramResult in array) { //再便利根據初始化的時候提取的子串的Key的數組 for (int i = 1; i<paramResult.numberOfRanges&&i <= self.routerParamNamesArr.count;i++ ) { NSString * paramName = self.routerParamNamesArr[i-1]; //將值取出,然后將key和value放入到paramDict NSString * paramValue = [string substringWithRange:[paramResult rangeAtIndex:i]]; [paramDict setObject:paramValue forKey:paramName]; } } //最后賦值給WLRMatchResult對象 result.paramProperties = paramDict; return result; }
核心代碼總共80多行,源碼大家可以詳閱
WLRRouteMatcher
#import <Foundation/Foundation.h> @class WLRRouteRequest; @interface WLRRouteMatcher : NSObject //傳入URL匹配的表達式,獲取一個matcher實例 +(instancetype)matcherWithRouteExpression:(NSString *)expression; //傳入URL,如果能匹配上,則生成WLRRouteRequest對象,同時將各種參數解析好交由WLRRouteRequest攜帶 -(WLRRouteRequest *)createRequestWithURL:(NSURL *)URL primitiveParameters:(NSDictionary *)primitiveParameters targetCallBack:(void(^)(NSError *, id responseObject))targetCallBack; @end
屬性有如下:
//scheme @property(nonatomic,copy) NSString * scheme; //WLRRegularExpression的實例 @property(nonatomic,strong)WLRRegularExpression * regexMatcher; //匹配的表達式 @property(nonatomic,copy)NSString * routeExpressionPattern;
初始化方法:
-(instancetype)initWithRouteExpression:(NSString *)routeExpression{ if (![routeExpression length]) { return nil; } if (self = [super init]) { //將scheme與path部分分別取出 NSArray * parts = [routeExpression componentsSeparatedByString:@"://"]; _scheme = parts.count>1?[parts firstObject]:nil; _routeExpressionPattern =[parts lastObject]; //將path部分當做URL匹配表達式生成WLRRegularExpression實例 _regexMatcher = [WLRRegularExpression expressionWithPattern:_routeExpressionPattern]; } return self; }
匹配方法:
-(WLRRouteRequest *)createRequestWithURL:(NSURL *)URL primitiveParameters:(NSDictionary *)primitiveParameters targetCallBack:(void (^)(NSError *, id))targetCallBack{ NSString * urlString = [NSString stringWithFormat:@"%@%@",URL.host,URL.path]; if (self.scheme.length && ![self.scheme isEqualToString:URL.scheme]) { return nil; } //調用self.regexMatcher將URL傳入,獲取WLRMatchResult結果,看是否匹配 WLRMatchResult * result = [self.regexMatcher matchResultForString:urlString]; if (!result.isMatch) { return nil; } //如果匹配,則將result.paramProperties路徑參數傳入,初始化一個WLRRouteRequest實例 WLRRouteRequest * request = [[WLRRouteRequest alloc]initWithURL:URL routeExpression:self.routeExpressionPattern routeParameters:result.paramProperties primitiveParameters:primitiveParameters targetCallBack:targetCallBack]; return request; }
WLRRouter
@class WLRRouteRequest; @class WLRRouteHandler; @interface WLRRouter : NSObject //注冊block回調的URL匹配表達式,可用作內部調用 -(void)registerBlock:(WLRRouteRequest *(^)(WLRRouteRequest * request))routeHandlerBlock forRoute:(NSString *)route; //注冊一個WLRRouteHandler對應的URL匹配表達式route -(void)registerHandler:(WLRRouteHandler *)handler forRoute:(NSString *)route; //判斷url是否可以被handle -(BOOL)canHandleWithURL:(NSURL *)url; -(void)setObject:(id)obj forKeyedSubscript:(NSString *)key; -(id)objectForKeyedSubscript:(NSString *)key; //調用handleURL方法,傳入URL、原生參數和targetCallBack和完成匹配的completionBlock -(BOOL)handleURL:(NSURL *)URL primitiveParameters:(NSDictionary *)primitiveParameters targetCallBack:(void(^)(NSError *, id responseObject))targetCallBack withCompletionBlock:(void(^)(BOOL handled, NSError *error))completionBlock;
在實現部分,有三個屬性:
//每一個URL的匹配表達式route對應一個matcher實例,放在字典中 @property(nonatomic,strong)NSMutableDictionary * routeMatchers; //每一個URL匹配表達式route對應一個WLRRouteHandler實例 @property(nonatomic,strong)NSMutableDictionary * routeHandles; //每一個URL匹配表達式route對應一個回調的block @property(nonatomic,strong)NSMutableDictionary * routeblocks;
在Route掛在Handler和回調的block的時候:
-(void)registerBlock:(WLRRouteRequest *(^)(WLRRouteRequest *))routeHandlerBlock forRoute:(NSString *)route{ if (routeHandlerBlock && [route length]) { //首先添加一個WLRRouteMatcher實例 [self.routeMatchers setObject:[WLRRouteMatcher matcherWithRouteExpression:route] forKey:route]; //刪除route對應的handler對象 [self.routeHandles removeObjectForKey:route]; //將routeHandlerBlock和route存入對應關系的字典中 self.routeblocks[route] = routeHandlerBlock; } } -(void)registerHandler:(WLRRouteHandler *)handler forRoute:(NSString *)route{ if (handler && [route length]) { //首先生成route對應的WLRRouteMatcher實例 [self.routeMatchers setObject:[WLRRouteMatcher matcherWithRouteExpression:route] forKey:route]; //刪除route對應的block回調 [self.routeblocks removeObjectForKey:route]; //設置route對應的handler self.routeHandles[route] = handler; } }
接下來完善handle方法:
-(BOOL)handleURL:(NSURL *)URL primitiveParameters:(NSDictionary *)primitiveParameters targetCallBack:(void(^)(NSError *error, id responseObject))targetCallBack withCompletionBlock:(void(^)(BOOL handled, NSError *error))completionBlock{ if (!URL) { return NO; } NSError * error; WLRRouteRequest * request; __block BOOL isHandled = NO; //遍歷routeMatchers中的WLRRouteMatcher對象,將URL傳入對象,看是否能得到WLRRouteRequest對象 for (NSString * route in self.routeMatchers.allKeys) { WLRRouteMatcher * matcher = [self.routeMatchers objectForKey:route]; WLRRouteRequest * request = [matcher createRequestWithURL:URL primitiveParameters:primitiveParameters targetCallBack:targetCallBack]; if (request) { //如果得到WLRRouteRequest對象,說明匹配成功,則進行handler的生命周期函數調用或是這block回調 isHandled = [self handleRouteExpression:route withRequest:request error:&error]; break; } } if (!request) { error = [NSError WLRNotFoundError]; } //在調用完畢block或者是handler的生命周期方法以后,回調完成的completionHandler [self completeRouteWithSuccess:isHandled error:error completionHandler:completionBlock]; return isHandled; } //根據request進行handler的生命周期函數調用或者是block回調 -(BOOL)handleRouteExpression:(NSString *)routeExpression withRequest:(WLRRouteRequest *)request error:(NSError *__autoreleasing *)error { id handler = self[routeExpression]; //self.routeHandles和self.routeblocks拿到route對應的回調block或者是handler實例 if ([handler isKindOfClass:NSClassFromString(@"NSBlock")]) { WLRRouteRequest *(^blcok)(WLRRouteRequest *) = handler; //調用回調的block WLRRouteRequest * backRequest = blcok(request); //判斷block里面是否消費了此request,如果沒有而目標設置了目標回調targetCallBack,那么在此進行默認回調 if (backRequest.isConsumed==NO) { if (backRequest.targetCallBack) { dispatch_async(dispatch_get_main_queue(), ^{ backRequest.targetCallBack(nil,nil); }); } } return YES; } else if ([handler isKindOfClass:[WLRRouteHandler class]]){ //拿到url對應的handler對象后,先調用handler的shouldHandleWithRequest方法,如果返回YES,則調用進行轉場的transitionWithRequest方法 WLRRouteHandler * rHandler = (WLRRouteHandler *)handler; if (![rHandler shouldHandleWithRequest:request]) { return NO; } return [rHandler transitionWithRequest:request error:error]; } return YES; }
以上我們可以看到,Route將匹配的邏輯單獨封裝到WLRRouteMatcher對象中,將匹配后的結果生成WLRRouteRequest實例以攜帶足夠完整的數據,同時將真正處理視圖控制器的轉場或者是組件的加載或者是未來可能拓展的handle業務封裝到WLRRouteHandler實例中,匹配邏輯對應的處理邏輯干凈分離,匹配邏輯可單獨塑造業務匹配,處理邏輯可以通過繼承擴展或者沖洗WLRRouteHandler的生命周期函數來更好的處理回調業務。如果WLRRouteHandler不能提供足夠多的擴展性,則可以使用block回調最大限度的進行擴展。
以上,就是路由部分的整體實現。
轉場的擴展
在WLRRouteHandler中,其實我們可以單獨控制路由經過的頁面跳轉的轉場。
-(UIViewController *)targetViewControllerWithRequest:(WLRRouteRequest *)request{ } -(UIViewController *)sourceViewControllerForTransitionWithRequest:(WLRRouteRequest *)request{ } -(BOOL)transitionWithRequest:(WLRRouteRequest *)request error:(NSError *__autoreleasing *)error{ }
這樣的生命周期函數是不是很像UIViewControllerContextTransitioning轉場上下文的協議的設定?- (nullable __kindof UIViewController *)viewControllerForKey:(UITransitionContextViewControllerKey)key;
方法使上下文提供目標控制器和源控制器,其實在handler中你完全可以自定義一個子類,在transitionWithRequest方法里,設置遵守UIViewControllerTransitioningDelegate的代理,然后在此提供遵守 UIViewControllerAnimatedTransitioning的動畫控制器,然后自定義轉場上下文,實現自定義UI轉場,而對應的匹配邏輯是與此無關的,我們就可以在路由曾控制全局的頁面轉場效果。對自定義轉場不太熟悉的同學請移步我之前的文章:
ContainerViewController的ViewController 轉場
路由的安全
有兩個方面可以去做
- WLRRouteHandler實例中,
-(BOOL)shouldHandleWithRequest:(WLRRouteRequest *)request
中可以檢測request中的參數,比方說效驗source或者是效驗業務參數完整等 - WLRRouter實例中handleURL方法,將在隨后的WLRRoute的0.0.2版本中加入中間件的支持,就是在找到handler之前,將按照中間件注冊的順序回調中間件,而我們可以在中間件中實現風控業務、認證機制、加密驗簽等等
路由的效率
目前我們實現的路由是一個同步阻塞型的,在處理並發的時候可能會出現一些問題,或者是在注冊比較多的route表達式以后,遍歷和匹配的過程會損耗性能,比較好的實現方式是,將Route修改成異步非阻塞型的,但是API全部要換成異步API,起步我們先把同步型的搞定,隨后慢慢提供異步版本的路由~路由的使用
在大部分App實踐MVVM架構或者更為復雜的VIPER架構的時候,除了迫切需要一個比較解耦的消息傳遞機制,如何更好的剝離目標實體的獲取和配合UIKit這一層的轉場邏輯是一項比較復雜的挑戰,路由實際上是充當MVVM的ViewModel中比較解耦的目標獲取邏輯和VIPER中Router層,P與V的調用全部靠Router轉發。
在實施以組件化為目的的工程化改造中,如何抽離單獨業務為組件,比較好的管理業務與業務之間的依賴,就必須使用一個入侵比較小的Route,WLRRoute入侵的地方在於WLRRouteHandler的transitionWithRequest邏輯中,通過一個UIViewController的擴展,給 targetViewController.wlr_request = request;設置了WLRRouteRequest對象給目標業務,但雖然如此,你依舊可以重寫WLRRouteHandler的transitionWithRequest方法,來構建你自己參數傳遞方式,這一點完全取決於你如何更好的使得業務無感知而使用路由。
最后附上代碼地址:
喜歡的來個星吧…
https://github.com/Neojoke/WLRRoute
12.27更新:
感謝這位同學,寫了一篇討論性的 文章,對我啟發很大,我尊敬以及欣賞能夠深入思考並且願意分享自己的idea的人。
轉自:http://www.jianshu.com/p/3a902f274a3d