带你一步步构建iOS路由

带你一步步构建iOS路由_第1张图片

接上一篇移动端路由层设计,这一篇是实战篇,手把手的带你编写一个简单的路由组件。有朋友说很多人都收藏以后就再也没看过,其实这属于时间管理问题,在你忙碌的工作和生活的时候,有时候需要你稍微停顿一下,思考一下,例如,你可以把本篇文章收藏以后再在iPhone的提醒事项里加入到一个阅读清单里,不用设置提醒,只需要在你闲的时候抽出一两个小时,看一下。想象一下你自己动手从发现问题到解决问题再到做出一个解决问题的组件的过程给你带来的成就感和获取的进阶经验,再稍微改变一下你对每天需要处理的繁杂事物的管理方式,也许你的生活和工作就会豁然开朗。

这个路由究竟是什么鬼?能解决什么问题?

举一些场景来看看

场景1:一个App项目中团队人员比较多,不同的人负责不同的模块开发,有的人直接使用资源文件设计的,有的人用代码直接写的,有的人负责登录,有的人负责订单,突然有一天搞订单的开发A找搞登录的开发B说要调一下登录,登录成功以后你要再回调下我写的模块的方法告诉我成功登录,我要刷新一下订单页面,B傻傻的就答应了,找B的人C、D、F....越来越多,B负责的代码越写越多,同时A也不怎么开心,因为A发现调B写的登录要通过类实例化函数获取模块,调C写的支付使用工厂方法,调D写的计算器组件又是另外一种写法,结果A自己的代码也越来越丑。

场景2:一个App里面有很多内嵌的H5页面,缠品A对猿B说,我们的活动页面要调用一下我们的订单页面,用户如果下了一个订单成功以后H5要能够拿到反馈有欢迎语,猿B和H5的开发猿C经过很久很久的讨论,确定了H5如果调用App的订单页面,参数怎么传,订单提交以后怎么再调H5的接口,参数怎么定义,各自把代码写到各自的项目里,没过多久缠品A说另外的H5要调用原生的界面,怎么怎么个流程,推送点击要调用原生的某个页面,点完要反馈给后台统计,兄弟App要跳转到我们的App某个页面跳转完成某个动作以后要再跳转回去......猿B每每接到这样的需求就紧紧握住自己中箭的膝盖,收拾了一下写的那么多代码,深藏功与名.......

出了什么问题?

我想上面的两个场景出现的问题大家或多或少都会遇见,总结一下就是:

  1. 因为不同人负责不同模块,调用他人必须了解他人编写的模块如何调用,对象是啥,初始化方式是啥,这违背了面向对象的封装原则
  2. 引入不同的模块头文件,多了以后,所依赖的外部发生一丁点变化你就要跟着变,逻辑变得越来越耦合,不利于维护
  3. 调用不同模块要反复与他人沟通传参、回调流程、接口定义等等,沟通效率低下
  4. 产品提出各种需求,但是我写的代码都是差不多的,来一个页面我需要写一些相同逻辑的代码,而且产品还抱怨每次加相同的东西就要改代码发版,这显然不能满足复用的要求。

总结:
依赖多、耦合高、复用低。
可我们都知道有这么句话啊:高内聚、低耦合,职责单一逻辑清晰。

路由就是解决上面的问题

我们已经发现依赖比较大是因为要导入其他模块的头文件,了解其他模块的逻辑和定义,如果多了,你的代码中引入的头文件或者导入的包名越来越多,改一下牵一发而动全身啊。大概是这个样子:

带你一步步构建iOS路由_第2张图片
相互引用

依赖的问题很严重,要想破除这样的依赖,我们能想到的办法就是找个调度中心去做这件事,其实各个业务模块并不关心其他模块具体的业务逻辑是什么,也不需要知道这个模块如何获取,我只关心怎么调用和反馈的结果,而这个有了调度中心这个东西,每个模块不需要依赖其他模块,只需要调度中心关心每个模块的调度。


带你一步步构建iOS路由_第3张图片
引入中介者

有了Route这个调度中心,每个模块就不用写那么多重复的耦合代码了,也不需要在导入那么多头文件了和引入那么多包名了,这些蓝色的箭头代表着调用方式,如果调用方式再统一一下,沟通效率就提升上去了,因为我们可以用一套约定好的数据协议来代替重复沟通,有时候我们需要靠约定和协议来提高我们的工作效率。

Tips:
发现问题这个环节很重要,你在工作中经常要反复做的,浪费时间的都是需要你去优化和花大力气去解决的,作为一个专业人士,不断改进你的代码,优化你的工作流程,带动团队向好的协作方式去转型,这是专业人士的习惯,更应该成为你的习惯。同时针对代码存在的问题,也许你经常会隐隐约约感到有问题,就是不知道问题在什么地方,那么需要问问自己有没有以下情况:哪些代码是经常写且重复度很高的,是不是可以抽象出来?哪些代码需要反复的变动,是不是可以做成配置或者是定义一套数据格式来满足动态兼容?有没有一些现成的设计模式可以解决这些问题?比方说,调度中心则使用的是中介者模式。我见过

为啥要说iOS路由呢?

路由层其实在逻辑功能上的设计都是一样的,很多人把App中的视图切换当做是路由组件的功能职责,这点我持否定态度,从单一职责角度和MVC框架分析来看,视图切换属于View中的交互逻辑并不属于消息传递或者是事件分发的范畴,但路由请求、视图转场的实现部分与Android平台和iOS平台上的导航机制有着非常紧密的关系,Android操作系统有着天然的架构优势,Intent机制可以协助应用间的交互与通讯,是对调用组件和数据传递的描述,本身这种机制就解除了代码逻辑和界面之间的依赖关系,只有数据依赖。而iOS的界面导航和转场机制则大部分依赖UI组件各自的实现,所以如何解决这个问题,iOS端路由的实现则比较有代表性。
其实说白一点,路由层解决的核心问题就是原来界面或者组件之间相互调用都必须相互依赖,需要导入目标的头文件、需要清楚目标对象的逻辑,而现在全部都通过路由中转,只依赖路由或者某种通讯协议,或者依靠一些消息传递机制连路由都不依赖。其次,路由的核心逻辑就是目标匹配,对于外部调用的情况来说,URL如何匹配Handler是最为重要的,匹配就必然用到正则表达式。了解这些关键点以后就有了设计的目的性,let‘s do it~

