前言
随着用户的需求越来越多,对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个很重要的目的:
- 代码高复用率
- 方便进行单元测试
如果需要测试一个业务是否正确,我们只要对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 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
(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