隨着用戶的需求愈來愈多,對App的用戶體驗也變的要求愈來愈高。爲了更好的應對各類需求,開發人員從軟件工程的角度,將App架構由原來簡單的MVC變成MVVM,VIPER等複雜架構。更換適合業務的架構,是爲了後期能更好的維護項目。javascript
可是用戶依舊不滿意,繼續對開發人員提出了更多更高的要求,不只須要高質量的用戶體驗,還要求快速迭代,最好一天出一個新功能,並且用戶還要求不更新就能體驗到新功能。爲了知足用戶需求,因而開發人員就用H5,ReactNative,Weex等技術對已有的項目進行改造。項目架構也變得更加的複雜,縱向的會進行分層,網絡層,UI層,數據持久層。每一層橫向的也會根據業務進行組件化。儘管這樣作了之後會讓開發更加有效率,更加好維護,可是如何解耦各層,解耦各個界面和各個組件,下降各個組件之間的耦合度,如何能讓整個系統無論多麼複雜的狀況下都能保持「高內聚,低耦合」的特色?這一系列的問題都擺在開發人員面前,亟待解決。今天就來談談解決這個問題的一些思路。html
大前端發展這麼多年了,相信也必定會遇到類似的問題。近兩年SPA發展極其迅猛,React 和 Vue一直處於風口浪尖,那咱們就看看他們是如何處理好這一問題的。前端
在SPA單頁面應用,路由起到了很關鍵的做用。路由的做用主要是保證視圖和 URL 的同步。在前端的眼裏看來,視圖是被當作是資源的一種表現。當用戶在頁面中進行操做時,應用會在若干個交互狀態中切換,路由則能夠記錄下某些重要的狀態,好比用戶查看一個網站,用戶是否登陸、在訪問網站的哪個頁面。而這些變化一樣會被記錄在瀏覽器的歷史中,用戶能夠經過瀏覽器的前進、後退按鈕切換狀態。總的來講,用戶能夠經過手動輸入或者與頁面進行交互來改變 URL,而後經過同步或者異步的方式向服務端發送請求獲取資源,成功後從新繪製 UI,原理以下圖所示:java
react-router經過傳入的location到最終渲染新的UI,流程以下:react
location的來源有2種,一種是瀏覽器的回退和前進,另一種是直接點了一個連接。新的 location 對象後,路由內部的 matchRoutes 方法會匹配出 Route 組件樹中與當前 location 對象匹配的一個子集,而且獲得了 nextState,在this.setState(nextState) 時就能夠實現從新渲染 Router 組件。android
大前端的作法大概是這樣的,咱們能夠把這些思想借鑑到iOS這邊來。上圖中的Back / Forward 在iOS這邊不少狀況下均可以被UINavgation所管理。因此iOS的Router主要處理綠色的那一塊。ios
既然前端能在SPA上解決URL和UI的同步問題,那這種思想能夠在App上解決哪些問題呢?git
思考以下的問題,平時咱們開發中是如何優雅的解決的:github
1.3D-Touch功能或者點擊推送消息,要求外部跳轉到App內部一個很深層次的一個界面。vim
好比微信的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內部的路由以前,先來談談在iOS系統間,不一樣App之間是怎麼實現跳轉的。
iOS系統是默認支持URL Scheme的,具體見官方文檔。
好比說,在iPhone的Safari瀏覽器上面輸入以下的命令,會自動打開一些App:
// 打開郵箱
mailto://
// 給110撥打電話
tel://110複製代碼
在iOS 9 以前只要在App的info.plist裏面添加URL types - URL Schemes,以下圖:
這裏就添加了一個com.ios.Qhomer的Scheme。這樣就能夠在iPhone的Safari瀏覽器上面輸入:
com.ios.Qhomer://複製代碼
就能夠直接打開這個App了。
關於其餘一些常見的App,能夠從iTunes裏面下載到它的ipa文件,解壓,顯示包內容裏面能夠找到info.plist文件,打開它,在裏面就能夠相應的URL Scheme。
// 手機QQ
mqq://
// 微信
weixin://
// 新浪微博
sinaweibo://
// 餓了麼
eleme://複製代碼
固然了,某些App對於調用URL Scheme比較敏感,它們不但願其餘的App隨意的就調用本身。
- (BOOL)application:(UIApplication *)application
openURL:(NSURL *)url
sourceApplication:(NSString *)sourceApplication
annotation:(id)annotation
{
NSLog(@"sourceApplication: %@", sourceApplication);
NSLog(@"URL scheme:%@", [url scheme]);
NSLog(@"URL query: %@", [url query]);
if ([sourceApplication isEqualToString:@"com.tencent.weixin"]){
// 容許打開
return YES;
}else{
return NO;
}
}複製代碼
若是待調用的App已經運行了,那麼它的生命週期以下:
若是待調用的App在後臺,那麼它的生命週期以下:
明白了上面的生命週期以後,咱們就能夠經過調用application:openURL:sourceApplication:annotation:這個方法,來阻止一些App的隨意調用。
如上圖,餓了麼App容許經過URL Scheme調用,那麼咱們能夠在Safari裏面調用到餓了麼App。手機QQ不容許調用,咱們在Safari裏面也就無法跳轉過去。
關於App間的跳轉問題,感興趣的能夠查看官方文檔Inter-App Communication。
App也是能夠直接跳轉到系統設置的。好比有些需求要求檢測用戶有沒有開啓某些系統權限,若是沒有開啓就彈框提示,點擊彈框的按鈕直接跳轉到系統設置裏面對應的設置界面。
iOS 10 支持經過 URL Scheme 跳轉到系統設置
iOS10跳轉系統設置的正確姿式
關於 iOS 系統功能的 URL 彙總列表
雖然在微信內部開網頁會禁止全部的Scheme,可是iOS 9.0新增長了一項功能是Universal Links,使用這個功能可使咱們的App經過HTTP連接來啓動App。
1.若是安裝過App,無論在微信裏面http連接仍是在Safari瀏覽器,仍是其餘第三方瀏覽器,均可以打開App。
2.若是沒有安裝過App,就會打開網頁。
具體設置須要3步:
1.App須要開啓Associated Domains服務,並設置Domains,注意必需要applinks:開頭。
2.域名必需要支持HTTPS。
3.上傳內容是Json格式的文件,文件名爲apple-app-site-association到本身域名的根目錄下,或者.well-known目錄下。iOS自動會去讀取這個文件。具體的文件內容請查看官方文檔。
若是App支持了Universal Links方式,那麼能夠在其餘App裏面直接跳轉到咱們本身的App裏面。以下圖,點擊連接,因爲該連接會Matcher到咱們設置的連接,因此菜單裏面會顯示用咱們的App打開。
在瀏覽器裏面也是同樣的效果,若是是支持了Universal Links方式,訪問相應的URL,會有不一樣的效果。以下圖:
以上就是iOS系統中App間跳轉的二種方式。
從iOS 系統裏面支持的URL Scheme方式,咱們能夠看出,對於一個資源的訪問,蘋果也是用URI的方式來訪問的。
統一資源標識符(英語:Uniform Resource Identifier,或URI)是一個用於標識某一互聯網資源名稱的字符串。 該種標識容許用戶對網絡中(通常指萬維網)的資源經過特定的協議進行交互操做。URI的最多見的形式是統一資源定位符(URL)。
舉個例子:
這是一段URI,每一段都表明了對應的含義。對方接收到了這樣一串字符串,按照規則解析出來,就能獲取到全部的有用信息。
這個能給咱們設計App組件間的路由帶來一些思路麼?若是咱們想要定義一個三端(iOS,Android,H5)的統一訪問資源的方式,能用URI的這種方式實現麼?
上一章節中咱們介紹了iOS系統中,系統是如何幫咱們處理App間跳轉邏輯的。這一章節咱們着重討論一下,App內部,各個組件之間的路由應該怎麼設計。關於App內部的路由設計,主要須要解決2個問題:
1.各個頁面和組件之間的跳轉問題。
2.各個組件之間相互調用。
先來分析一下這兩個問題。
在iOS開發的過程當中,常常會遇到如下的場景,點擊按鈕跳轉Push到另一個界面,或者點擊一個cell Present一個新的ViewController。在MVC模式中,通常都是新建一個VC,而後Push / Present到下一個VC。可是在MVVM中,會有一些不合適的狀況。
衆所周知,MVVM把MVC拆成了上圖演示的樣子,原來View對應的與數據相關的代碼都移到ViewModel中,相應的C也變瘦了,演變成了M-VM-C-V的結構。這裏的C裏面的代碼能夠只剩下頁面跳轉相關的邏輯。若是用代碼表示就是下面這樣子:
假設一個按鈕的執行邏輯都封裝成了command。
@weakify(self);
[[[_viewModel.someCommand executionSignals] flatten] subscribeNext:^(id x) {
@strongify(self);
// 跳轉邏輯
[self.navigationController pushViewController:targetViewController animated:YES];
}];複製代碼
上述的代碼自己沒啥問題,可是可能會弱化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個問題的路由,讓咱們先來看看GitHub上優秀開源庫的設計思路。如下是我從Github上面找的一些路由方案,按照Star從高到低排列。依次來分析一下它們各自的設計思路。
JLRoutes在整個Github上面Star最多,那就來從它來分析分析它的具體設計思路。
首先JLRoutes是受URL Scheme思路的影響。它把全部對資源的請求當作是一個URI。
首先來熟悉一下NSURLComponent的各個字段:
Note
The URLs employed by the NSURL
class are described in RFC 1808, RFC 1738, and RFC 2732.
JLRoutes會傳入每一個字符串,都按照上面的樣子進行切分處理,分別根據RFC的標準定義,取到各個NSURLComponent。
JLRoutes全局會保存一個Map,這個Map會以scheme爲Key,JLRoutes爲Value。因此在routeControllerMap裏面每一個scheme都是惟一的。
至於爲什麼有這麼多條路由,筆者認爲,若是路由按照業務線進行劃分的話,每一個業務線可能會有不相同的邏輯,即便每一個業務裏面的組件名字可能相同,可是因爲業務線不一樣,會有不一樣的路由規則。
舉個例子:若是滴滴按照每一個城市的打車業務進行組件化拆分,那麼每一個城市就對應着這裏的每一個scheme。每一個城市的打車業務都有叫車,付款……等業務,可是因爲每一個城市的地方法規不相同,因此這些組件即便名字相同,可是裏面的功能也許千差萬別。因此這裏劃分出了多個route,也能夠理解爲不一樣的命名空間。
在每一個JLRoutes裏面都保存了一個數組,這個數組裏面保存了每一個路由規則JLRRouteDefinition裏面會保存外部傳進來的block閉包,pattern,和拆分以後的pattern。
在每一個JLRoutes的數組裏面,會按照路由的優先級進行排列,優先級高的排列在前面。
- (void)_registerRoute:(NSString *)routePattern priority:(NSUInteger)priority handler:(BOOL (^)(NSDictionary *parameters))handlerBlock
{
JLRRouteDefinition *route = [[JLRRouteDefinition alloc] initWithScheme:self.scheme pattern:routePattern priority:priority handlerBlock:handlerBlock];
if (priority == 0 || self.routes.count == 0) {
[self.routes addObject:route];
} else {
NSUInteger index = 0;
BOOL addedRoute = NO;
// 找到當前已經存在的一條優先級比當前待插入的路由低的路由
for (JLRRouteDefinition *existingRoute in [self.routes copy]) {
if (existingRoute.priority < priority) {
// 若是找到,就插入數組
[self.routes insertObject:route atIndex:index];
addedRoute = YES;
break;
}
index++;
}
// 若是沒有找到任何一條路由比當前待插入的路由低的路由,或者最後一條路由優先級和當前路由同樣,那麼就只能插入到最後。
if (!addedRoute) {
[self.routes addObject:route];
}
}
}複製代碼
因爲這個數組裏面的路由是一個單調隊列,因此查找優先級的時候只用從高往低遍歷便可。
具體查找路由的過程以下:
首先根據外部傳進來的URL初始化一個JLRRouteRequest,而後用這個JLRRouteRequest在當前的路由數組裏面依次request,每一個規則都會生成一個response,可是隻有符合條件的response纔會match,最後取出匹配的JLRRouteResponse拿出其字典parameters裏面對應的參數就能夠了。查找和匹配過程當中重要的代碼以下:
- (BOOL)_routeURL:(NSURL *)URL withParameters:(NSDictionary *)parameters executeRouteBlock:(BOOL)executeRouteBlock
{
if (!URL) {
return NO;
}
[self _verboseLog:@"Trying to route URL %@", URL];
BOOL didRoute = NO;
JLRRouteRequest *request = [[JLRRouteRequest alloc] initWithURL:URL];
for (JLRRouteDefinition *route in [self.routes copy]) {
// 檢查每個route,生成對應的response
JLRRouteResponse *response = [route routeResponseForRequest:request decodePlusSymbols:shouldDecodePlusSymbols];
if (!response.isMatch) {
continue;
}
[self _verboseLog:@"Successfully matched %@", route];
if (!executeRouteBlock) {
// 若是咱們被要求不容許執行,可是又找了匹配的路由response。
return YES;
}
// 裝配最後的參數
NSMutableDictionary *finalParameters = [NSMutableDictionary dictionary];
[finalParameters addEntriesFromDictionary:response.parameters];
[finalParameters addEntriesFromDictionary:parameters];
[self _verboseLog:@"Final parameters are %@", finalParameters];
didRoute = [route callHandlerBlockWithParameters:finalParameters];
if (didRoute) {
// 調用Handler成功
break;
}
}
if (!didRoute) {
[self _verboseLog:@"Could not find a matching route"];
}
// 若是在當前路由規則裏面沒有找到匹配的路由,當前路由不是global 的,而且容許降級到global裏面去查找,那麼咱們繼續在global的路由規則裏面去查找。
if (!didRoute && self.shouldFallbackToGlobalRoutes && ![self _isGlobalRoutesController]) {
[self _verboseLog:@"Falling back to global routes..."];
didRoute = [[JLRoutes globalRoutes] _routeURL:URL withParameters:parameters executeRouteBlock:executeRouteBlock];
}
// 最後,依舊沒有找到任何能匹配的,若是有unmatched URL handler,調用這個閉包進行最後的處理。
if, after everything, we did not route anything and we have an unmatched URL handler, then call it
if (!didRoute && executeRouteBlock && self.unmatchedURLHandler) {
[self _verboseLog:@"Falling back to the unmatched URL handler"];
self.unmatchedURLHandler(self, URL, parameters);
}
return didRoute;
}複製代碼
舉個例子:
咱們先註冊一個Router,規則以下:
[[JLRoutes globalRoutes] addRoute:@"/:object/:primaryKey" handler:^BOOL(NSDictionary *parameters) {
NSString *object = parameters[@"object"];
NSString *primaryKey = parameters[@"primaryKey"];
// stuff
return YES;
}];複製代碼
咱們傳入一個URL,讓Router進行處理。
NSURL *editPost = [NSURL URLWithString:@"ele://post/halfrost?debug=true&foo=bar"];
[[UIApplication sharedApplication] openURL:editPost];複製代碼
匹配成功以後,咱們會獲得下面這樣一個字典:
{
"object": "post",
"action": "halfrost",
"debug": "true",
"foo": "bar",
"JLRouteURL": "ele://post/halfrost?debug=true&foo=bar",
"JLRoutePattern": "/:object/:action",
"JLRouteScheme": "JLRoutesGlobalRoutesScheme"
}複製代碼
把上述過程圖解出來,見下圖:
JLRoutes還能夠支持Optional的路由規則,假如定義一條路由規則:
/the(/foo/:a)(/bar/:b)複製代碼
JLRoutes 會幫咱們默認註冊以下4條路由規則:
/the/foo/:a/bar/:b
/the/foo/:a
/the/bar/:b
/the複製代碼
Routable路由是用在in-app native端的 URL router, 它能夠用在iOS上也能夠用在Android上。
UPRouter裏面保存了2個字典。routes字典裏面存儲的Key是路由規則,Value存儲的是UPRouterOptions。cachedRoutes裏面存儲的Key是最終的URL,帶傳參的,Value存儲的是RouterParams。RouterParams裏面會包含在routes匹配的到的UPRouterOptions,還有額外的打開參數openParams和一些額外參數extraParams。
- (RouterParams *)routerParamsForUrl:(NSString *)url extraParams: (NSDictionary *)extraParams {
if (!url) {
//if we wait, caching this as key would throw an exception
if (_ignoresExceptions) {
return nil;
}
@throw [NSException exceptionWithName:@"RouteNotFoundException"
reason:[NSString stringWithFormat:ROUTE_NOT_FOUND_FORMAT, url]
userInfo:nil];
}
if ([self.cachedRoutes objectForKey:url] && !extraParams) {
return [self.cachedRoutes objectForKey:url];
}
// 比對url經過/分割以後的參數個數和pathComponents的個數是否同樣
NSArray *givenParts = url.pathComponents;
NSArray *legacyParts = [url componentsSeparatedByString:@"/"];
if ([legacyParts count] != [givenParts count]) {
NSLog(@"Routable Warning - your URL %@ has empty path components - this will throw an error in an upcoming release", url);
givenParts = legacyParts;
}
__block RouterParams *openParams = nil;
[self.routes enumerateKeysAndObjectsUsingBlock:
^(NSString *routerUrl, UPRouterOptions *routerOptions, BOOL *stop) {
NSArray *routerParts = [routerUrl pathComponents];
if ([routerParts count] == [givenParts count]) {
NSDictionary *givenParams = [self paramsForUrlComponents:givenParts routerUrlComponents:routerParts];
if (givenParams) {
openParams = [[RouterParams alloc] initWithRouterOptions:routerOptions openParams:givenParams extraParams: extraParams];
*stop = YES;
}
}
}];
if (!openParams) {
if (_ignoresExceptions) {
return nil;
}
@throw [NSException exceptionWithName:@"RouteNotFoundException"
reason:[NSString stringWithFormat:ROUTE_NOT_FOUND_FORMAT, url]
userInfo:nil];
}
[self.cachedRoutes setObject:openParams forKey:url];
return openParams;
}複製代碼
這一段代碼裏面重點在幹一件事情,遍歷routes字典,而後找到參數匹配的字符串,封裝成RouterParams返回。
- (NSDictionary *)paramsForUrlComponents:(NSArray *)givenUrlComponents routerUrlComponents:(NSArray *)routerUrlComponents {
__block NSMutableDictionary *params = [NSMutableDictionary dictionary];
[routerUrlComponents enumerateObjectsUsingBlock:
^(NSString *routerComponent, NSUInteger idx, BOOL *stop) {
NSString *givenComponent = givenUrlComponents[idx];
if ([routerComponent hasPrefix:@":"]) {
NSString *key = [routerComponent substringFromIndex:1];
[params setObject:givenComponent forKey:key];
}
else if (![routerComponent isEqualToString:givenComponent]) {
params = nil;
*stop = YES;
}
}];
return params;
}複製代碼
上面這段函數,第一個參數是外部傳進來URL帶有各個入參的分割數組。第二個參數是路由規則分割開的數組。routerComponent因爲規定:號後面纔是參數,因此routerComponent的第1個位置就是對應的參數名。params字典裏面以參數名爲Key,參數爲Value。
NSDictionary *givenParams = [self paramsForUrlComponents:givenParts routerUrlComponents:routerParts];
if (givenParams) {
openParams = [[RouterParams alloc] initWithRouterOptions:routerOptions openParams:givenParams extraParams: extraParams];
*stop = YES;
}複製代碼
最後經過RouterParams的初始化方法,把路由規則對應的UPRouterOptions,上一步封裝好的參數字典givenParams,還有
routerParamsForUrl: extraParams: 方法的第二個入參,這3個參數做爲初始化參數,生成了一個RouterParams。
[self.cachedRoutes setObject:openParams forKey:url];複製代碼
最後一步self.cachedRoutes的字典裏面Key爲帶參數的URL,Value是RouterParams。
最後將匹配封裝出來的RouterParams轉換成對應的Controller。
- (UIViewController *)controllerForRouterParams:(RouterParams *)params {
SEL CONTROLLER_CLASS_SELECTOR = sel_registerName("allocWithRouterParams:");
SEL CONTROLLER_SELECTOR = sel_registerName("initWithRouterParams:");
UIViewController *controller = nil;
Class controllerClass = params.routerOptions.openClass;
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
if ([controllerClass respondsToSelector:CONTROLLER_CLASS_SELECTOR]) {
controller = [controllerClass performSelector:CONTROLLER_CLASS_SELECTOR withObject:[params controllerParams]];
}
else if ([params.routerOptions.openClass instancesRespondToSelector:CONTROLLER_SELECTOR]) {
controller = [[params.routerOptions.openClass alloc] performSelector:CONTROLLER_SELECTOR withObject:[params controllerParams]];
}
#pragma clang diagnostic pop
if (!controller) {
if (_ignoresExceptions) {
return controller;
}
@throw [NSException exceptionWithName:@"RoutableInitializerNotFound"
reason:[NSString stringWithFormat:INVALID_CONTROLLER_FORMAT, NSStringFromClass(controllerClass), NSStringFromSelector(CONTROLLER_CLASS_SELECTOR), NSStringFromSelector(CONTROLLER_SELECTOR)]
userInfo:nil];
}
controller.modalTransitionStyle = params.routerOptions.transitionStyle;
controller.modalPresentationStyle = params.routerOptions.presentationStyle;
return controller;
}複製代碼
若是Controller是一個類,那麼就調用allocWithRouterParams:方法去初始化。若是Controller已是一個實例了,那麼就調用initWithRouterParams:方法去初始化。
將Routable的大體流程圖解以下:
這是布丁動畫的一個Router,靈感來自於 ABRouter 和 Routable iOS。
先來看看HHRouter的Api。它提供的方法很是清晰。
ViewController提供了2個方法。map是用來設置路由規則,matchController是用來匹配路由規則的,匹配爭取以後返回對應的UIViewController。
- (void)map:(NSString *)route toControllerClass:(Class)controllerClass;
- (UIViewController *)matchController:(NSString *)route;複製代碼
block閉包提供了三個方法,map也是設置路由規則,matchBlock:是用來匹配路由,找到指定的block,可是不會調用該block。callBlock:是找到指定的block,找到之後就當即調用。
- (void)map:(NSString *)route toBlock:(HHRouterBlock)block;
- (HHRouterBlock)matchBlock:(NSString *)route;
- (id)callBlock:(NSString *)route;複製代碼
matchBlock:和callBlock:的區別就在於前者不會自動調用閉包。因此matchBlock:方法找到對應的block以後,若是想調用,須要手動調用一次。
除去上面這些方法,HHRouter還爲咱們提供了一個特殊的方法。
- (HHRouteType)canRoute:(NSString *)route;複製代碼
這個方法就是用來找到執行路由規則對應的RouteType,RouteType總共就3種:
typedef NS_ENUM (NSInteger, HHRouteType) {
HHRouteTypeNone = 0,
HHRouteTypeViewController = 1,
HHRouteTypeBlock = 2
};複製代碼
再來看看HHRouter是如何管理路由規則的。整個HHRouter就是由一個NSMutableDictionary *routes控制的。
@interface HHRouter ()
@property (strong, nonatomic) NSMutableDictionary *routes;
@end複製代碼
別看只有這一個看似「簡單」的字典數據結構,可是HHRouter路由設計的仍是很精妙的。
- (void)map:(NSString *)route toBlock:(HHRouterBlock)block
{
NSMutableDictionary *subRoutes = [self subRoutesToRoute:route];
subRoutes[@"_"] = [block copy];
}
- (void)map:(NSString *)route toControllerClass:(Class)controllerClass
{
NSMutableDictionary *subRoutes = [self subRoutesToRoute:route];
subRoutes[@"_"] = controllerClass;
}複製代碼
上面兩個方法分別是block閉包和ViewController設置路由規則調用的方法實體。無論是ViewController仍是block閉包,設置規則的時候都會調用subRoutesToRoute:方法。
- (NSMutableDictionary *)subRoutesToRoute:(NSString *)route
{
NSArray *pathComponents = [self pathComponentsFromRoute:route];
NSInteger index = 0;
NSMutableDictionary *subRoutes = self.routes;
while (index < pathComponents.count) {
NSString *pathComponent = pathComponents[index];
if (![subRoutes objectForKey:pathComponent]) {
subRoutes[pathComponent] = [[NSMutableDictionary alloc] init];
}
subRoutes = subRoutes[pathComponent];
index++;
}
return subRoutes;
}複製代碼
上面這段函數就是來構造路由匹配規則的字典。
舉個例子:
[[HHRouter shared] map:@"/user/:userId/"
toControllerClass:[UserViewController class]];
[[HHRouter shared] map:@"/story/:storyId/"
toControllerClass:[StoryViewController class]];
[[HHRouter shared] map:@"/user/:userId/story/?a=0"
toControllerClass:[StoryListViewController class]];複製代碼
設置3條規則之後,按照上面構造路由匹配規則的字典的方法,該路由規則字典就會變成這個樣子:
{
story = {
":storyId" = {
"_" = StoryViewController;
};
};
user = {
":userId" = {
"_" = UserViewController;
story = {
"_" = StoryListViewController;
};
};
};
}複製代碼
路由規則字典生成以後,等到匹配的時候就會遍歷這個字典。
假設這時候有一條路由過來:
[[[HHRouter shared] matchController:@"hhrouter20://user/1/"] class],複製代碼
HHRouter對這條路由的處理方式是先匹配前面的scheme,若是連scheme都不正確的話,會直接致使後面匹配失敗。
而後再進行路由匹配,最後生成的參數字典以下:
{
"controller_class" = UserViewController;
route = "/user/1/";
userId = 1;
}複製代碼
具體的路由參數匹配的函數在
- (NSDictionary *)paramsInRoute:(NSString *)route複製代碼
這個方法裏面實現的。這個方法就是按照路由匹配規則,把傳進來的URL的參數都一一解析出來,帶?號的也都會解析成字典。這個方法沒什麼難度,就不在贅述了。
ViewController 的字典裏面默認還會加上2項:
"controller_class" =
route =複製代碼
route裏面都會保存傳過來的完整的URL。
若是傳進來的路由後面帶訪問字符串呢?那咱們再來看看:
[[HHRouter shared] matchController:@"/user/1/?a=b&c=d"]複製代碼
那麼解析出全部的參數字典會是下面的樣子:
{
a = b;
c = d;
"controller_class" = UserViewController;
route = "/user/1/?a=b&c=d";
userId = 1;
}複製代碼
同理,若是是一個block閉包的狀況呢?
仍是先添加一條block閉包的路由規則:
[[HHRouter shared] map:@"/user/add/"
toBlock:^id(NSDictionary* params) {
}];複製代碼
這條規則對應的會生成一個路由規則的字典。
{
story = {
":storyId" = {
"_" = StoryViewController;
};
};
user = {
":userId" = {
"_" = UserViewController;
story = {
"_" = StoryListViewController;
};
};
add = {
"_" = "<__NSMallocBlock__: 0x600000240480>";
};
};
}複製代碼
注意」_」後面跟着是一個block。
匹配block閉包的方式有兩種。
// 1.第一種方式匹配到對應的block以後,還須要手動調用一次閉包。
HHRouterBlock block = [[HHRouter shared] matchBlock:@"/user/add/?a=1&b=2"];
block(nil);
// 2.第二種方式匹配block以後自動會調用改閉包。
[[HHRouter shared] callBlock:@"/user/add/?a=1&b=2"];複製代碼
匹配出來的參數字典是以下:
{
a = 1;
b = 2;
block = "<__NSMallocBlock__: 0x600000056b90>";
route = "/user/add/?a=1&b=2";
}複製代碼
block的字典裏面會默認加上下面這2項:
block =
route =複製代碼
route裏面都會保存傳過來的完整的URL。
生成的參數字典最終會被綁定到ViewController的Associated Object關聯對象上。
- (void)setParams:(NSDictionary *)paramsDictionary
{
objc_setAssociatedObject(self, &kAssociatedParamsObjectKey, paramsDictionary, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (NSDictionary *)params
{
return objc_getAssociatedObject(self, &kAssociatedParamsObjectKey);
}複製代碼
這個綁定的過程是在match匹配完成的時候進行的。
- (UIViewController *)matchController:(NSString *)route
{
NSDictionary *params = [self paramsInRoute:route];
Class controllerClass = params[@"controller_class"];
UIViewController *viewController = [[controllerClass alloc] init];
if ([viewController respondsToSelector:@selector(setParams:)]) {
[viewController performSelector:@selector(setParams:)
withObject:[params copy]];
}
return viewController;
}複製代碼
最終獲得的ViewController也是咱們想要的。相應的參數都在它綁定的params屬性的字典裏面。
將上述過程圖解出來,以下:
這是蘑菇街的一個路由的方法。
這個庫的由來:
JLRoutes 的問題主要在於查找 URL 的實現不夠高效,經過遍歷而不是匹配。還有就是功能偏多。
HHRouter 的 URL 查找是基於匹配,因此會更高效,MGJRouter 也是採用的這種方法,但它跟 ViewController 綁定地過於緊密,必定程度上下降了靈活性。
因而就有了 MGJRouter。
從數據結構來看,MGJRouter仍是和HHRouter如出一轍的。
@interface MGJRouter ()
@property (nonatomic) NSMutableDictionary *routes;
@end複製代碼
那麼咱們就來看看它對HHRouter作了哪些優化改進。
[MGJRouter openURL:@"mgj://category/travel" withUserInfo:@{@"user_id": @1900} completion:nil];複製代碼
這個對比HHRouter,僅僅只是寫法上的一個語法糖,在HHRouter中雖然不支持帶字典的參數,可是在URL後面能夠用URL Query Parameter來彌補。
if (parameters) {
MGJRouterHandler handler = parameters[@"block"];
if (completion) {
parameters[MGJRouterParameterCompletion] = completion;
}
if (userInfo) {
parameters[MGJRouterParameterUserInfo] = userInfo;
}
if (handler) {
[parameters removeObjectForKey:@"block"];
handler(parameters);
}
}複製代碼
MGJRouter對userInfo的處理是直接把它封裝到Key = MGJRouterParameterUserInfo對應的Value裏面。
[parameters enumerateKeysAndObjectsUsingBlock:^(id key, NSString *obj, BOOL *stop) {
if ([obj isKindOfClass:[NSString class]]) {
parameters[key] = [obj stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
}
}];複製代碼
這裏就是須要注意一下編碼。
這一點是模仿的JLRoutes的匹配不到會自動降級到global的思想。
if (parameters) {
MGJRouterHandler handler = parameters[@"block"];
if (handler) {
[parameters removeObjectForKey:@"block"];
handler(parameters);
}
}複製代碼
parameters字典裏面會先存儲下一個路由規則,存在block閉包中,在匹配的時候會取出這個handler,降級匹配到這個閉包中,進行最終的處理。
在MGJRouter裏面,做者對原來的HHRouter字典裏面存儲的路由規則的結構進行了改造。
NSString *const MGJRouterParameterURL = @"MGJRouterParameterURL";
NSString *const MGJRouterParameterCompletion = @"MGJRouterParameterCompletion";
NSString *const MGJRouterParameterUserInfo = @"MGJRouterParameterUserInfo";複製代碼
這3個key會分別保存一些信息:
MGJRouterParameterURL保存的傳進來的完整的URL信息。
MGJRouterParameterCompletion保存的是completion閉包。
MGJRouterParameterUserInfo保存的是UserInfo字典。
舉個例子:
[MGJRouter registerURLPattern:@"ele://name/:name" toHandler:^(NSDictionary *routerParameters) {
void (^completion)(NSString *) = routerParameters[MGJRouterParameterCompletion];
if (completion) {
completion(@"完成了");
}
}];
[MGJRouter openURL:@"ele://name/halfrost/?age=20" withUserInfo:@{@"user_id": @1900} completion:^(id result) {
NSLog(@"result = %@",result);
}];複製代碼
上面的URL會匹配成功,那麼生成的參數字典結構以下:
{
MGJRouterParameterCompletion = "<__NSGlobalBlock__: 0x107ffe680>";
MGJRouterParameterURL = "ele://name/halfrost/?age=20";
MGJRouterParameterUserInfo = {
"user_id" = 1900;
};
age = 20;
block = "<__NSMallocBlock__: 0x608000252120>";
name = halfrost;
}複製代碼
這個功能很是有用。
URL 的處理一不當心,就容易散落在項目的各個角落,不容易管理。好比註冊時的 pattern 是 mgj://beauty/:id,而後 open 時就是 mgj://beauty/123,這樣到時候 url 有改動,處理起來就會很麻煩,很差統一管理。
因此 MGJRouter 提供了一個類方法來處理這個問題。
#define TEMPLATE_URL @"qq://name/:name"
[MGJRouter registerURLPattern:TEMPLATE_URL toHandler:^(NSDictionary *routerParameters) {
NSLog(@"routerParameters[name]:%@", routerParameters[@"name"]); // halfrost
}];
[MGJRouter openURL:[MGJRouter generateURLWithPattern:TEMPLATE_URL parameters:@[@"halfrost"]]];
}複製代碼
generateURLWithPattern:函數會對咱們定義的宏裏面的全部的:進行替換,替換成後面的字符串數組,依次賦值。
將上述過程圖解出來,以下:
蘑菇街爲了區分開頁面間調用和組件間調用,因而想出了一種新的方法。用Protocol的方法來進行組件間的調用。
每一個組件之間都有一個 Entry,這個 Entry,主要作了三件事:
頁面間的openURL調用就是以下的樣子:
每一個組件間都會向MGJRouter註冊,組件間相互調用或者是其餘的App均可以經過openURL:方法打開一個界面或者調用一個組件。
在組件間的調用,蘑菇街採用了Protocol的方式。
[ModuleManager registerClass:ClassA forProtocol:ProtocolA] 的結果就是在 MM 內部維護的 dict 裏新加了一個映射關係。
[ModuleManager classForProtocol:ProtocolA] 的返回結果就是以前在 MM 內部 dict 裏 protocol 對應的 class,使用方不須要關心這個 class 是個什麼東東,反正實現了 ProtocolA 協議,拿來用就行。
這裏須要有一個公共的地方來容納這些 public protocl,也就是圖中的 PublicProtocl.h。
我猜想,大概實現多是下面的樣子:
@interface ModuleProtocolManager : NSObject
+ (void)registServiceProvide:(id)provide forProtocol:(Protocol*)protocol;
+ (id)serviceProvideForProtocol:(Protocol *)protocol;
@end複製代碼
而後這個是一個單例,在裏面註冊各個協議:
@interface ModuleProtocolManager ()
@property (nonatomic, strong) NSMutableDictionary *serviceProvideSource;
@end
@implementation ModuleProtocolManager
+ (ModuleProtocolManager *)sharedInstance
{
static ModuleProtocolManager * instance;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [[self alloc] init];
});
return instance;
}
- (instancetype)init
{
self = [super init];
if (self) {
_serviceProvideSource = [[NSMutableDictionary alloc] init];
}
return self;
}
+ (void)registServiceProvide:(id)provide forProtocol:(Protocol*)protocol
{
if (provide == nil || protocol == nil)
return;
[[self sharedInstance].serviceProvideSource setObject:provide forKey:NSStringFromProtocol(protocol)];
}
+ (id)serviceProvideForProtocol:(Protocol *)protocol
{
return [[self sharedInstance].serviceProvideSource objectForKey:NSStringFromProtocol(protocol)];
}複製代碼
在ModuleProtocolManager中用一個字典保存每一個註冊的protocol。如今再來猜猜ModuleEntry的實現。
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
@protocol DetailModuleEntryProtocol <NSObject>
@required;
- (UIViewController *)detailViewControllerWithId:(NSString*)Id Name:(NSString *)name;
@end複製代碼
而後每一個模塊內都有一個和暴露到外面的協議相鏈接的「接頭」。
#import <Foundation/Foundation.h>
@interface DetailModuleEntry : NSObject
@end複製代碼
在它的實現中,須要引入3個外部文件,一個是ModuleProtocolManager,一個是DetailModuleEntryProtocol,最後一個是所在模塊須要跳轉或者調用的組件或者頁面。
#import "DetailModuleEntry.h"
#import <DetailModuleEntryProtocol/DetailModuleEntryProtocol.h>
#import <ModuleProtocolManager/ModuleProtocolManager.h>
#import "DetailViewController.h"
@interface DetailModuleEntry()<DetailModuleEntryProtocol>
@end
@implementation DetailModuleEntry
+ (void)load
{
[ModuleProtocolManager registServiceProvide:[[self alloc] init] forProtocol:@protocol(DetailModuleEntryProtocol)];
}
- (UIViewController *)detailViewControllerWithId:(NSString*)Id Name:(NSString *)name
{
DetailViewController *detailVC = [[DetailViewController alloc] initWithId:id Name:name];
return detailVC;
}
@end複製代碼
至此基於Protocol的方案就完成了。若是須要調用某個組件或者跳轉某個頁面,只要先從ModuleProtocolManager的字典裏面根據對應的ModuleEntryProtocol找到對應的DetailModuleEntry,找到了DetailModuleEntry就是找到了組件或者頁面的「入口」了。再把參數傳進去便可。
- (void)didClickDetailButton:(UIButton *)button
{
id< DetailModuleEntryProtocol > DetailModuleEntry = [ModuleProtocolManager serviceProvideForProtocol:@protocol(DetailModuleEntryProtocol)];
UIViewController *detailVC = [DetailModuleEntry detailViewControllerWithId:@「詳情界面」 Name:@「個人購物車」];
[self.navigationController pushViewController:detailVC animated:YES];
}複製代碼
這樣就能夠調用到組件或者界面了。
若是組件之間有相同的接口,那麼還能夠進一步的把這些接口都抽離出來。這些抽離出來的接口變成「元接口」,它們是能夠足夠支撐起整個組件一層的。
再來講說@casatwy的方案,這方案是基於Mediator的。
傳統的中間人Mediator的模式是這樣的:
這種模式每一個頁面或者組件都會依賴中間者,各個組件之間互相再也不依賴,組件間調用只依賴中間者Mediator,Mediator仍是會依賴其餘組件。那麼這是最終方案了麼?
看看@casatwy是怎麼繼續優化的。
主要思想是利用了Target-Action簡單粗暴的思想,利用Runtime解決解耦的問題。
- (id)performTarget:(NSString *)targetName action:(NSString *)actionName params:(NSDictionary *)params shouldCacheTarget:(BOOL)shouldCacheTarget
{
NSString *targetClassString = [NSString stringWithFormat:@"Target_%@", targetName];
NSString *actionString = [NSString stringWithFormat:@"Action_%@:", actionName];
Class targetClass;
NSObject *target = self.cachedTarget[targetClassString];
if (target == nil) {
targetClass = NSClassFromString(targetClassString);
target = [[targetClass alloc] init];
}
SEL action = NSSelectorFromString(actionString);
if (target == nil) {
// 這裏是處理無響應請求的地方之一,這個demo作得比較簡單,若是沒有能夠響應的target,就直接return了。實際開發過程當中是能夠事先給一個固定的target專門用於在這個時候頂上,而後處理這種請求的
return nil;
}
if (shouldCacheTarget) {
self.cachedTarget[targetClassString] = target;
}
if ([target respondsToSelector:action]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
return [target performSelector:action withObject:params];
#pragma clang diagnostic pop
} else {
// 有可能target是Swift對象
actionString = [NSString stringWithFormat:@"Action_%@WithParams:", actionName];
action = NSSelectorFromString(actionString);
if ([target respondsToSelector:action]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
return [target performSelector:action withObject:params];
#pragma clang diagnostic pop
} else {
// 這裏是處理無響應請求的地方,若是無響應,則嘗試調用對應target的notFound方法統一處理
SEL action = NSSelectorFromString(@"notFound:");
if ([target respondsToSelector:action]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
return [target performSelector:action withObject:params];
#pragma clang diagnostic pop
} else {
// 這裏也是處理無響應請求的地方,在notFound都沒有的時候,這個demo是直接return了。實際開發過程當中,能夠用前面提到的固定的target頂上的。
[self.cachedTarget removeObjectForKey:targetClassString];
return nil;
}
}
}
}複製代碼
targetName就是調用接口的Object,actionName就是調用方法的SEL,params是參數,shouldCacheTarget表明是否須要緩存,若是須要緩存就把target存起來,Key是targetClassString,Value是target。
經過這種方式進行改造的,外面調用的方法都很統一,都是調用performTarget: action: params: shouldCacheTarget:。第三個參數是一個字典,這個字典裏面能夠傳不少參數,只要Key-Value寫好就能夠了。處理錯誤的方式也統一在一個地方了,target沒有,或者是target沒法響應相應的方法,均可以在Mediator這裏進行統一出錯處理。
可是在實際開發過程當中,無論是界面調用,組件間調用,在Mediator中須要定義不少方法。因而作做者又想出了建議咱們用Category的方法,對Mediator的全部方法進行拆分,這樣就就能夠不會致使Mediator這個類過於龐大了。
- (UIViewController *)CTMediator_viewControllerForDetail
{
UIViewController *viewController = [self performTarget:kCTMediatorTargetA
action:kCTMediatorActionNativFetchDetailViewController
params:@{@"key":@"value"}
shouldCacheTarget:NO
];
if ([viewController isKindOfClass:[UIViewController class]]) {
// view controller 交付出去以後,能夠由外界選擇是push仍是present
return viewController;
} else {
// 這裏處理異常場景,具體如何處理取決於產品
return [[UIViewController alloc] init];
}
}
- (void)CTMediator_presentImage:(UIImage *)image
{
if (image) {
[self performTarget:kCTMediatorTargetA
action:kCTMediatorActionNativePresentImage
params:@{@"image":image}
shouldCacheTarget:NO];
} else {
// 這裏處理image爲nil的場景,如何處理取決於產品
[self performTarget:kCTMediatorTargetA
action:kCTMediatorActionNativeNoImage
params:@{@"image":[UIImage imageNamed:@"noImage"]}
shouldCacheTarget:NO];
}
}複製代碼
把這些具體的方法一個個的都寫在Category裏面就行了,調用的方式都很是的一致,都是調用performTarget: action: params: shouldCacheTarget:方法。
最終去掉了中間者Mediator對組件的依賴,各個組件之間互相再也不依賴,組件間調用只依賴中間者Mediator,Mediator不依賴其餘任何組件。
除了上面開源的路由方案,還有一些並無開源的設計精美的方案。這裏能夠和你們一塊兒分析交流一下。
這個方案是Uber 騎手App的一個方案。
Uber在發現MVC的一些弊端以後:好比動輒上萬行巨胖無比的VC,沒法進行單元測試等缺點後,因而考慮把架構換成VIPER。可是VIPER也有必定的弊端。由於它的iOS特定的結構,意味着iOS必須爲Android作出一些妥協的權衡。以視圖爲驅動的應用程序邏輯,表明應用程序狀態由視圖驅動,整個應用程序都鎖定在視圖樹上。由操做應用程序狀態所關聯的業務邏輯的改變,就必須通過Presenter。所以會暴露業務邏輯。最終致使了視圖樹和業務樹進行了牢牢的耦合。這樣想實現一個牢牢只有業務邏輯的Node節點或者牢牢只有視圖邏輯的Node節點就很是的困難了。
經過改進VIPER架構,吸取其優秀的特色,改進其缺點,就造成了Uber 騎手App的全新架構——Riblets(肋骨)。
在這個新的架構中,即便是類似的邏輯也會被區分紅很小很小,相互獨立,能夠單獨進行測試的組件。每一個組件都有很是明確的用途。使用這些一小塊一小塊的Riblets(肋骨),最終把整個App拼接成一顆Riblets(肋骨)樹。
經過抽象,一個Riblets(肋骨)被定義成一下6個更小的組件,這些組件各自有各自的職責。經過一個Riblets(肋骨)進一步的抽象業務邏輯和視圖邏輯。
一個Riblets(肋骨)被設計成這樣,那和以前的VIPER和MVC有什麼區別呢?最大的區別在路由上面。
Riblets(肋骨)內的Router再也不是視圖邏輯驅動的,如今變成了業務邏輯驅動。這一重大改變就致使了整個App再也不是由表現形式驅動,如今變成了由數據流驅動。
每個Riblet都是由一個路由Router,一個關聯器Interactor,一個構造器Builder和它們相關的組件構成的。因此它的命名(Router - Interactor - Builder,Rib)也由此得來。固然還能夠有可選的展現器Presenter和視圖View。路由Router和關聯器Interactor處理業務邏輯,展現器Presenter和視圖View處理視圖邏輯。
重點分析一下Riblet裏面路由的職責。
在整個App的結構樹中,路由的職責是用來關聯和取消關聯其餘子Riblet的。至於決定是由關聯器Interactor傳遞過來的。在狀態轉換過程當中,關聯和取消關聯子Riblet的時候,路由也會影響到關聯器Interactor的生命週期。路由只包含2個業務邏輯:
1.提供關聯和取消關聯其餘路由的方法。
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和支撐模塊化架構起了重大做用。
Riblet中的數據流
在這個新的架構中,數據流動是單向的。Data數據流從service服務流到Model Stream生成Model流。Model流再從Model Stream流動到Interactor關聯器。Interactor關聯器,scheduler調度器,遠程推送均可以想Service觸發變化來引發Model Stream的改動。Model Stream生成不可改動的models。這個強制的要求就致使關聯器只能經過Service層改變App的狀態。
舉兩個例子:
數據從後臺到視圖View上
一個狀態的改變,引發服務器後臺觸發推送到App。數據就被Push到App,而後生成不可變的數據流。關聯器收到model以後,把它傳遞給展現器Presenter。展現器Presenter把model轉換成view model傳遞給視圖View。
數據從視圖到服務器後臺
當用戶點擊了一個按鈕,好比登陸按鈕。視圖View就會觸發UI事件傳遞給展現器Presenter。展現器Presenter調用關聯器Interactor登陸方法。關聯器Interactor又會調用Service call的實際登陸方法。請求網絡以後會把數據pull到後臺服務器。
Riblet間的數據流
當一個關聯器Interactor在處理業務邏輯的工程中,須要調用其餘Riblet的事件的時候,關聯器Interactor須要和子關聯器Interactor進行關聯。見上圖5個步驟。
若是調用方法是從子調用父類,父類的Interactor的接口一般被定義成監聽者listener。若是調用方法是從父類調用到子類,那麼子類的接口一般是一個delegate,實現父類的一些Protocol。
在Riblet的方案中,路由Router僅僅只是用來維護一個樹型關係,而關聯器Interactor才擔當的是用來決定觸發組件間的邏輯跳轉的角色。
通過上面的分析,能夠發現,路由的設計思路是從URLRoute ->Protocol-class ->Target-Action一步步的深刻的過程。這也是逐漸深刻本質的過程。
首先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是不夠友好的,它最可能是傳遞一個字典。
Protocol-Class方案的優勢,這個方案沒有硬編碼。
Protocol-Class方案也是存在一些缺點的,每一個Protocol都要向ModuleManager進行註冊。
這種方案ModuleEntry是同時須要依賴ModuleManager和組件裏面的頁面或者組件二者的。固然ModuleEntry也是會依賴ModuleEntryProtocol的,可是這個依賴是能夠去掉的,好比用Runtime的方法NSProtocolFromString,加上硬編碼是能夠去掉對Protocol的依賴的。可是考慮到硬編碼的方式對出現bug,後期維護都是不友好的,因此對Protocol的依賴仍是不要去除。
最後一個缺點是組件方法的調用是分散在各處的,沒有統一的入口,也就無法作組件不存在時或者出現錯誤時的統一處理。
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處再把字典拆包成常規參數,這就形成了一部分的硬編碼。
這個問題其實應該是在打算實施組件化以前就應該考慮的問題。爲什麼還要放在這裏說呢?由於組件的拆分每一個公司都有屬於本身的拆分方案,按照業務線拆?按照最細小的業務功能模塊拆?仍是按照一個完成的功能進行拆分?這個就牽扯到了拆分粗細度的問題了。組件拆分的粗細度就會直接關係到將來路由須要解耦的程度。
假設,把登陸的全部流程封裝成一個組件,因爲登陸裏面會涉及到多個頁面,那麼這些頁面都會打包在一個組件裏面。那麼其餘模塊須要調用登陸狀態的時候,這時候就須要用到登陸組件暴露在外面能夠獲取登陸狀態的接口。那麼這個時候就能夠考慮把這些接口寫到Protocol裏面,暴露給外面使用。或者用Target-Action的方法。這種把一個功能所有都劃分紅登陸組件的話,劃分粒度就稍微粗一點。
若是僅僅把登陸狀態的細小功能劃分紅一個元組件,那麼外面想獲取登陸狀態就直接調用這個組件就好。這種劃分的粒度就很是細了。這樣就會致使組件個數巨多。
因此在進行拆分組件的時候,也許當時業務並不複雜的時候,拆分紅組件,相互耦合也不大。可是隨着業務無論變化,以前劃分的組件間耦合性愈來愈大,因而就會考慮繼續把以前的組件再進行拆分。也許有些業務砍掉了,以前一些小的組件也許還會被組合到一塊兒。總之,在業務沒有徹底固定下來以前,組件的劃分可能一直進行時。
關於架構,我以爲拋開業務談架構是沒有意義的。由於架構是爲了業務服務的,空談架構只是一種理想的狀態。因此沒有最好的方案,只有最適合的方案。
最適合本身公司業務的方案纔是最好的方案。分而治之,針對不一樣業務選擇不一樣的方案纔是最優的解決方案。若是非要籠統的採用一種方案,不一樣業務之間須要同一種方案,須要妥協犧牲的東西太多就很差了。
但願本文能拋磚引玉,幫助你們選擇出最適合自家業務的路由方案。固然確定會有更加優秀的方案,但願你們能多多指點我。
References:
在現有工程中實施基於CTMediator的組件化方案
iOS應用架構談 組件化方案
蘑菇街 App 的組件化之路
蘑菇街 App 的組件化之路·續
ENGINEERING THE ARCHITECTURE BEHIND UBER’S NEW RIDER APP