总结一下这个路由都要有什么?(需求分析)

我们先根据上面的模糊的总结梳理一下:

  1. 路由需要能够实现被其他模块调度,从而调度另外一个模块
  2. 接入路由的模块不需要知道目标模块的实现
  3. 调度发起方需要有目标的响应回调,类似于http请求,有一个request就要有一个response,才能实现双向的调用
  4. 调用方式需要统一,统一而松散的调用协议和数据协议可以减少大量接入成本和沟通成本
    那一个完整的调度流程应该是这样的:
带你一步步构建iOS路由_第4张图片
Route流程

看到这个流程以后,可以确定以下几件事:

  1. A模块调用路由,为表达自己需要调用的是B模块,考虑到H5、推送以及其他App的外部调用,可以使用URL这种方式来定义目标,也就是说用URL来表示目标B
  2. 对一个URL的请求来说,路由需要有统一的回调处理,当然,如果不需要回调也是可以的,回调是需要目标去触发的
  3. 路由要有处理URL的功能,并调用其他模块的能力

根据以上粗略的定义一下路由的框架:

带你一步步构建iOS路由_第5张图片
框架类图

这里面以供有4部分:
WLRRouter就是一个实体对象,用来提供给其他模块调用。
WLRRouteRequest是一个以URL为基础的实体对象,为什么不直接用URL字符串?因为考虑到如果路由在内部调用其他模块的时候需要传入一些原生对象,而URL上只能携带类型单一的字符串键值对表示参数,所以需要使用这么一个对象进行包装。
WLRRouteHandler是一个处理某一个WLRRouteRequest请求的对象,当路由接收一个WLRRouteRequest请求,转发给一个WLRRouteHandler处理,处理完毕以后如果有回调,则回调给调用者。URL的请求与Handler的对应关系肯定需要匹配的逻辑,为了使得路由内部逻辑更加清晰单独使用WLRRouteMatcher来处理匹配的逻辑。

深入具体需求,细化功能实现(详细设计)

有了粗略的需求分析接下来就是细化需求并给出详细设计的阶段了,其实编写一个模块要有系统性思维,粗略的需求里面包含了整个模块要实现的主要核心功能,核心流程是什么,要有哪几个类才能实现这样的流程,不要妄图一下子深入到细枝末节上,让细节左右宏观上的逻辑架构,大脑不适合同时考虑宏观和微观的事情,尤其是对经验不太足的开发者来说,要逐渐学会大脑在不同的时期进行宏观和微观的无缝切换,这样才能专注目标和结果,在实现过程中再投入全部精力考虑细节,才能保证具体的实现是不偏离总体目标的。

WLRRouteRequest设计

路由层的请求,无论是跨应用的外部调用(H5调用、其他App调用)还是内部调用(内部模块相互调用),最后都要形成一个路由请求,一个以URL为基础的request对象,首先需要有携带URL,再一个要携带请求所需要的参数,参数有三种,一种是Url上的键值对参数,一种是RESTFul风格的Url上的路径参数,一种是内部调用适用的原生参数,具体是:

带你一步步构建iOS路由_第6张图片
WLRRouteRequest

这里说一下路径参数,很多有后端开发经验的人都知道,一个url上传递参数,或者是匹配后端服务的service,Url的路径对于表达转发语义十分重要,比方说 :
http://aaaa.com/login
http://aaaa.com/userCenter
那Url中的login和userCenter可以代表是哪个后端服务,那路由就需要设置正则匹配表达式去匹配 http://aaaa.com/ 这部分,截取login、userCenter部分,说回我们的路由,App的路由需要通过设置Url的正则表达式来获取路径参数,同时我们必须知道这些参数的值和名称,那么我可以这样定义Url匹配的表达式
scheme://host/path/:name([a-zA-Z_-]+)
熟悉正则表达式的孩子都知道分组模式,path后name是key,([a-zA-Z_-]+)是规定name对应的value应该是什么格式的。那么routeParameters就是存放路径参数的

//url
@property (nonatomic, copy, readonly) NSURL *URL;
//url上?以后的键值对参数
@property (nonatomic, copy, readonly) NSDictionary *queryParameters;
//url上匹配的路径参数
@property (nonatomic, copy, readonly) NSDictionary *routeParameters;
//原生参数,比方说要传给目标UIImage对象,NSArray对象等等
@property (nonatomic, copy, readonly) NSDictionary *primitiveParams;
//目标预留的callBack block,当完成处理以后,回到此Block,完成调用者的回调
@property(nonatomic,copy)void(^targetCallBack)(NSError *error,id responseObject);
//是否消费掉,一个request只能处理一次,该字段反应request是否被处理过
@property(nonatomic)BOOL isConsumed;

WLRRouteHandler设计

handler对象要接收一个WLRRouteRequest对象来启动处理流程,前面经过我们的分析,这个handler应该担负起通过url和参数获取目标对象的职责,在一般的route处理中,目标往往是一个视图控制器,先实现这样一个通过url调用某一个视图控制器的并跳转处理的handler,那么应该是如下的:

带你一步步构建iOS路由_第7张图片
WLRRouteHandler

handler处理一个request请求是一个具有过程性的逻辑,WLRRouteHandler要作为一个基类,我们知道,这个handler在需要处理获取目标视图控制器->参数传递给目标视图控制器->视图控制器的转场->完成回调,那么我们需要设计这样的接口

//即将开始处理request请求,返回值决定是否要继续相应request
- (BOOL)shouldHandleWithRequest:(WLRRouteRequest *)request;
//开始处理request请求
-(BOOL)handleRequest:(WLRRouteRequest *)request error:(NSError *__autoreleasing *)error;
// 根据request获取目标控制器
-(UIViewController *)targetViewControllerWithRequest:(WLRRouteRequest *)request;
//转场一定是从一个视图控制器跳转到另外一个视图控制器,该方法用以获取转场中的源视图控制器
-(UIViewController *)sourceViewControllerForTransitionWithRequest:(WLRRouteRequest *)request;
//改方法内根据request、获取的目标和源视图控制器,完成转场逻辑
-(BOOL)transitionWithWithRequest:(WLRRouteRequest *)request sourceViewController:(UIViewController *)sourceViewController targetViewController:(UIViewController *)targetViewController isPreferModal:(BOOL)isPreferModal error:(NSError *__autoreleasing *)error;
//根据request来返回是否是模态跳转
- (BOOL)preferModalPresentationWithRequest:(WLRRouteRequest *)request;

