一步步构建iOS路由
什么是移动端路由层:
路由层的概念在服务端是指url请求的分层解析,将一个请求分发到对应的应用处理程序。移动端的路由层指的是将诸如App内页面访问、H5与App访问的访问请求和App间的访问请求,进行分发处理的逻辑层。
移动端路由层需要解决的问题:
- 对外部提供远程访问的功能,实现跨应用调用响应,包括H5应用调用、其他App应用调用、系统访问调用等
- 原生页面、模块、组件等定义,统称为资源(Resource),在跨应用调用和路由层在不同端实现的业务表现需要一致的前提下,需要对资源进行定义,在路由提供内部请求分发的时候则可以提供不依赖对外进行资源定义的功能
- 外部调用如何使用统一标示(Uniform)进行表示资源
- 如何在移动端统一定义访问请求的过程,从而达成移动端与web端的统一性
- 如何更好的兼容iOS、Android的系统访问机制、App链接协议、web端路由机制与前端开发规范等
- 如何兼容各平台(Android、iOS)App页面导航机制
- 如何解决安全访问问题
- 移动端在客户端进行动态配置
移动端路由所应用的场景:
- H5页面与App原生页面、模块与组件的交互
- App与App之间的相互访问
- App内部页面跳转、模块调度与组件加载等
- 推送与通知系统解除硬编码的逻辑,动态访问原生资源,更好的支持通过通知和推送完成动态页面访问和逻辑执行
- Extension等动态调用主App的资源
- App实现更复杂的架构MVVM或者是VIPER架构,提供解除业务相互依赖的能力
- 以组件化为目的的工程改造,隔离各个业务,以制作单独的组件
对外如何定义资源
在路由提供对外的资源请求转发的时候,因为要照顾到其他应用的请求表达方式,比如H5应用或者是其他App的应用的访问请求,定义单纯依赖业务的资源定义就显得有些必要了。
举个例子,一个H5的商品详情页,被用户分享,当其他用户看到这个H5应用的页面的时候,点击,如果该用户装了有对应这个H5商品详情页的App的时候,应该跳转到该App的原生商品详情页,如果没有安装则加载这个H5页面,在这个过程中,H5的页面是通过URL进行标识的,那这个URL的标识也应该对照到App的原生页面,但是要只依赖业务标识而不能依赖App的代码实现,比如说iOS端的App的商品详情页叫做DetailViewController,那这个URL是不能包含这个名字的,Android端可能叫DetailActivity,如果不单纯依赖业务,那H5应用就要根据平台来重新发送不同的资源定义的URL,就造成了硬编码问题,H5应用要依赖App的实现逻辑,如果有一天,原生App的页面代码实现变成了GoodDetailViewController,所有依赖DetailViewController这个资源标示的H5应用都要进行更改,就会出现问题。所以路由层的设计应该具备根据业务定义来映射App内的资源定义。
常常在设计路由层的时候,我们会更加关注通信行为的细节、如何改进特定通信机制的表现,常常忽略了一个事实,那就是改变应用程序的互动风格比改变协议对整体的表现有更大的影响。
所谓资源,就是一个应用程序提供的不可分割的服务,从这个层面上看,App的资源即是一种实体的存在,可以进行获取和访问,必须进行良好的表示,在有些必要的情况下,必须是独一无二的识别符来表示一个应用程序所提供的服务是什么。表示资源我们更倾向于使用URI进行标示,因为移动端没有一个横跨iOS、Android、Web后端与H5应用的资源标示方式,而URI是web service模式的资源通用表示方式,包括后面将要提到的Android与iOS统一支持的universal link(通用链接)也是借用URI的概念,App路由层所涉及到的资源表示方法还是建议使用URI的标示方式,同时更应该借鉴RESTful风格来架构这一层,原因是App的页面、组件或者说一整套功能性的服务是非常复杂的,相比于H5有更加多与复杂的交互,相比于后端存在更加苛刻的网络环境与多设备多平台的技术考量,所以URI在标示横跨多平台多版本的资源的情况下,能够更好的表示某一个资源实体而不是资源的表现形式。
在Android与iOS系统中,均支持URL Scheme,所以资源的标示通常会是这个样子:
AppScheme://path
//例如qq app:
mqq://
//支付宝:
支付宝alipay://
如果协议是Http或者是Https标示的是Web应用或者是H5应用,你的App也是一个与WebService相同级别的应用,那么URL的协议部分应该是App的唯一标示符,这个主机部分和路径部分则需要我们使用RESTful的风格进行重新设计。
重点是如何标示资源,例如表示App中的登录服务,那可以表示为:
AppScheme://host/login
host为主机部分,在一般的WebService上,在业务表现形式上一般是比较大的业务条线的标示,比方说 https://news.sina.com.cn ,主机部分是news.sina.com.cn,则标示新浪新闻这条业务线,在App内你的业务条线也应该是清晰的,假如移动App的主UI框架是Tab分栏,那么每个Tab分栏就是你的业务条线的分割,这点跟WebService应用的导航栏类似,App的资源大多是页面或者是可交互的组件,与UI关系比较大,假如你的Tab有四个:分别叫首页、商品、发现、我的,那么我们可以这样定义:
AppScheme://index/
AppScheme://goods/
AppScheme://discover/
AppScheme://user/
当然,也可以有额外的定义,比方说App有Api服务,Api提供实现一个纯数据同步的服务标示,那么这个URL可以设计为:
AppScheme://api-asycn/collections?action='insert'&value='***'&&userUoken='*******'&&source="https//***.***.com/collection.html"
由于RESTful风格强调URL的资源标示而不是行为表示,所以”AppScheme://api-asycn/collections” 是一个良好的资源标示,表示了一个收藏功能的实体,而”?”后面的GET方式的参数实际上是不得已为之,因为实际上没有Web的http request的实体,所以只能勉强借助GET参数来替代RESTful风格中强调的Accept和Content-Type字段来标示表现层的行为描述。
当然action与value这样的描述可以根据业务划分,但是重点是要用参数表现形式。
iOS与Android的系统访问机制、统一的链接协议
苹果的URL Scheme由来已久: Apple URLScheme,Android平台同样也实现了该功能,使得App能够在沙盒机制的前提下,能够相互调用声明过的服务。由于URL Scheme天生没有返回的callBack机制,著名的App Drafts的作者联合Marco Arment、Justin Williams 等人开发了x-callback-URL来做出统一跳转的协议: x-callback-url,在此不过多表述。
利用URL-Scheme的机制,可以定义如下的统一链接协议:
- 协议部分来标示App应用
- 主机Host部分用于标示业务线或者是应用提供的划分好的服务实体,比方说index、discover是业务条线,api-asycn是对外提供的api,pushService是App内部的推送服务等。
- 路径部分则可以是细分的页面、组件或者服务的标示
- 参数定义有一些是必要的,比如说action来标示动作,比方说可以使用get标示获取、insert增加,userToken表示安全的用户令牌,source表示来源,当然像是userToken与source这些都是路由层需要进行解析和验证的,而action则是业务相关的参数,这一点在路由曾设计的时候需要进行详细区分
统一访问请求过程
整个统一的访问请求过程如图,关于最后的response返回有一些说明:
在WebService的工作栈中,http的request与response是有标准协议规范的,而App的路由层只是套用的URI的资源标示和RESTFul风格的交互,没有标准的request和response结构,这部分实现在App内部,response对外部调用系统而言关心的有三个重要元素,资源状态码、返回值与错误,在路由层在响应外部调用的时候需要返回这三种元素
路由层逻辑结构
路由层安全
路由层的安全包含两个方面:
- 跨应用时,需要注意注入攻击,做到敏感参数加密防篡改,同时需要注意路由层应提供能够实现风控的机制
- 跨业务系统的时候,需要开启会话访问机制,通过令牌或者是session会话等来实现路由层身份认证
路由层实现
敬请期待下一篇文章:《一步步构建iOS路由》
番外:App孤岛、API经济与App开放性讨论
什么叫App孤岛
移动操作系统中的App一般都采用沙盒机制来严格限制访问权限,App与App之间是不通的,用户往往会安装大量的App,比方说找吃饭的地方是大众点评,聊天是微信,地图是高德等等,那么我们想象一下没有URL Scheme的世界,你在大众点评上找到了一个好吃的地方,然后需要切换到高德去找找在哪,然后脑子记录下来地址然后在微信上发给你的朋友,这么一个过程中,众多App之间是不能传递信息和相互协作的,那一个个App就成了信息孤岛,给用户带来极大的不便,而实现了URL Scheme的App一般都是大厂,用户过亿,给上亿人带来了方便。
打破App孤岛
本质上URL Scheme是操作系统支持的,也就是说,打破App孤岛,必须过操作系统这一关,而无论是第三方开发者还是Apple与Google都在努力打破信息孤岛。
Apple与Google分别在iOS9与Android M支持了universal link以打通H5应用和原生应用的屏障。
Apple则在iOS操作系统中通过Spotlight应用内搜索、AppGroups、AppExtension、ShareExtension与SiriKit等打破原生应用之间的信息屏障。
Google则通过PWA希望替代原生应用来实现大一统。
第三方开发者们也积极推动着这一趋势。比如说:
前面提到的著名的App Drafts的作者联合Marco Arment、Justin Williams 等人开发了x-callback-URL来做出统一跳转的协议: x-callback-url,希望大部分App开发者能够响应号召,更好的进行开发。
国内的一些深度链接的开发者平台 DeepShare - Share your App with the world
锤子手机开源的onestep等等。
作为一名开发者,构建安全高效而开放的路由实际上不仅仅满足技术架构的需求更能为打破App孤岛,更好的发展移动端生态做出贡献。什么叫做API经济
API经济是基于API所产生的经济活动的总和,在当今发展阶段主要包括API业务,以及通过API进行的业务功能、性能等方面的商业交易。API经济是当今各行业(零售、金融、物联网、医疗等)中驱动数字变革的主要力量。 ———百度百科
为什么这里需要谈到API经济呢?我们都知道经济学的第一要务是效率优先原则,就像上面我们聊到的App孤岛,在日益便利的移动化时代,实际上降低了信息共享的效率,而增加了用户的操作成本,则会阻碍这个平台上用户的活跃度,那上层利用移动平台的可能性就会被限制。比如,二维码和NFC解决了pos终端、商家与支付App之间的信息共享问题,就导致了繁盛的线下支付经济,同样的道理,各系统之间无论是App、WebServices或者是其他应用能够开放API则会形成平台或者产业上的信息共享的规模效应,则会形成良性发展。
作为App开发者,你需要实现路由这一层,才能够支持跨应用之间的调用,才能放开你想开发的API。
如果一个App的后端Services能够和App一起开放API,那则更加具有优势。比方说微信,如果开放了收藏的WebService API接口,同时微信App也开放URLScheme的收藏接口,那么无论在浏览器、手机中都能无缝实现随时随地的收藏一切内容,极大的方便用户。
App开放性讨论
这个环节主要是讨论开放的时候要注意哪些:
- App类型(决定要不要开放)
- 路由安全(决定开放程度)
- 开放时机
未完,希望大家多多评论,一起讨论。
接上一篇移动端路由层设计
为啥要说iOS路由呢?
路由层其实在逻辑上的设计都是一样的,关于对界面跳转的实现部分却与Android平台和iOS平台上的导航机制有着非常紧密的关系,Android操作系统有着天然的架构优势,Intent机制可以协助应用间的交互与通讯,是对调用组件和数据传递的描述,本身这种机制就解除了代码逻辑和界面之间的依赖关系,只有数据依赖。而iOS的界面导航和转场机制则大部分依赖UI组件各自的实现,所以如何解决这个问题,iOS端路由的实现则比较有代表性。
其实说白一点,路由层解决的核心问题就是原来界面或者组件之间相互调用都必须相互依赖,需要导入目标的头文件、需要清楚目标对象的逻辑,而现在全部都通过路由中转,只依赖路由,或者依靠一些消息传递机制连路由都不依赖。其次,路由的核心逻辑就是目标匹配,对于外部调用的情况来说,URL如何匹配Handler是最为重要的,匹配就必然用到正则表达式。了解这些关键点以后就有了设计的目的性,let‘s do it~
设计类图:
这里面有如下几个类:
- WLRRouteRequest,路由层的请求,无论是跨应用的外部调用还是内部调用,最后都形成一个路由请求,该请求包含了URL上的queryparameters和路径参数,还有内部调用时直接传入的原生参数,还有请求发起者对目标预留的回调block
- WLRRouteHandler,路由层的handler处理,handler接收一个WLRRouteRequest对象,来完成是否是界面跳转,还是组件加载,还是内部逻辑
- WLRRouter,路由核心对象,内部持有注册的Handler,比方说负责界面跳转的Handler,负责组件加载的Handler,负责API的Handler等等,路由的作用就是将外部调用传入的URL或者是内部调用传入的target,在内部匹配上对应的handler,然后调用生命周期方法,完成处理过程,当然,图中还有route的中间件,实际上是预留AOP的口子,方面后期扩展
- WLRRouteMatcher,用以处理外部调用的URL是否能与预设的正则表达式匹配,在WLRRouter中,每一次注册一个handler,都会用一个URL匹配的表达式生成一个WLRRouteMatcher
- WLRRegularExpression,继承NSRegularExpression,用以匹配URL,WLRRouteMatcher内部有一个WLRRegularExpression对象,WLRRouteMatcher接受一个URL,会使用WLRRegularExpression生成一个WLRMatchResult对象,来确定是否匹配成功,如果匹配成果则将URL上的路径参数给取出来
- WLRMatchResult,用以描述WLRRegularExpression的匹配结果,包含路径参数
工作流程:
- App启动实例化WLRRouter对象
- 实例化WLRRouteHandler对象
- WLRRouter对象挂载WLRRouteHandler实例与URL的表达式相对应,WLRRouter内部生成一个WLRRouteMatcher对象,与URL的表达式相对应
- 外部调用的URL和callback传入WLRRouter对象
- WLRRouter对象遍历内部持有的URL的匹配表达式,并找到每一个WLRRouteMatcher对象,将URL传入看是否能返回WLRRouteRequest对象
- 将WLRRouteRequest对象传入对应的WLRRouteHandler对象
- WLRRouteHandler对象根据WLRRouteRequest寻找到TargetViewController和SourceViewController,在生命周期函数里,完成参数传递与视图转场
WLRRouteRequest:
了解了以上,我们从WLRRouteRequest入手。
其实WLRRouteRequest跟NSURLRequest差不多,不过WLRRouteRequest继承NSObject,实现NSCopying协议,大概如下:
#import <Foundation/Foundation.h>
@interface WLRRouteRequest : NSObject<NSCopying>
//外部调用的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
NSURLRequest其实应该是个值类型的对象,所以实现拷贝协议,该对象的实现部分没有什么可讲的,对照源代码查阅即可。
WLRRouteHandler
#import <Foundation/Foundation.h>
@class WLRRouteRequest;
@interface WLRRouteHandler : NSObject
//即将handle某一个请求
- (BOOL)shouldHandleWithRequest:(WLRRouteRequest *)request;
//根据request取出调用的目标视图控制器
-(UIViewController *)targetViewControllerWithRequest:(WLRRouteRequest *)request;
//根据request取出来源的视图控制器
-(UIViewController *)sourceViewControllerForTransitionWithRequest:(WLRRouteRequest *)request;
//开始进行转场
-(BOOL)transitionWithRequest:(WLRRouteRequest *)request error:(NSError *__autoreleasing *)error;
@end
当WLRRouter对象完成了URL的匹配生成Request,并寻找到Handler的时候,首先会调用- (BOOL)shouldHandleWithRequest:(WLRRouteRequest *)request;
,来确定handler是否愿意处理,如果愿意,则调用-(BOOL)transitionWithRequest:(WLRRouteRequest *)request error:(NSError *__autoreleasing *)error;
,内部则通过便利方法获取targetViewController与SourceViewController,然后进行转场,核心方法的实现为:
-(BOOL)transitionWithRequest:(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;
}
if ([self preferModalPresentationWithRequest:request]||![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;
}
- (BOOL)preferModalPresentationWithRequest:(WLRRouteRequest *)request;{
return NO;
}
这里根据SourceController的类型进行判断,其实request对象的信息足够可以判断目标视图应该如何打开,从本质上来讲,URL的匹配表达式是跟业务强关联的也是跟UI交互逻辑强关联的,transitionWithRequest方法实现里,你大可以继承一下,然后重写转场过程,甚至你可以在这自己设置iOS7自定义的转场,提供动画控制器和实现转场协议的对象,进而可以整体的控制Appp内部的实现。
WLRRegularExpression
该类继承NSRegularExpression
#import <Foundation/Foundation.h>
@class WLRMatchResult;
@interface WLRRegularExpression : NSRegularExpression
//传入一个URL返回一个匹配结果
-(WLRMatchResult *)matchResultForString:(NSString *)string;
//根据一个URL的表达式创建一个WLRRegularExpression实例
+(WLRRegularExpression *)expressionWithPattern:(NSString *)pattern;
@end
该对象主要的功能是将一个URL传入查看是否匹配,并且将表达式上声明的路径参数从URL上取下来。
比说,我们设置的URL匹配的表达式是: login/:phone([0-9]+),那AppScheme://user/login/138** 这样的URL应该是匹配,并且将138的手机号取出来,对应到phone上,这个过程必须用到正则表达式的分组提取子串的功能,:phone是约定好的提取子串的值对应的key的名字,其实这个url的正则表达式应该是: /login/([0-9]+)$,那么WLRRegularExpression对象需要知道需要提取所有子串的key还有将URL匹配的表达式转换为真正的正则表达式。
-(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<paramResult.numberOfRanges&&i <= self.routerParamNamesArr.count;i++ ) {
NSString * paramName = self.routerParamNamesArr[i-1];
//将值取出,然后将key和value放入到paramDict
NSString * paramValue = [string substringWithRange:[paramResult rangeAtIndex:i]];
[paramDict setObject:paramValue forKey:paramName];
}
}
//最后赋值给WLRMatchResult对象
result.paramProperties = paramDict;
return result;
}
核心代码总共80多行,源码大家可以详阅
WLRRouteMatcher
#import <Foundation/Foundation.h>
@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
属性有如下:
//scheme
@property(nonatomic,copy) NSString * scheme;
//WLRRegularExpression的实例
@property(nonatomic,strong)WLRRegularExpression * regexMatcher;
//匹配的表达式
@property(nonatomic,copy)NSString * routeExpressionPattern;
初始化方法:
-(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;
}
匹配方法:
-(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
@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;
在实现部分,有三个属性:
//每一个URL的匹配表达式route对应一个matcher实例,放在字典中
@property(nonatomic,strong)NSMutableDictionary * routeMatchers;
//每一个URL匹配表达式route对应一个WLRRouteHandler实例
@property(nonatomic,strong)NSMutableDictionary * routeHandles;
//每一个URL匹配表达式route对应一个回调的block
@property(nonatomic,strong)NSMutableDictionary * routeblocks;
在Route挂在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方法:
-(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;
}
以上我们可以看到,Route将匹配的逻辑单独封装到WLRRouteMatcher对象中,将匹配后的结果生成WLRRouteRequest实例以携带足够完整的数据,同时将真正处理视图控制器的转场或者是组件的加载或者是未来可能拓展的handle业务封装到WLRRouteHandler实例中,匹配逻辑对应的处理逻辑干净分离,匹配逻辑可单独塑造业务匹配,处理逻辑可以通过继承扩展或者冲洗WLRRouteHandler的生命周期函数来更好的处理回调业务。如果WLRRouteHandler不能提供足够多的扩展性,则可以使用block回调最大限度的进行扩展。
以上,就是路由部分的整体实现。
转场的扩展
在WLRRouteHandler中,其实我们可以单独控制路由经过的页面跳转的转场。
-(UIViewController *)targetViewControllerWithRequest:(WLRRouteRequest *)request{
}
-(UIViewController *)sourceViewControllerForTransitionWithRequest:(WLRRouteRequest *)request{
}
-(BOOL)transitionWithRequest:(WLRRouteRequest *)request error:(NSError *__autoreleasing *)error{
}
这样的生命周期函数是不是很像UIViewControllerContextTransitioning转场上下文的协议的设定?- (nullable __kindof UIViewController *)viewControllerForKey:(UITransitionContextViewControllerKey)key;
方法使上下文提供目标控制器和源控制器,其实在handler中你完全可以自定义一个子类,在transitionWithRequest方法里,设置遵守UIViewControllerTransitioningDelegate的代理,然后在此提供遵守 UIViewControllerAnimatedTransitioning的动画控制器,然后自定义转场上下文,实现自定义UI转场,而对应的匹配逻辑是与此无关的,我们就可以在路由曾控制全局的页面转场效果。对自定义转场不太熟悉的同学请移步我之前的文章:
ContainerViewController的ViewController 转场
路由的安全
有两个方面可以去做
- WLRRouteHandler实例中,
-(BOOL)shouldHandleWithRequest:(WLRRouteRequest *)request
中可以检测request中的参数,比方说效验source或者是效验业务参数完整等 - WLRRouter实例中handleURL方法,将在随后的WLRRoute的0.0.2版本中加入中间件的支持,就是在找到handler之前,将按照中间件注册的顺序回调中间件,而我们可以在中间件中实现风控业务、认证机制、加密验签等等
路由的效率
目前我们实现的路由是一个同步阻塞型的,在处理并发的时候可能会出现一些问题,或者是在注册比较多的route表达式以后,遍历和匹配的过程会损耗性能,比较好的实现方式是,将Route修改成异步非阻塞型的,但是API全部要换成异步API,起步我们先把同步型的搞定,随后慢慢提供异步版本的路由~
路由的使用
在大部分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的人。
转自:http://www.jianshu.com/p/3a902f274a3d
一步步构建iOS路由的更多相关文章
- 用 VIPER 构建 iOS 应用架构(1)
[编者按]本篇文章由 Jeff Gilbert 和 Conrad Stoll 共同编写,通过构建一个基础示例应用,深入了解 VIPER,并从视图.交互器等多个部件理清 VIPER 的整体布局及思路.通 ...
- 用Model-View-ViewModel构建iOS App(转)
转载自 Model-View-ViewModel for iOS [译] 如果你已经开发一段时间的iOS应用,你一定听说过Model-View-Controller, 即MVC.MVC是构建iOS a ...
- 用Model-View-ViewModel构建iOS App
如果你已经开发一段时间的iOS应用,你一定听说过Model-View-Controller,即MVC.MVC是构建iOS App的标准模式.然而,最近我已经越来越厌倦MVC的一些缺点.在本文,我将重温 ...
- jenkins自动化构建iOS应用配置过程中遇到的问题
最近配置jenkins来自动构建iOS应用,期间遇上不少问题.在这里分享给大家,也给自己留个底,方便下次解决问题. 首先说明下基本情况,我们因为部署jenkins的机器不是Mac,所以不能安装Xcod ...
- 构建 iOS 风格移动 Web 应用程序的8款开发框架
使用 HTML5,CSS3 和 JavaScript 开发移动应用经过实践证明是一种可行的方式.这里收录了几款 iOS 风格的手机应用程序开发框架,帮助您使用擅长的 Web 技术来开发移动应用程序.这 ...
- 用 VIPER 构建 iOS 应用架构(2)
[编者按]本篇文章由 Jeff Gilbert 和 Conrad Stoll 共同编写,通过构建一个基础示例应用,深入了解 VIPER,并从视图.交互器等多个部件理清 VIPER 的整体布局及思路.通 ...
- Java网络编程和NIO详解2:JAVA NIO一步步构建IO多路复用的请求模型
Java网络编程与NIO详解2:JAVA NIO一步步构建IO多路复用的请求模型 知识点 nio 下 I/O 阻塞与非阻塞实现 SocketChannel 介绍 I/O 多路复用的原理 事件选择器与 ...
- Java网络编程与NIO详解2:JAVA NIO 一步步构建I/O多路复用的请求模型
微信公众号[黄小斜]作者是蚂蚁金服 JAVA 工程师,专注于 JAVA 后端技术栈:SpringBoot.SSM全家桶.MySQL.分布式.中间件.微服务,同时也懂点投资理财,坚持学习和写作,相信终身 ...
- 18-Node.js学习笔记-Express-请求处理-构建模块化路由
构建模块化路由 const express = require('express') //创建路由对象 const home = express.Router(); //将路由和请求路径进行匹配 ap ...
随机推荐
- ECharts配置项之title(标题)
1.标题居中 //left的值为'left', 'center', 'right' title:{ left:'center' } 2.主副标题之间的间距 title:{ //默认为10 itemGa ...
- React native中的组建通知通信:
有这么一个需求,在B页面pop()回到A页面,需要A页面执行刷新,那么我们可以采用以下方法: 1:在A页面Push到B页面中,加上一个A页面中的刷新函数做为参数,然后在B页面中在pop()函数封装后通 ...
- P2512 [HAOI2008]糖果传递
题目描述 有n个小朋友坐成一圈,每人有ai个糖果.每人只能给左右两人传递糖果.每人每次传递一个糖果代价为1. 输入输出格式 输入格式: 小朋友个数n 下面n行 ai 输出格式: 求使所有人获得均等糖果 ...
- CentOS6.5下卸载自带的MySQL数据库安装MySQL5.6
1)查看CentOS自带的mysql 输入 rpm -qa | grep mysql mysql-libs-5.1.71-1.el6.x86_64 2)将其自带的mysql版本全部卸载(非常重要,如不 ...
- 幂率定律及绘制Power-law函数
来自:Eastmount 在我们日常生活中Power Law(幂次分布,Power-law Distributions)是常见的一个数学模型,如二八原则.这个世界上是20%的人掌握80%的人的金钱去经 ...
- Codeforces 797C - Minimal string
C. Minimal string 题目链接:http://codeforces.com/problemset/problem/797/C time limit per test 1 second m ...
- Angular 学习笔记 ( timezone + moment + material date-picker + date pipe + asp.net core )
参考 : https://stackoverflow.com/questions/29979609/time-conversion-with-timezoneinfo-for-past-years h ...
- Day3-scrapy爬虫下载图片自定义名称
学习Scrapy过程中发现用Scrapy下载图片时,总是以他们的URL的SHA1 hash值为文件名,如: 图片URL:http://www.example.com/image.jpg 它的SHA1 ...
- HTML标签使用
`<!-- 什么是HTML 超文本标记语言 由标签(属性和实体组成)和内容组成 --> <!-- 定义文档类型 --> <!DOCTYPE html> <!- ...
- (10)进程---Manager数据共享
Manager 能够实现进程之间的数据共享(dict list),但是必须上锁来确保数据的准确性, 队列则可以实现进程之间数据通信 from multiprocessing import Proce ...