组件化也是一个老生常谈的话题了,本文主要说一下在组件化中,站比较重要的位置的路由设计
。
你的项目里可能是直接依赖了三方的路由组件,也可能是自己根据项目的实际需求私人订制了一套路由组件,下面我想通过几个呼声比较高的三方组件来聊一聊路由的设计和分析。这里不推荐说大家用哪个好哪个不好,只是学习他们设计思想
。就好比我们看三方库源码,应该都是学习编程和设计的思想为主。
随着App的需求越来越多,业务越来越复杂,为了更高效的迭代以及提高用户体验,降低维护成本,对一个更高效的框架的需求也越来越急切。
所以我们可能都经历过项目的重构、组件化,根据项目的实际需求,新的框架可能需要横向,纵向不同粒度的分层,为了以后更有效率的开发和维护。随之而来的一个问题,如何保持“高内聚,低耦合”的特点,下面就来谈谈解决这个问题的一些思路。
列举几个平时开发中遇到的问题,或者说是需求:
以上这些问题,都可以通过设计一个路由来解决,下面带着这些问题继续看如何实现跳转。
通过上面的问题,我们希望设计一套路由,实现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的调用,可以根据需要做一些拦截或者其他操作。
App也是可以直接跳转到系统设置
的。比如有些需求要求检测用户有没有开启某些系统权限,如果没有开启就弹框提示,点击弹框的按钮直接跳转到系统设置里面对应的设置界面。
Universal links
这个功能可以使我们的App通过http链接来启动。
设置方式:
注意必须要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,根据自己项目的实际情况,分析决定要使用哪一种组件化方案。