WLRRouteMatcher设计

一个matcher应该具有根据url和参数判断是否匹配某个url表达式的逻辑

带你一步步构建iOS路由_第8张图片
WLRRouteMatcher

matcher对象必须拥有url的匹配表达式,类似于 scheme://host/path/:name([a-zA-Z_-]+) ,也有拥有该表达式真正的正则表达式,^ scheme://host/path/([a-zA-Z_-]+)$

@interface WLRRouteMatcher : NSObject
//url匹配表达式
@property(nonatomic,copy)NSString * routeExpressionPattern;
//url匹配的正则表达式
@property(nonatomic,copy)NSString * originalRouteExpression;
+(instancetype)matcherWithRouteExpression:(NSString *)expression;
-(WLRRouteRequest *)createRequestWithURL:(NSURL *)URL primitiveParameters:(NSDictionary *)primitiveParameters targetCallBack:(void(^)(NSError *, id responseObject))targetCallBack;

设计-(WLRRouteRequest *)createRequestWithURL:(NSURL *)URL primitiveParameters:(NSDictionary *)primitiveParameters targetCallBack:(void(^)(NSError *, id responseObject))targetCallBack;这个方法,可以通过传入url和参数,检查是否返回request请求,来表示该WLRRouteMatcher对象所拥有的匹配表达式与url是否能够匹配,这句话有点绕,看不懂的多看几遍。

WLRRouter

WLRRouter是路由实体对象,后端开发者对于路由挂载的概念非常了解,其实这样一个路由实体对象可以完成对URL的拦截和处理并返回结果,事实上,根据前面的梳理和总结,WLRRouter对象内部应该保存了需要匹配拦截的URL表达式,而前面我们知道Url的匹配表达式是存储在WLRRouteMatcher对象中的,并且一个Url传入检查是否匹配也是Matcher对象提供的功能,对于匹配上的Url需要有对应的Handler处理,所以Router对象的内部存在Machter对象和Handler对象一一对应的关系,并且拥有注册Url表达式对应到Handler的功能,也具有传入Url和参数就能匹配到Handler的功能,还要有一个检测Url是否能有对应Handler处理的功能,所以应该是:

带你一步步构建iOS路由_第9张图片
WLRRouter

这里有两种注册的方法,注册handler的就不需再多描述,另外一个是注册Block的回调形式,因为有时候可能会需要一些简单的Url拦截,去做一些事情,这里面的Block需要返回一个request对象,这是因为,如果Block没有对request的回调做处理,Router应该处理调用者的回调问题,否则就会出现调用者设置了回调的Block而没有人调用回来,这样就尴尬了。

/**
 注册一个route表达式并与一个block处理相关联
 
 @param routeHandlerBlock block用以处理匹配route表达式的url的请求
 @param route url的路由表达式,支持正则表达式的分组,例如app://login/:phone({0,9+})是一个表达式,:phone代表该路径值对应的key,可以在WLRRouteRequest对象中的routeParameters中获取
 */
-(void)registerBlock:(WLRRouteRequest *(^)(WLRRouteRequest * request))routeHandlerBlock forRoute:(NSString *)route;
/**
 注册一个route表达式并与一个block处理相关联
 
 @param routeHandlerBlock handler对象用以处理匹配route表达式的url的请求
 @param route url的路由表达式,支持正则表达式的分组,例如app://login/:phone({0,9+})是一个表达式,:phone代表该路径值对应的key,可以在WLRRouteRequest对象中的routeParameters中获取
 */
-(void)registerHandler:(WLRRouteHandler *)handler forRoute:(NSString *)route;

/**
 检测url是否能够被处理,不包含中间件的检查

 @param url 请求的url
 @return 是否可以handle
 */
-(BOOL)canHandleWithURL:(NSURL *)url;
/**
 处理url请求

 @param URL 调用的url
 @param primitiveParameters 携带的原生对象
 @param targetCallBack 传给目标对象的回调block
 @param completionBlock 完成路由中转的block
 @return 是否能够handle
 */
-(BOOL)handleURL:(NSURL *)URL primitiveParameters:(NSDictionary *)primitiveParameters targetCallBack:(void(^)(NSError *, id responseObject))targetCallBack withCompletionBlock:(void(^)(BOOL handled, NSError *error))completionBlock;

梳理总结:

从以上我们规划的几个类的接口,我们可以清楚的看到Router工作的流程。

  1. 首先实例化Router对象
  2. 实例化Handler或者是Block,通过Router的注册接口使得一个Url的匹配表达式对应一个Handler或者是一个block
  3. Router内部会将Url的表达式形成一个Matcher对象进行保存,对应的Handler或处理的Block会与Matcher一一对应,怎么对应呢?应该使用路由表达式进行关联
  4. Router通过handle方法,接收一个Url的请求,内部遍历所有的Matcher对象,将Url和参数转换为Request对象,如果能转换为Request对象则说明能匹配,如果不能则说明该Url不能被路由实体处理
  5. 拿到Request对象以后,则根据Matcher对应的路由表达式找到对应的Handler或者是Block
  6. 根据Handler的几个关键方法,传入Request对象,按照顺序完成处理逻辑的触发,最后如果有request当中包含有目标的回调,则将处理结果通过回调的Block响应给调用方
  7. Handler完成处理后,Router完成本次路由请求
带你一步步构建iOS路由_第10张图片
WLRRoute

