iOS组件化-路由设计分析

组件化也是一个老生常谈的话题了,本文主要说一下在组件化中,站比较重要的位置的路由设计
你的项目里多是直接依赖了三方的路由组件,也多是本身根据项目的实际需求私人订制了一套路由组件,下面我想经过几个呼声比较高的三方组件来聊一聊路由的设计和分析。这里不推荐说你们用哪一个好哪一个很差,只是学习他们设计思想。就比如咱们看三方库源码,应该都是学习编程和设计的思想为主。html

前言

随着App的需求愈来愈多,业务愈来愈复杂,为了更高效的迭代以及提升用户体验,下降维护成本,对一个更高效的框架的需求也愈来愈急切。
因此咱们可能都经历过项目的重构、组件化,根据项目的实际需求,新的框架可能须要横向,纵向不一样粒度的分层,为了之后更有效率的开发和维护。随之而来的一个问题,如何保持“高内聚,低耦合”的特色,下面就来谈谈解决这个问题的一些思路。git

路由能解决哪些问题

列举几个平时开发中遇到的问题,或者说是需求:github

  1. 推送消息,或是网页打开一个url须要跳转进入App内部的某个页面
  2. 其余App,或者本身公司的别的App之间的相互跳转
  3. 不一样组件之间页面的跳转
  4. 如何统一两端的页面跳转逻辑
  5. 线上某个页面出现bug,如何能降级成一个其余的H5或者错误页面
  6. 页面跳转埋点
  7. 跳转过程当中的逻辑检查

以上这些问题,均可以经过设计一个路由来解决,下面带着这些问题继续看如何实现跳转。编程

实现跳转

经过上面的问题,咱们但愿设计一套路由,实现App外部和内部的统一跳转,因此先说一下App外部跳转的实现。数组

URL Scheme

在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

Universal links这个功能可使咱们的App经过http连接来启动。app

  • 若是安装过App,不论是在Safari仍是其余三方浏览器或别的软件中,均可以打开App
  • 若是没安装过,就会打开网页

设置方式:

注意必需要 applinks:开头

以上就是iOS系统中App间跳转的二种方式。

路由设计

说完App间的跳转逻辑,接下来就进入重点,App内部的路由设计。
主要要解决两个问题:

  • 各个组件之间相互调用,随着业务愈来愈复杂,若是组件化的粒度不太合适,会致使组件愈来愈多,组件间不可避免的依赖也愈来愈多
  • 页面和他所在组件之间的调用,组件内例如push一个VC ,就须要import这个类,从而致使强依赖,这样写死的代码也没法在出现线上bug的时候降级为其余页面

综合上面所说的两个问题,咱们该如何设计一个路由呢?固然是先去看看别人造好的轮子-。-,下面会列举几个我在开发中用到过,以及参考过的轮子,有拿来主义直接使用的,也有借鉴人家思想本身封装的,总之都值得学习。

Route分析

JLRoutes

JLRoutes目前GitHub上star5.3k,应该是星最多的路由组件了,因此咱们第一个分析他的设计思路。

  1. JLRoutes维护了一个全局的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;
}
复制代码
  1. scheme能够看作是一个URI,每个注册进来的字符串都会进行切分处理
- (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];
}
复制代码
  1. 按优先级插入JLRoutes的数组中,优先级高的排列在前面
- (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]; } 复制代码
  1. 查找路由
    根据URL初始化一个JLRRouteRequest,而后在JLRoutes的数组中依次查找,直到找到一个匹配的而后获取parameters,执行Handler
- (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

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_为前缀命名。
  • 为每个组件建立一个CTMediator的category
    这个category就是供调用方依赖完成调度的,这样category中全部方法的调用就很统一,所有是performTarget: action: params: shouldCacheTarget:
  • 最终的调度逻辑都交给CTMediator
  • 调用方只须要依赖有调度需求的组件的category

感兴趣的还能够看一下做者的文章,详细介绍了CTMediator的设计思想以及为已有项目添加CTMediator
iOS应用架构谈 组件化方案
在现有工程中实施基于CTMediator的组件化方案

MGJRouter

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"
复制代码
  • 在组件内部实现block的注册工做,调用方经过URL对block发起调用
+ (void)registerGotoUserVC
{
    [MMCRouter registerURLPattern:MMCRouterGetUserViewController toHandler:^(NSDictionary *params) {
    
    }];
}
复制代码
  • 经过openURL调用,能够经过GET请求的方式在url后面拼接参数,也能够经过param传入一个字典
[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;
    }];
}
复制代码
Protocol-class方案

根据上面介绍的MGJRouter的使用,不难看出存在URL硬编码和参数局限性的问题,为了解决这些问题,蘑菇街又提出来Protocol方案。Protocol方案由两部分组成,进行组件间通讯的ModuleManager类以及MGJComponentProtocol协议类。

经过中间件ModuleManager进行消息的调用转发,在ModuleManager内部维护一张映射表,映射表由以前的"URL -> block"变成"Protocol -> Class"。

由于目前手里的项目没有用到这个,因此使用代码就不贴了,感兴趣的能够自行百度。

优缺点

URL注册方案

优势:

  1. 最容易想到的最简单的方式
  2. 能够统一三端的调度
  3. 线上bug动态降级处理

缺点:

  1. 硬编码,URL须要专门管理
  2. URL规则须要提早注册
  3. 有常驻内存,可能发生内存问题

Protocol-Class方案

优势:

  1. 无硬编码
  2. 参数无限制,甚至能够传递model

缺点:

  1. 增长了新的中间件以及不少protocol,调用编码复杂
  2. route的跳转逻辑分散在各个类中,很差维护

Target-Action方案

优势:

  1. 利用runtime实现解耦调用,无需注册
  2. 有必定的安全处理
  3. 统一App外部和组件间的处理

缺点:

  1. 须要为每个组件另外建立一个category,做者建议category也是一个单独的pod
  2. 调用内部也是硬编码,要求Target_ ,Action_ 命名规则

最后想说的是,没有最好的route,只有最适合你项目的route,根据本身项目的实际状况,分析决定要使用哪种组件化方案。

相关文章
相关标签/搜索