iOS 组件化 —— 路由设计思路分析_jlrroutedefinition-程序员宅基地

技术标签: 大数据入门到精通  APP  架构  MVC  iOS  

前言

随着用户的需求越来越多,对App的用户体验也变的要求越来越高。为了更好的应对各种需求,开发人员从软件工程的角度,将App架构由原来简单的MVC变成MVVM,VIPER等复杂架构。更换适合业务的架构,是为了后期能更好的维护项目。

但是用户依旧不满意,继续对开发人员提出了更多更高的要求,不仅需要高质量的用户体验,还要求快速迭代,最好一天出一个新功能,而且用户还要求不更新就能体验到新功能。为了满足用户需求,于是开发人员就用H5,ReactNative,Weex等技术对已有的项目进行改造。项目架构也变得更加的复杂,纵向的会进行分层,网络层,UI层,数据持久层。每一层横向的也会根据业务进行组件化。尽管这样做了以后会让开发更加有效率,更加好维护,但是如何解耦各层,解耦各个界面和各个组件,降低各个组件之间的耦合度,如何能让整个系统不管多么复杂的情况下都能保持“高内聚,低耦合”的特点?这一系列的问题都摆在开发人员面前,亟待解决。今天就来谈谈解决这个问题的一些思路。

目录

  • 1.引子
  • 2.App路由能解决哪些问题
  • 3.App之间跳转实现
  • 4.App内组件间路由设计
  • 5.各个方案优缺点
  • 6.最好的方案

一. 引子

大前端发展这么多年了,相信也一定会遇到相似的问题。近两年SPA发展极其迅猛,React 和 Vue一直处于风口浪尖,那我们就看看他们是如何处理好这一问题的。

在SPA单页面应用,路由起到了很关键的作用。路由的作用主要是保证视图和 URL 的同步。在前端的眼里看来,视图是被看成是资源的一种表现。当用户在页面中进行操作时,应用会在若干个交互状态中切换,路由则可以记录下某些重要的状态,比如用户查看一个网站,用户是否登录、在访问网站的哪一个页面。而这些变化同样会被记录在浏览器的历史中,用户可以通过浏览器的前进、后退按钮切换状态。总的来说,用户可以通过手动输入或者与页面进行交互来改变 URL,然后通过同步或者异步的方式向服务端发送请求获取资源,成功后重新绘制 UI,原理如下图所示:

react-router通过传入的location到最终渲染新的UI,流程如下:

location的来源有2种,一种是浏览器的回退和前进,另外一种是直接点了一个链接。新的 location 对象后,路由内部的 matchRoutes 方法会匹配出 Route 组件树中与当前 location 对象匹配的一个子集,并且得到了 nextState,在this.setState(nextState) 时就可以实现重新渲染 Router 组件。

大前端的做法大概是这样的,我们可以把这些思想借鉴到iOS这边来。上图中的Back / Forward 在iOS这边很多情况下都可以被UINavgation所管理。所以iOS的Router主要处理绿色的那一块。

二. App路由能解决哪些问题

既然前端能在SPA上解决URL和UI的同步问题,那这种思想可以在App上解决哪些问题呢?

思考如下的问题,平时我们开发中是如何优雅的解决的:

1.3D-Touch功能或者点击推送消息,要求外部跳转到App内部一个很深层次的一个界面。

比如微信的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之间跳转实现

在谈App内部的路由之前,先来谈谈在iOS系统间,不同App之间是怎么实现跳转的。

1. URL Scheme方式

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 汇总列表

2. Universal Links方式

虽然在微信内部开网页会禁止所有的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的这种方式实现么?

四. App内组件间路由设计

上一章节中我们介绍了iOS系统中,系统是如何帮我们处理App间跳转逻辑的。这一章节我们着重讨论一下,App内部,各个组件之间的路由应该怎么设计。关于App内部的路由设计,主要需要解决2个问题:

1.各个页面和组件之间的跳转问题。 2.各个组件之间相互调用。

先来分析一下这两个问题。

1. 关于页面跳转

在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个很重要的目的:

  1. 代码高复用率
  2. 方便进行单元测试

如果需要测试一个业务是否正确,我们只要对ViewModel进行单元测试即可。前提是假定我们使用ReactiveCocoa进行UI绑定的过程是准确无误的。目前绑定是正确的。所以我们只需要单元测试到ViewModel即可完成业务逻辑的测试。

页面跳转也属于业务逻辑,所以应该放在ViewModel中一起单元测试,保证业务逻辑测试的覆盖率。