Tips:
很多开发者把敏捷开发当做来了需求不管三七二十一,一把梭子就是干,不断写不断改。其实敏捷开发是一种模式,并不简单是快速迭代的意思。初入行的程序员其实都是coder(编码员),基本上在靠模仿代码和代码套路去工作,真正的Programmer(程序设计师)是在设计代码,科班出身的程序员往往在进阶过程中突然发现大学里面的软件工程有多么重要,其实设计能力的培养需要有一个正规的流程,就像本教程的大纲一样,发现问题->需求分析->总体设计->具体实现->测试->发布维护,有了清晰的流程,把你的精力和时间按照不同的阶段进行分配和投入,你就会豁然开朗,比方说在总体设计过程中,就需要你以宏观的功能流程去考虑大体的模块有几个,模块的关系是怎样,每个模块的核心职责是什么,建议根据需求去画一个逻辑流程图,将每个逻辑分支都补全,再根据流程图规划总体框架,总体框架通过类图来表达,每个类的属性和行为都确定以后,再进入具体设计阶段就非常轻松和容易了,同时类图画完,技术方案的可行性和实现所需时间也就非常容易精确评估了

给架子填充骨血(具体实现):

有了上面大体上的架子我们就能相信只要按照这个架子,就能完成你在需求分析阶段规划的功能目标,现在我们要做的就是在我们设计的这个牛逼的框架里填充血肉,去实现它,这部分是一个非常有意思的过程,在上一步你已经获得了相当大的信心,在这一步你只需要按照规定尽力去实现,在有信心的情况下,你会思维活跃,因为你明确了要实现何种功能的目标,大脑会自动根据目标和现在差距不断想考出各种办法去弥补这样的差距,你所做的就是不断尝试你大脑迸发出的这些代码,选择最有效、可读性最好、性能最好和代码最健壮的代码。

WLRRouteRequest:

了解了以上,我们从WLRRouteRequest入手。
其实WLRRouteRequest跟NSURLRequest差不多,不过WLRRouteRequest继承NSObject,实现NSCopying协议,我们再来看一下头文件的声明:

#import 

@interface WLRRouteRequest : NSObject
//外部调用的URL
@property (nonatomic, copy, readonly) NSURL *URL;
//URL表达式,比方说调用登录界面的表达式可以为:AppScheme://user/login/138********,那URL的匹配表达式可以是:/login/:phone([0-9]+),路径必须以/login开头,后面接0-9的电话号码数字,当然你也可以直接把电话号码的正则匹配写全
@property(nonatomic,copy)NSString * routeExpression;
//如果URL是AppScheme://user/login/138********?callBack="",那么这个callBack就出现在这
@property (nonatomic, copy, readonly) NSDictionary *queryParameters;
//这里面会出现{@"phone":@"138********"}
@property (nonatomic, copy, readonly) NSDictionary *routeParameters;
//这里面存放的是内部调用传递的原生参数
@property (nonatomic, copy, readonly) NSDictionary *primitiveParams;
//自动检测窃取回调的callBack 的Url
@property (nonatomic, strong) NSURL *callbackURL;
//目标的viewcontrolller或者是组件可以通过这个
@property(nonatomic,copy)void(^targetCallBack)(NSError *error,id responseObject);
//用以表明该request是否被消费
@property(nonatomic)BOOL isConsumed;
//简便方法,用以下标法取参数
- (id)objectForKeyedSubscript:(NSString *)key;
//初始化方法
-(instancetype)initWithURL:(NSURL *)URL routeExpression:(NSString *)routeExpression routeParameters:(NSDictionary *)routeParameters primitiveParameters:(NSDictionary *)primitiveParameters targetCallBack:(void(^)(NSError * error,id responseObject))targetCallBack;
-(instancetype)initWithURL:(NSURL *)URL;
//默认完成目标的回调
-(void)defaultFinishTargetCallBack;
@end

初始化方法就是将三个存放入参的字典初始化,并直接将Url上的?以后的参数取出来:

-(instancetype)initWithURL:(NSURL *)URL{
    if (!URL) {
        return nil;
    }
    self = [super init];
    if (self) {
        _URL = URL;
        _queryParameters = [[_URL query] WLRParametersFromQueryString];
    }
    return self;
}
-(instancetype)initWithURL:(NSURL *)URL routeExpression:(NSString *)routeExpression routeParameters:(NSDictionary *)routeParameters primitiveParameters:(NSDictionary *)primitiveParameters targetCallBack:(void (^)(NSError *, id))targetCallBack{
    if (!URL) {
        return nil;
    }
    self = [super init];
    if (self) {
        _URL = URL;
        _queryParameters = [[_URL query] WLRParametersFromQueryString];
        _routeExpression = routeExpression;
        _routeParameters = routeParameters;
        _primitiveParams = primitiveParameters;
        self.targetCallBack = targetCallBack;
    }
    return self;
}

在调用方设置传入callBack的时候,因为request是消费型的,所以将TargetCallBack重新包装,在回调的时候需要将isConsumed属性设置为YES,表示该request已经被处理消耗,这里实现的比较简单,其实request是应该具有状态的,比方说未处理,处理中,已处理,实现一个优雅的状态机会更好的表达逻辑:

-(void)setTargetCallBack:(void (^)(NSError *, id))targetCallBack{
    __weak WLRRouteRequest * weakRequest = self;
    if (targetCallBack == nil) {
        return;
    }
    self.isConsumed = NO;
    _targetCallBack = ^(NSError *error, id responseObject){
        weakRequest.isConsumed = YES;
        targetCallBack(error,responseObject);
    };
    
}

默认的回调方法是为了处理响应者没有触发回调,则需要有默认的回调给调用者:

-(void)defaultFinishTargetCallBack{
    if (self.targetCallBack && self.isConsumed == NO) {
        self.targetCallBack(nil,@"正常执行回调");
    }
}

WLRRouteHandler

#import 
@class WLRRouteRequest;
@interface WLRRouteHandler : NSObject
//即将handle某一个请求
- (BOOL)shouldHandleWithRequest:(WLRRouteRequest *)request;
//根据request取出调用的目标视图控制器
-(UIViewController *)targetViewControllerWithRequest:(WLRRouteRequest *)request;
//根据request取出来源的视图控制器
-(UIViewController *)sourceViewControllerForTransitionWithRequest:(WLRRouteRequest *)request;
//开始进行转场
-(BOOL)transitionWithWithRequest:(WLRRouteRequest *)request sourceViewController:(UIViewController *)sourceViewController targetViewController:(UIViewController *)targetViewController isPreferModal:(BOOL)isPreferModal error:(NSError *__autoreleasing *)error;
//是否模态跳转
- (BOOL)preferModalPresentationWithRequest:(WLRRouteRequest *)request;
@end

