組件化也是一個老生常談的話題了,本文主要說一下在組件化中,站比較重要的位置的路由設計
。
你的項目裏多是直接依賴了三方的路由組件,也多是本身根據項目的實際需求私人訂製了一套路由組件,下面我想經過幾個呼聲比較高的三方組件來聊一聊路由的設計和分析。這裏不推薦說你們用哪一個好哪一個很差,只是學習他們設計思想
。就比如咱們看三方庫源碼,應該都是學習編程和設計的思想爲主。html
隨着App的需求愈來愈多,業務愈來愈複雜,爲了更高效的迭代以及提升用戶體驗,下降維護成本,對一個更高效的框架的需求也愈來愈急切。
因此咱們可能都經歷過項目的重構、組件化,根據項目的實際需求,新的框架可能須要橫向,縱向不一樣粒度的分層,爲了之後更有效率的開發和維護。隨之而來的一個問題,如何保持「高內聚,低耦合」的特色,下面就來談談解決這個問題的一些思路。git
列舉幾個平時開發中遇到的問題,或者說是需求:github
以上這些問題,均可以經過設計一個路由來解決,下面帶着這些問題繼續看如何實現跳轉。編程
經過上面的問題,咱們但願設計一套路由,實現App外部和內部的統一跳轉,因此先說一下App外部跳轉的實現。數組
在info.plist裏面添加URL types - URL Schemes瀏覽器
而後在Safari中輸入這裏設置的URL Schemes就能夠直接打開App安全
- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication annotation:(id)annotation
{
}
複製代碼
經過上面這個方法就能夠監聽到外部App的調用,能夠根據須要作一些攔截或者其餘操做。bash
App也是能夠直接跳轉到系統設置
的。好比有些需求要求檢測用戶有沒有開啓某些系統權限,若是沒有開啓就彈框提示,點擊彈框的按鈕直接跳轉到系統設置裏面對應的設置界面。架構
Universal links
這個功能可使咱們的App經過http連接來啓動。app
設置方式:
applinks:
開頭
以上就是iOS系統中App間跳轉的二種方式。
說完App間的跳轉邏輯,接下來就進入重點,App內部的路由設計。
主要要解決兩個問題:
綜合上面所說的兩個問題,咱們該如何設計一個路由呢?固然是先去看看別人造好的輪子-。-,下面會列舉幾個我在開發中用到過,以及參考過的輪子,有拿來主義直接使用的,也有借鑑人家思想本身封裝的,總之都值得學習。
JLRoutes目前GitHub上star5.3k,應該是星最多的路由組件了,因此咱們第一個分析他的設計思路。
JLRGlobal_routeControllersMap
,這個map以scheme爲key,JLRoutes爲value,因此每個scheme都是惟一的。+ (instancetype)routesForScheme:(NSString *)scheme
{
JLRoutes *routesController = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
JLRGlobal_routeControllersMap = [[NSMutableDictionary alloc] init];
});
if (!JLRGlobal_routeControllersMap[scheme]) {
routesController = [[self alloc] init];
routesController.scheme = scheme;
JLRGlobal_routeControllersMap[scheme] = routesController;
}
routesController = JLRGlobal_routeControllersMap[scheme];
return routesController;
}
複製代碼
- (void)addRoute:(NSString *)routePattern priority:(NSUInteger)priority handler:(BOOL (^)(NSDictionary<NSString *, id> *parameters))handlerBlock
{
NSArray <NSString *> *optionalRoutePatterns = [JLRParsingUtilities expandOptionalRoutePatternsForPattern:routePattern];
JLRRouteDefinition *route = [[JLRGlobal_routeDefinitionClass alloc] initWithPattern:routePattern priority:priority handlerBlock:handlerBlock];
if (optionalRoutePatterns.count > 0) {
// there are optional params, parse and add them
for (NSString *pattern in optionalRoutePatterns) {
JLRRouteDefinition *optionalRoute = [[JLRGlobal_routeDefinitionClass alloc] initWithPattern:pattern priority:priority handlerBlock:handlerBlock];
[self _registerRoute:optionalRoute];
[self _verboseLog:@"Automatically created optional route: %@", optionalRoute];
}
return;
}
[self _registerRoute:route];
}
複製代碼
- (void)_registerRoute:(JLRRouteDefinition *)route
{
if (route.priority == 0 || self.mutableRoutes.count == 0) {
[self.mutableRoutes addObject:route];
} else {
NSUInteger index = 0;
BOOL addedRoute = NO;
// search through existing routes looking for a lower priority route than this one
for (JLRRouteDefinition *existingRoute in [self.mutableRoutes copy]) {
if (existingRoute.priority < route.priority) {
// if found, add the route after it
[self.mutableRoutes insertObject:route atIndex:index];
addedRoute = YES;
break;
}
index++;
}
// if we weren't able to find a lower priority route, this is the new lowest priority route (or same priority as self.routes.lastObject) and should just be added if (!addedRoute) { [self.mutableRoutes addObject:route]; } } [route didBecomeRegisteredForScheme:self.scheme]; } 複製代碼
- (BOOL)_routeURL:(NSURL *)URL withParameters:(NSDictionary *)parameters executeRouteBlock:(BOOL)executeRouteBlock
{
if (!URL) {
return NO;
}
[self _verboseLog:@"Trying to route URL %@", URL];
BOOL didRoute = NO;
JLRRouteRequestOptions options = [self _routeRequestOptions];
JLRRouteRequest *request = [[JLRRouteRequest alloc] initWithURL:URL options:options additionalParameters:parameters];
for (JLRRouteDefinition *route in [self.mutableRoutes copy]) {
// check each route for a matching response
JLRRouteResponse *response = [route routeResponseForRequest:request];
if (!response.isMatch) {
continue;
}
[self _verboseLog:@"Successfully matched %@", route];
if (!executeRouteBlock) {
// if we shouldn't execute but it was a match, we're done now
return YES;
}
[self _verboseLog:@"Match parameters are %@", response.parameters];
// Call the handler block
didRoute = [route callHandlerBlockWithParameters:response.parameters];
if (didRoute) {
// if it was routed successfully, we're done - otherwise, continue trying to route break; } } if (!didRoute) { [self _verboseLog:@"Could not find a matching route"]; } // if we couldn't find a match and this routes controller specifies to fallback and its also not the global routes controller, then...
if (!didRoute && self.shouldFallbackToGlobalRoutes && ![self _isGlobalRoutesController]) {
[self _verboseLog:@"Falling back to global routes..."];
didRoute = [[JLRoutes globalRoutes] _routeURL:URL withParameters:parameters executeRouteBlock:executeRouteBlock];
}
// 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;
}
複製代碼
CTMediator 目前github上star 3.3k ,這個庫特別的輕量級,只有一個類和一個category,一共也沒幾行代碼,更可的是做者還在關鍵代碼處添加了中文註釋
以及比較詳細的example
。
主要思想是利用Target-Action
,使用runtime
實現解耦。這種模式每一個組件之間互不依賴,可是都依賴中間件進行調度。
頭文件中暴露了兩個分別處理遠程App和本地組件調用的方法
// 遠程App調用入口
- (id _Nullable)performActionWithUrl:(NSURL * _Nullable)url completion:(void(^_Nullable)(NSDictionary * _Nullable info))completion;
// 本地組件調用入口
- (id _Nullable )performTarget:(NSString * _Nullable)targetName action:(NSString * _Nullable)actionName params:(NSDictionary * _Nullable)params shouldCacheTarget:(BOOL)shouldCacheTarget;
複製代碼
對於遠程App,還作了一步安全處理,最後解析完也是一樣調用了本地組件處理的方法中
- (id)performActionWithUrl:(NSURL *)url completion:(void (^)(NSDictionary *))completion
{
if (url == nil) {
return nil;
}
NSMutableDictionary *params = [[NSMutableDictionary alloc] init];
NSString *urlString = [url query];
for (NSString *param in [urlString componentsSeparatedByString:@"&"]) {
NSArray *elts = [param componentsSeparatedByString:@"="];
if([elts count] < 2) continue;
[params setObject:[elts lastObject] forKey:[elts firstObject]];
}
// 這裏這麼寫主要是出於安全考慮,防止黑客經過遠程方式調用本地模塊。這裏的作法足以應對絕大多數場景,若是要求更加嚴苛,也能夠作更加複雜的安全邏輯。
NSString *actionName = [url.path stringByReplacingOccurrencesOfString:@"/" withString:@""];
if ([actionName hasPrefix:@"native"]) {
return @(NO);
}
// 這個demo針對URL的路由處理很是簡單,就只是取對應的target名字和method名字,但這已經足以應對絕大部份需求。若是須要拓展,能夠在這個方法調用以前加入完整的路由邏輯
id result = [self performTarget:url.host action:actionName params:params shouldCacheTarget:NO];
if (completion) {
if (result) {
completion(@{@"result":result});
} else {
completion(nil);
}
}
return result;
}
複製代碼
對於無響應的請求還統一作了處理
- (void)NoTargetActionResponseWithTargetString:(NSString *)targetString selectorString:(NSString *)selectorString originParams:(NSDictionary *)originParams
{
SEL action = NSSelectorFromString(@"Action_response:");
NSObject *target = [[NSClassFromString(@"Target_NoTargetAction") alloc] init];
NSMutableDictionary *params = [[NSMutableDictionary alloc] init];
params[@"originParams"] = originParams;
params[@"targetString"] = targetString;
params[@"selectorString"] = selectorString;
[self safePerformAction:action target:target params:params];
}
複製代碼
具體使用須要下面幾個步驟 :
Target
類,以Target_
爲前綴命名
Action_
爲前綴命名。performTarget: action: params: shouldCacheTarget:
感興趣的還能夠看一下做者的文章,詳細介紹了CTMediator的設計思想以及爲已有項目添加CTMediator
iOS應用架構談 組件化方案
在現有工程中實施基於CTMediator的組件化方案
MGJRouter 目前github上star 2.2k
這個庫的由來:JLRoutes 的問題主要在於查找 URL 的實現不夠高效,經過遍歷而不是匹配。還有就是功能偏多。 HHRouter 的 URL 查找是基於匹配,因此會更高效,MGJRouter 也是採用的這種方法,但它跟 ViewController 綁定地過於緊密,必定程度上下降了靈活性。 因而就有了 MGJRouter。
/**
* 保存了全部已註冊的 URL
* 結構相似 @{@"beauty": @{@":id": {@"_", [block copy]}}}
*/
@property (nonatomic) NSMutableDictionary *routes;
複製代碼
MGJRouter是一個單例對象,在其內部維護着一個「URL -> block」格式的註冊表,經過這個註冊表來保存服務方註冊的block。使調用方能夠經過URL映射出block,並經過MGJRouter對服務方發起調用。
大概的使用流程以下:
PublicHeader
,在PublicHeader
中聲明外部能夠調用的一系列URL#ifndef MMCUserUrlDefines_h
#define MMCUserUrlDefines_h
/**
description 個人我的中心頁
@return MMCUserViewController
*/
#define MMCRouterGetUserViewController @"MMC://User/UserCenter"
/**
description 個人消息列表
@return MMCMessageListViewController
*/
#define MMCRouterGetMessageVC @"MMC://User/MMCMessageListViewController"
複製代碼
+ (void)registerGotoUserVC
{
[MMCRouter registerURLPattern:MMCRouterGetUserViewController toHandler:^(NSDictionary *params) {
}];
}
複製代碼
[MMCRouter openURL:MMCRouterGetUserViewController];
複製代碼
MGJRouter
還提供了能夠返回一個對象的方法+ (void)registerURLPattern:(NSString *)URLPattern toObjectHandler:(MGJRouterObjectHandler)handler
複製代碼
舉個例子:這個route就返回了一個控制器,能夠交給調用方自行處理。
+(void)registerSearchCarListVC{
[MMCRouter registerURLPattern:MMCRouterGetSearchCarListController toObjectHandler:^id(NSDictionary *params) {
NSDictionary *userInfo = [params objectForKey:MMCRouterParameterUserInfo];
NSString *carType = [userInfo objectForKey:MMCRouterCarType];
MMCSearchCarListViewController *vc = [[MMCSearchCarListViewController alloc] init];
vc.strCarType = carType;
return vc;
}];
}
複製代碼
根據上面介紹的MGJRouter
的使用,不難看出存在URL硬編碼和參數侷限性的問題,爲了解決這些問題,蘑菇街又提出來Protocol
方案。Protocol方案由兩部分組成,進行組件間通訊的ModuleManager
類以及MGJComponentProtocol
協議類。
經過中間件ModuleManager
進行消息的調用轉發,在ModuleManager內部維護一張映射表,映射表由以前的"URL -> block
"變成"Protocol -> Class
"。
由於目前手裏的項目沒有用到這個,因此使用代碼就不貼了,感興趣的能夠自行百度。
優勢:
缺點:
優勢:
缺點:
優勢:
缺點:
Target_ ,Action_
命名規則最後想說的是,沒有最好的route,只有最適合你項目的route,根據本身項目的實際狀況,分析決定要使用哪種組件化方案。