把页面跳转放到ViewModel中,有2种做法,第一种就是用路由来实现,第二种由于和路由没有关系,所以这里就不多阐述,有兴趣的可以看lpd-mvvm-kit这个库关于页面跳转的具体实现。

页面跳转相互的耦合性也就体现出来了:

1.由于pushViewController或者presentViewController,后面都需要带一个待操作的ViewController,那么就必须要引入该类,import头文件也就引入了耦合性。 2.由于跳转这里写死了跳转操作,如果线上一旦出现了bug,这里是不受我们控制的。 3.推送消息或者是3D-Touch需求,要求直接跳转到内部第10级界面,那么就需要写一个入口跳转到指定界面。

2. 关于组件间调用

关于组件间的调用,也需要解耦。随着业务越来越复杂,我们封装的组件越来越多,要是封装的粒度拿捏不准,就会出现大量组件之间耦合度高的问题。组件的粒度可以随着业务的调整,不断的调整组件职责的划分。但是组件之间的调用依旧不可避免,相互调用对方组件暴露的接口。如何减少各个组件之间的耦合度,是一个设计优秀的路由的职责所在。

3. 如何设计一个路由

如何设计一个能完美解决上述2个问题的路由,让我们先来看看GitHub上优秀开源库的设计思路。以下是我从Github上面找的一些路由方案,按照Star从高到低排列。依次来分析一下它们各自的设计思路。

(1)JLRoutes Star 3189

JLRoutes在整个Github上面Star最多,那就来从它来分析分析它的具体设计思路。

首先JLRoutes是受URL Scheme思路的影响。它把所有对资源的请求看成是一个URI。

首先来熟悉一下NSURLComponent的各个字段:

Note The URLs employed by the NSURL  class are described in RFC 1808RFC 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

(2)routable-ios Star 1415

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的大致流程图解如下:

(3)HHRouter Star 1277

这是布丁动画的一个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_R
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/qq_39658251/article/details/80100303

智能推荐

02、江苏专转本(专业课笔记)第二章、计算机的组成原理_集中式控制系统从计算机的()到存储器、外部设备、过程i/o等所有部分都是通过计算-程序员宅基地

文章浏览阅读2.3k次,点赞3次,收藏23次。文章目录一、计算机的组成原理1、认识计算机1.1、认识1-4代计算机1.2、计算机应用的四种模式1.3、计算机特性以及未来发展2、计算机组成2.1、计算机逻辑组成与现代化硬件组成2.2、各种设备介绍2.3、计算机分类二、CPU的结构与原理1、原理介绍1.1、冯诺依曼计算机结构与原理1.2、存储程序控制原理2、了解CPU2.1、CPU任务介绍2.2、CPU结构与任务2.3、指令与指令系统2.4、CPU性能指标及影响性能因素2.5、提高CPU性能措施3、扩展Intel公司CPU发展史怎么认识双核、四核处理器?三_集中式控制系统从计算机的()到存储器、外部设备、过程i/o等所有部分都是通过计算

超简单java环境配置(2021版)_java21配置环境教程-程序员宅基地

文章浏览阅读6.7k次,点赞10次,收藏10次。①找到 高级系统设置②点击环境变量在系统环境变量 区块中点击新建按钮配置1:添加下图配置变量名为:JAVA_HOME变量值为:jdk的安装路径 我的为:D:\Program Files\Java\jdk1.8.0_152配置2:继续新建系统环境变量(和我的保持一致)系统名:CLASSPATH变量值:.;%JAVA_HOME%\lib;%JAVA_HOME%\lib\tools.jar配置3:双击系统环境中的Path在弹出的窗口中点击新建 然后加入: %JAVA_HOME%\_java21配置环境教程

XAML四大原则及五种常见布局_简述声明一个xaml元素必须遵循的4大原则-程序员宅基地

文章浏览阅读1.7k次。xaml介绍xaml就是微软为构建应用程序界面而创建的一种描述性语言一种编程语言xaml里面的控件相当于类,控件的属性相当于封装的对象XAML必须遵循的4大原则:XAML是大小写区分的,元素和属性的名称必须严格区分大小写。所有的属性值,无论它是什么数据类型,都必须包含在双引号中;所有的元素都必须是封闭的;如<Button …/> …最终的XAML文件也必须是合适的XML文档。**常用的五种布局:**Canvas Grid StackPanel DockPanel W_简述声明一个xaml元素必须遵循的4大原则

pycharm安装教程,超详细-程序员宅基地