当WLRRouter对象完成了URL的匹配生成Request,并寻找到Handler的时候,首先会调用

-(BOOL)shouldHandleWithRequest:(WLRRouteRequest *)request{
    return YES;
}

子类可以覆写这个方法,实现一些参数检查或者当然App状态检查的工作,比方说检测当前界面是否已经有模态跳转覆盖的界面或者是弹窗之类,做一些清场工作来为本次转场进行准备。随后调用调用:

-(BOOL)handleRequest:(WLRRouteRequest *)request error:(NSError *__autoreleasing *)error{
    UIViewController * sourceViewController = [self sourceViewControllerForTransitionWithRequest:request];
    UIViewController * targetViewController = [self targetViewControllerWithRequest:request];
    if ((![sourceViewController isKindOfClass:[UIViewController class]])||(![targetViewController isKindOfClass:[UIViewController class]])) {
        *error = [NSError WLRTransitionError];
        return NO;
    }
    if (targetViewController != nil) {
        targetViewController.wlr_request = request;
    }
    BOOL isPreferModal = [self preferModalPresentationWithRequest:request];
    return [self transitionWithWithRequest:request sourceViewController:sourceViewController targetViewController:targetViewController isPreferModal:isPreferModal error:error];
}

注意targetViewController.wlr_request = request这一句,在获取目标控制器以后,我们会将request对象赋值给目标控制器,这是一种正向赋值的做法,为了降低接入成本,创建一个UIViewController的扩展,添加wlr_request这么一个属性,这么做也是不得已为之,因为UIKit库中ViewController并没有上下文机制,也就不能通过无入侵的形式进行消息传递。
在此我们准备好转场所需要的一切,包括是否模态跳转,目标控制器,源控制器,request等等,紧接着就开始进行转场:

-(BOOL)transitionWithWithRequest:(WLRRouteRequest *)request sourceViewController:(UIViewController *)sourceViewController targetViewController:(UIViewController *)targetViewController isPreferModal:(BOOL)isPreferModal error:(NSError *__autoreleasing *)error;{
    if (isPreferModal||![sourceViewController isKindOfClass:[UINavigationController class]]) {
        [sourceViewController presentViewController:targetViewController animated:YES completion:nil];
    }
    else if ([sourceViewController isKindOfClass:[UINavigationController class]]){
        UINavigationController * nav = (UINavigationController *)sourceViewController;
        [nav pushViewController:targetViewController animated:YES];
    }
    return YES;
}

这里着重说一下handler的targetViewControllerWithRequest和sourceViewController方法,你可能会觉得,我要为每一个视图控制器配置一子类对象,在targetViewControllerWithRequest里面写上获取目标控制器的方法,会造成继承很多很多handler,实际上你可以发挥一下想象力,在targetViewControllerWithRequest方法里,你已经能拿到request,而request里面有路由表达式,实际上一个路由表达式应该对应一个或者多个视图控制器,你完全可以做一个配置文件,在targetViewControllerWithRequest方法里通过路由表达式来获取目标视图控制器的初始化信息,目标控制器的初始化无非分为直接代码初始化,要么是storyboard初始化,要么是xibs初始化,但是这个配置文件如何设计和使用,不在本文讨论范畴,需要你开动脑筋去解决。
同样preferModalPresentationWithRequest也可以根据配置来处理是否要进行模态跳转的问题。transitionWithRequest方法里面根据source来进行导航push或者是模态跳转,你大可以继承一下,然后重写转场过程。
Handler类里面的生命周期函数是不是很像UIViewControllerContextTransitioning转场上下文的协议的设定?- (nullable __kindof UIViewController *)viewControllerForKey:(UITransitionContextViewControllerKey)key;方法使上下文提供目标控制器和源控制器,其实在handler中你完全可以自定义一个子类,在transitionWithRequest方法里,设置遵守UIViewControllerTransitioningDelegate的代理,然后在此提供遵守 UIViewControllerAnimatedTransitioning的动画控制器,然后自定义转场上下文,实现自定义UI转场,而对应的匹配逻辑是与此无关的,我们就可以在路由曾控制全局的页面转场效果。对自定义转场不太熟悉的同学请移步我之前的文章:
ContainerViewController的ViewController 转场

WLRRouteMatcher

#import 
@class WLRRouteRequest;
@interface WLRRouteMatcher : NSObject
//传入URL匹配的表达式,获取一个matcher实例
+(instancetype)matcherWithRouteExpression:(NSString *)expression;
//传入URL,如果能匹配上,则生成WLRRouteRequest对象,同时将各种参数解析好交由WLRRouteRequest携带
-(WLRRouteRequest *)createRequestWithURL:(NSURL *)URL primitiveParameters:(NSDictionary *)primitiveParameters targetCallBack:(void(^)(NSError *, id responseObject))targetCallBack;
@end

Matcher对象需要传入一个URL对象来判断是否匹配,生成request对象,说到匹配就一定不会傻傻的用字符串相等,肯定是要借助正则表达式来判断是否匹配和将分组的内容从字符串中取出,我们初始化Macter对象的时候要分配一个Url表达式给它,格式前面提到过,Url诸如http://abc.com/:name([a-zA-Z0-9-]+) 的格式并不是真正的标准正则表达式,如果要把name取出,则必须将其装换为标准的正则表达式像:^http://abc.com/([a-zA-Z0-9-]+)$ 这样,那么就需要进行转换和提取:后面的key值,在Fundaction库中,NSRegularExpression是用来处理正则表达式的,为了使逻辑更加清晰,我们可以把这样的逻辑通过继承NSRegularExpression封装到一个叫WLRRegularExpression的类中,这个类提供匹配和转换标准正则表达式的功能如下:

带你一步步构建iOS路由_第11张图片
WLRRegularExpression

匹配的结果我们可以通过一个WLRMatchResult来表示。传入路由url的匹配表达式穿实话WLRRegularExpression实例,在初始化的时候就转换为标准的正则表达式和将:后面的key的名字保存到routerParamNamesArr当中,matchResultForString接口可接受一个Url从而生成匹配结果的对象,匹配结果的对象里存储着路径参数信息和是否匹配的结果。

#import 
@class WLRMatchResult;
@interface WLRRegularExpression : NSRegularExpression
//传入一个URL返回一个匹配结果
-(WLRMatchResult *)matchResultForString:(NSString *)string;
//根据一个URL的表达式创建一个WLRRegularExpression实例
+(WLRRegularExpression *)expressionWithPattern:(NSString *)pattern;
@end

WLRRegularExpression继承NSRegularExpression

-(instancetype)initWithPattern:(NSString *)pattern options:(NSRegularExpressionOptions)options error:(NSError * _Nullable __autoreleasing *)error{
//初始化方法中将URL匹配的表达式pattern转换为真正的正则表达式
    NSString *transformedPattern = [WLRRegularExpression transfromFromPattern:pattern];
//用转化后的结果初始化父类
    if (self = [super initWithPattern:transformedPattern options:options error:error]) {
//同时将需要提取的子串的值的Key保存到数组中
        self.routerParamNamesArr = [[self class] routeParamNamesFromPattern:pattern];
    }
    return self;
}
//转换为正则表达式
+(NSString*)transfromFromPattern:(NSString *)pattern{
//将pattern拷贝
    NSString * transfromedPattern = [NSString stringWithString:pattern];
//利用:[a-zA-Z0-9-_][^/]+这个正则表达式,将URL匹配的表达式的子串key提取出来,也就是像 /login/:phone([0-9]+)/:name[a-zA-Z-_]这样的pattern,需要将:phone([0-9]+)和:name[a-zA-Z-_]提取出来
    NSArray * paramPatternStrings = [self paramPatternStringsFromPattern:pattern];
    NSError * err;
//再根据:[a-zA-Z0-9-_]+这个正则表达式,将带有提取子串的key全部去除,比如将:phone([0-9]+)去除:phone改成([0-9]+)
    NSRegularExpression * paramNamePatternEx = [NSRegularExpression regularExpressionWithPattern:WLRRouteParamNamePattern options:NSRegularExpressionCaseInsensitive error:&err];
    for (NSString * paramPatternString in paramPatternStrings) {
        NSString * replaceParamPatternString = [paramPatternString copy];
        NSTextCheckingResult * foundParamNamePatternResult =[paramNamePatternEx matchesInString:paramPatternString options:NSMatchingReportProgress range:NSMakeRange(0, paramPatternString.length)].firstObject;
        if (foundParamNamePatternResult) {
            NSString *paramNamePatternString =[paramPatternString substringWithRange: foundParamNamePatternResult.range];
            replaceParamPatternString = [replaceParamPatternString stringByReplacingOccurrencesOfString:paramNamePatternString withString:@""];
        }
        if (replaceParamPatternString.length == 0) {
            replaceParamPatternString = WLPRouteParamMatchPattern;
        }
        transfromedPattern = [transfromedPattern stringByReplacingOccurrencesOfString:paramPatternString withString:replaceParamPatternString];
    }
    if (transfromedPattern.length && !([transfromedPattern characterAtIndex:0] == '/')) {
        transfromedPattern = [@"^" stringByAppendingString:transfromedPattern];
    }
//最后结尾要用$符号
    transfromedPattern = [transfromedPattern stringByAppendingString:@"$"];
//最后会将/login/:phone([0-9]+)转换为login/([0-9]+)$
    return transfromedPattern;
}

在Matcher对象匹配一个URL的时候