文章浏览阅读10w+次,点赞1.5k次,收藏8.2k次。在安装pycharm之前,确保你的电脑配置好了python,没有下载安装的可以去看我的文章—>>>python安装详细教程pycharm windows版本下载地址:https://www.jetbrains.com/pycharm/download/#section=windows下载社区版本(日常学习使用够用了),专业版是收费的哦(功能更强大)下载完后打开安装程序,点击next选择一个路径然后next把这些选项都勾选上。点击Install安装完后点击Finish_pycharm安装教程

收藏 10款内网穿透工具_虚拟机内网穿透的软件-程序员宅基地

文章浏览阅读140次。首先,我们生活中的网络从应用上可以分为内网和外网;内网就是你自己的网络环境,就你自己能访问,比如你本地测试进行的localhost;外网就不言而喻了,你看网页,视频等这些网址都是外网。那么什么又是内网穿透呢?简单的说就是通过访问一个外网地址,然后穿透到你的内网地址。内网穿透又叫端口映射,用一句最简单的话来讲就是:将你的计算机所连接的私有网络映射到公网上,别人通过你给的域名或ip即可访问你本地的服务。_虚拟机内网穿透的软件

向中级程序员转变必备的10个秘诀-程序员宅基地

文章浏览阅读427次。在一封与TechRepublic会员交流的邮件当中,提到了面向程序员的博客、文章及杂志分成两类:面向初学者类以及面向专家类。这个观点很好,有关程序员如何从初级跃升到中级的信息极少。以下是为了实现这种转变需要你去做的10件事。   1.学习另一门语言 其实你学的是哪一门语言并没有关系,但是学习另一门语言(不管你已经了解多少种语言)将把你打造为更好的程序员。能学会一门与你日常使用的语言风格迥异的 语言则更佳。打个比方,如果你是C#程序员,学习VB.NET或者Java对你的帮助

随便推点

UE4 材质学习笔记_ue4 材质local offset-程序员宅基地

文章浏览阅读553次。常用节点Lerp在A和B之间根据Alpha的值进行插值运算FlipBook制作类似幻动片一样的,根据UV坐标一张一张进行播放通过控制他的行数列数动画的相位来控制UV的切换TwoSidedSign常量表达式用翻转法线,表示双面材质的正反面Saturate用于把输入的值钳制的0和1之间Noise实用表达式用于生成一个程序化的噪波,这个可以进行控制,通过设置参数VertexNormalWS坐标表达式这节点输出世界空间下的顶点的法线LocalPositio_ue4 材质local offset

postman实现传递session给后端_postman session设置session数据-程序员宅基地

文章浏览阅读6.9k次。https://blog.csdn.net/m0_37166734/article/details/80765326用过postman的人都知道,postman可以模拟各种http请求,对于前后端分离的开发特别有好处。开发中遇到这样的问题,登录信息保存在session中,拦截器会判断session中的值,为空则不允许访问,但我们在postman中模拟登录后,想进行下一步操作,..._postman session设置session数据

借助Flex实现SysY词法分析-程序员宅基地

文章浏览阅读2.6k次,点赞12次,收藏38次。词法分析(lexical analysis)是编译器的第一阶段,主要是将代码的字符序列转换为token的过程。简单地来说,就是对代码进行切块的一个过程,并将每一块添加上其所属的类别标签。比如说int asd=897;,其词法分析的结果即为int :< TYPE , ‘int’ >asd :< ID , ‘ast’ >= ..._sysy词法分析

【Hyperledger Fabric 2.2】在已有组织中添加peer节点_fabric 只能合约 添加节点-程序员宅基地

文章浏览阅读395次。hyperledger fabric 2.2 添加节点_fabric 只能合约 添加节点

[转] 字符集编码(GBK,BIG5,UNICODE)与C++的string/wstring-程序员宅基地

文章浏览阅读248次。 GBK,BIG5等字符集编码范围的具体说明 一 预备知识1,字符:字符是抽象的最小文本单位。它没有固定的形状(可能是一个字形),而且没有值。“A”是一个字符,“€”(德国、法国和许多其他欧洲国家通用货币的标志)也是一个字符。“中”“国”这是两个汉字字符。字符仅仅代表一个符号,没有任何实际值的意义。2,字符集:字符集是字符的集合。例如,汉字字符是中国人最先发明的字符,在中文、日文、..._big5转unicode 转码表

A3 STM32_HAL库函数 之 ADC通用驱动器 -- B -- 所有函数的介绍及使用-程序员宅基地

文章浏览阅读575次,点赞11次,收藏23次。以上就是A3 STM32_HAL库函数 之 ADC通用驱动器 – B – 所有函数的介绍及使用的内容。有不明白的地方欢迎留言;有建议欢迎留言,我后面编写文档好改进。创作不容,如果文档对您有帮助,记得给个赞。

推荐文章

热门文章

相关标签