-(WLRMatchResult *)matchResultForString:(NSString *)string{
//首先通过自身方法将URL进行匹配得出NSTextCheckingResult结果的数组
    NSArray * array = [self matchesInString:string options:0 range:NSMakeRange(0, string.length)];
    WLRMatchResult * result = [[WLRMatchResult alloc]init];
    if (array.count == 0) {
        return result;
    }
    result.match = YES;
    NSMutableDictionary * paramDict = [NSMutableDictionary dictionary];
//遍历NSTextCheckingResult结果
    for (NSTextCheckingResult * paramResult in array) {
//再便利根据初始化的时候提取的子串的Key的数组
        for (int i = 1; i

Ok,有了WLRRegularExpression和WLRMatchResult以后,WLRRouteMatcher可以接下来补充,属性有如下:

//scheme
@property(nonatomic,copy) NSString * scheme;
//WLRRegularExpression的实例 
@property(nonatomic,strong)WLRRegularExpression * regexMatcher;
//匹配的表达式
@property(nonatomic,copy)NSString * routeExpressionPattern;

初始化方法将形成WLRRegularExpression对象:

-(instancetype)initWithRouteExpression:(NSString *)routeExpression{
    if (![routeExpression length]) {
        return nil;
    }
    if (self = [super init]) {
//将scheme与path部分分别取出
        NSArray * parts = [routeExpression componentsSeparatedByString:@“://“];
        _scheme = parts.count>1?[parts firstObject]:nil;
        _routeExpressionPattern =[parts lastObject];
//将path部分当做URL匹配表达式生成WLRRegularExpression实例
        _regexMatcher = [WLRRegularExpression expressionWithPattern:_routeExpressionPattern];
    }
    return self;
}

匹配方法就是通过WLRRegularExpression对象来生成result来检测是否能够匹配,如果能匹配则生成WLRRouteRequest对象:

-(WLRRouteRequest *)createRequestWithURL:(NSURL *)URL primitiveParameters:(NSDictionary *)primitiveParameters targetCallBack:(void (^)(NSError *, id))targetCallBack{
    NSString * urlString = [NSString stringWithFormat:@“%@%@“,URL.host,URL.path];
    if (self.scheme.length && ![self.scheme isEqualToString:URL.scheme]) {
        return nil;
    }
//调用self.regexMatcher将URL传入,获取WLRMatchResult结果,看是否匹配
    WLRMatchResult * result = [self.regexMatcher matchResultForString:urlString];
    if (!result.isMatch) {
        return nil;
    }
//如果匹配,则将result.paramProperties路径参数传入,初始化一个WLRRouteRequest实例
    WLRRouteRequest * request = [[WLRRouteRequest alloc]initWithURL:URL routeExpression:self.routeExpressionPattern routeParameters:result.paramProperties primitiveParameters:primitiveParameters targetCallBack:targetCallBack];
    return request;
}

告一段落,这部分其实算是路由组件中比较核心的逻辑,为什么要拆成三个类,大家思考一下分割逻辑的思想,职责单一以后,你才会聚焦目的本身。

WLRRouter

完成了以上几个类的实现,WLRRouter就比较容易了。

@class WLRRouteRequest;
@class WLRRouteHandler;
@interface WLRRouter : NSObject
//注册block回调的URL匹配表达式,可用作内部调用
-(void)registerBlock:(WLRRouteRequest *(^)(WLRRouteRequest * request))routeHandlerBlock forRoute:(NSString *)route;
//注册一个WLRRouteHandler对应的URL匹配表达式route
-(void)registerHandler:(WLRRouteHandler *)handler forRoute:(NSString *)route;
//判断url是否可以被handle
-(BOOL)canHandleWithURL:(NSURL *)url;
-(void)setObject:(id)obj forKeyedSubscript:(NSString *)key;
-(id)objectForKeyedSubscript:(NSString *)key;
//调用handleURL方法,传入URL、原生参数和targetCallBack和完成匹配的completionBlock
-(BOOL)handleURL:(NSURL *)URL primitiveParameters:(NSDictionary *)primitiveParameters targetCallBack:(void(^)(NSError *, id responseObject))targetCallBack withCompletionBlock:(void(^)(BOOL handled, NSError *error))completionBlock;

在实现部分,有三个属性,这三个属性用来设置matcher、handler、block的对应关系,key都为路由表达式:

//每一个URL的匹配表达式对应一个matcher实例,放在字典中
@property(nonatomic,strong)NSMutableDictionary * routeMatchers;
//每一个URL匹配表达式route对应一个WLRRouteHandler实例
@property(nonatomic,strong)NSMutableDictionary * routeHandles;
//每一个URL匹配表达式route对应一个回调的block
@property(nonatomic,strong)NSMutableDictionary * routeblocks;

在Router注册Handler和回调的block的时候:

-(void)registerBlock:(WLRRouteRequest *(^)(WLRRouteRequest *))routeHandlerBlock forRoute:(NSString *)route{
    if (routeHandlerBlock && [route length]) {
//首先添加一个WLRRouteMatcher实例
        [self.routeMatchers setObject:[WLRRouteMatcher matcherWithRouteExpression:route] forKey:route];
//删除route对应的handler对象
        [self.routeHandles removeObjectForKey:route];
//将routeHandlerBlock和route存入对应关系的字典中
        self.routeblocks[route] = routeHandlerBlock;
    }
}
-(void)registerHandler:(WLRRouteHandler *)handler forRoute:(NSString *)route{
    if (handler && [route length]) {
//首先生成route对应的WLRRouteMatcher实例
        [self.routeMatchers setObject:[WLRRouteMatcher matcherWithRouteExpression:route] forKey:route];
//删除route对应的block回调
        [self.routeblocks removeObjectForKey:route];
//设置route对应的handler
        self.routeHandles[route] = handler;
    }
}

接下来完善handle方法,设置completionHandler可以监听Router的每一次handle动作:

-(BOOL)handleURL:(NSURL *)URL primitiveParameters:(NSDictionary *)primitiveParameters targetCallBack:(void(^)(NSError *error, id responseObject))targetCallBack withCompletionBlock:(void(^)(BOOL handled, NSError *error))completionBlock{
    if (!URL) {
        return NO;
    }
    NSError * error;
    WLRRouteRequest * request;
    __block BOOL isHandled = NO;
//遍历routeMatchers中的WLRRouteMatcher对象,将URL传入对象,看是否能得到WLRRouteRequest对象
    for (NSString * route in self.routeMatchers.allKeys) {
        WLRRouteMatcher * matcher = [self.routeMatchers objectForKey:route];
        WLRRouteRequest * request = [matcher createRequestWithURL:URL primitiveParameters:primitiveParameters targetCallBack:targetCallBack];
        if (request) {
//如果得到WLRRouteRequest对象,说明匹配成功,则进行handler的生命周期函数调用或是这block回调
            isHandled = [self handleRouteExpression:route withRequest:request error:&error];
            break;
        }
    }
    if (!request) {
        error = [NSError WLRNotFoundError];
    }
//在调用完毕block或者是handler的生命周期方法以后,回调完成的completionHandler
    [self completeRouteWithSuccess:isHandled error:error completionHandler:completionBlock];
    return isHandled;
}
//根据request进行handler的生命周期函数调用或者是block回调
-(BOOL)handleRouteExpression:(NSString *)routeExpression withRequest:(WLRRouteRequest *)request error:(NSError *__autoreleasing *)error {
    id handler = self[routeExpression];
//self.routeHandles和self.routeblocks拿到route对应的回调block或者是handler实例
    if ([handler isKindOfClass:NSClassFromString(@"NSBlock")]) {
        WLRRouteRequest *(^blcok)(WLRRouteRequest *) = handler;
//调用回调的block
        WLRRouteRequest * backRequest = blcok(request);
//判断block里面是否消费了此request,如果没有而目标设置了目标回调targetCallBack,那么在此进行默认回调
        if (backRequest.isConsumed==NO) {
            if (backRequest.targetCallBack) {
                dispatch_async(dispatch_get_main_queue(), ^{
                    backRequest.targetCallBack(nil,nil);
                });
            }
        }
        return YES;
    }
    else if ([handler isKindOfClass:[WLRRouteHandler class]]){
//拿到url对应的handler对象后,先调用handler的shouldHandleWithRequest方法,如果返回YES,则调用进行转场的transitionWithRequest方法
        WLRRouteHandler * rHandler = (WLRRouteHandler *)handler;
        if (![rHandler shouldHandleWithRequest:request]) {
            return NO;
        }
       return [rHandler transitionWithRequest:request error:error];
    }
    return YES;
}

以上我们可以看到,Router将匹配的逻辑单独封装到WLRRouteMatcher对象中,将匹配后的结果生成WLRRouteRequest实例以携带足够完整的数据,同时将真正处理视图控制器的转场或者是组件的加载或者是未来可能拓展的handler业务封装到WLRRouteHandler实例中,匹配逻辑对应的处理逻辑干净分离,Matcher对象将匹配逻辑单独封装,如果有一天需要增加多种URL的匹配逻辑,则可以更换Matcher或者添加Matcher就可以,处理逻辑可以通过继承扩展或者冲洗WLRRouteHandler的生命周期函数来更好的处理回调业务。如果WLRRouteHandler不能提供足够多的扩展性,则可以使用block回调最大限度的进行扩展。
以上,就是路由部分的整体实现。

Tips:
在具体的实现的过程中,不要被总体设计给套死,前面设计了几个类,设计了几个层次以后,就画地为牢,在实现具体细节的时候仍可以继续进行局部设计,分割逻辑,控制代码复杂度,一方面你要注重在局部进行进一步设计和分割的时候带来的究竟是什么,可读性?可维护性?还是帮助你能够进行思考?有时候,注释可能更加能给你的逻辑带来清晰的表述,毕竟几千行的代码很正常,如果没有经验,可能逻辑重点记得不太清楚,那就需要动动笔头好好记录一下,再进一步实现,将复杂逻辑进行适当分割,是一种常见的策略

最终的路由整体类图和如何设计代码的总结:

带你一步步构建iOS路由_第12张图片
WLRRoute

整体的设计就是如此,我们走了一趟从发现问题到最后通过封装一个路由组件解决问题的过程,在这里,对于新手程序员我想讨论一些有意义的东西,也就是手把手写出一个路由的过程中,思路从何而来?代码设计怎么从无到有?对于一个逻辑问题怎么转换为代码实践?授之以鱼不如授之以渔,在这个过程中我们总结一下哪些是比较重要的:

  1. 对于经常重复的工作和低效的工作流程有没有敏锐的察觉,是否有想解决的愿望,毕竟能解决了你能节省更多时间,何乐而不为?可有些人宁愿加班做一些简单的重复工作也不希望直面挑战创造一些解决问题的新方式
  2. 在思考问题的解决方案的时候,大脑有没有将问题抽象一下去寻找有没有相应解决问题的模式、方法论或者是方案?(比如说发现路由方案,发现中介者模式,发现分割思想和职责单一,发现软件工程的流程)
  3. 天赋并不是最重要的,甚至天赋是一种结论而不是条件,大脑不擅长同时处理很多事情,在你设计或者编码的过程中,通过标准化流程能最大程度的提高大脑思考的效率,比方说,总结问题就是总结问题而不要变总结变思考解决方案,否则会片面或者影响问题的客观描述,总结问题->需求分析->总体设计->具体实现,这就是一个简单的大脑思考流程,如果想不清楚,通过思维导图、流程图、UML建模等等,去把大脑需要同时照顾到的东西呈现在眼前,你只要去对比对照,一步步来就可以了,并不是有人特别有天赋,而是有人比你有方法
  4. 发散很重要,每当你从出现问题到解决问题以后,你可以安心的天马行空的头脑风暴,在这个过程中你可能对解决问题的方式进行重新审视和回顾,发现不足,甚至可以对其扩展使其能解决更多问题,就比如你会考虑路由的效率、安全,从而诞生中间件、异步事件的想法,这点很重要,但却很简单
  5. 自我引导能力,看了文章一步步做了出来,其实就跟做菜一样,你学会了一道菜该怎么做,但为什么别人能做,或者说别人从无到有的过程经历了什么,你为什么没想到,这种想法会激励你自我引导,从而提升自己,进一步改变自己,如果你从来没有对他人为什么能做到感到好奇,那就说明你的自我意识和自我引导力不够强,需要停下每天写代码的手,认真思考一下人生了

路由的安全

有两个方面可以去做

  1. WLRRouteHandler实例中, -(BOOL)shouldHandleWithRequest:(WLRRouteRequest *)request中可以检测request中的参数,比方说效验source或者是效验业务参数完整等
  2. WLRRouter实例中handleURL方法,将按照中间件注册的顺序回调中间件,而我们可以在中间件中实现风控业务、认证机制、加密验签等等,中间件的实现大家可查看源码

路由的效率

目前我们实现的路由是一个同步阻塞型的,在处理并发的时候可能会出现一些问题,或者是在注册比较多的route表达式以后,遍历和匹配的过程会损耗性能,比较好的实现方式是,将Route修改成异步非阻塞型的,但是API全部要换成异步API,起步我们先把同步型的搞定,随后我们将会参照promise范式来进行异步改造,提升路由效率。

路由的使用

在大部分App实践MVVM架构或者更为复杂的VIPER架构的时候,除了迫切需要一个比较解耦的消息传递机制,如何更好的剥离目标实体的获取和配合UIKit这一层的转场逻辑是一项比较复杂的挑战,路由实际上是充当MVVM的ViewModel中比较解耦的目标获取逻辑和VIPER中Router层,P与V的调用全部靠Router转发。
在实施以组件化为目的的工程化改造中,如何抽离单独业务为组件,比较好的管理业务与业务之间的依赖,就必须使用一个入侵比较小的Route,WLRRoute入侵的地方在于WLRRouteHandler的transitionWithRequest逻辑中,通过一个UIViewController的扩展,给 targetViewController.wlr_request = request;设置了WLRRouteRequest对象给目标业务,但虽然如此,你依旧可以重写WLRRouteHandler的transitionWithRequest方法,来构建你自己参数传递方式,这一点完全取决于你如何更好的使得业务无感知而使用路由。

最后附上代码地址:
喜欢的来个星吧…
https://github.com/Neojoke/WLRRoute
12.27更新:
感谢这位同学,写了一篇讨论性的 文章,对我启发很大,我尊敬以及欣赏能够深入思考并且愿意分享自己的idea的人。
2017.02.26更新:
WLRRoute增加了核心逻辑的注释升级到0.0.9版本,下一个0.1.0版本将加入异步处理,敬请期待
2017.03.01更新:
更新文章的整体阅读流畅度

你可能感兴趣的:(带你一步步构建iOS路由)