介绍
JLRoutes是一个URL解析库,可以很方便的处理不同URL schemes以及解析它们的参数,并通过回调block来处理URL对应的操作。
使用场景
对一个App中单独的模块,可以使用openURL的方式进行页面跳转,很好地解耦不同的模块,蘑菇街的组件化之路就是基于URL跳转的方式,当然casa也提出了Target—Action模式下配合category实现的组件化架构,时隔几个月又重写看了两位大神的文章,感觉脑细胞真的不够用啊。
我没有组件化的经验,所以使用JSRoutes仅限于远程调用(服务端下发、Push跳转等),本地调用还是不太敢用这种URL的统一跳转。
使用实例
先来个简单的使用demo
在didFinishLaunchingWithOptions中注册所有的URL
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
[JLRoutes addRoute:@"/:controller" handler:^BOOL(NSDictionary *parameters) {
NSString *controller = parameters[@"controller"];
[self.window.rootViewController presentViewController:[[NSClassFromString(controller) alloc] init] animated:YES completion:^{
}];
return YES;
}];
return YES;
}
- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication annotation:(id)annotation {
return [JLRoutes routeURL:url];
}
打开指定URL资源
NSURL *viewUserURL = [NSURL URLWithString:@"myapp://user/view/joeldev"];
[[UIApplication sharedApplication] openURL:viewUserURL];
原理
JLRoutes本质可以理解为:保存一个全局的Map,key是url,value是对应的block,url和block都会常驻在内存中,这也是为什么casa反对使用URL跳转来实现组件化的原因,当注册的url很多了,对内存的消耗也是很大的。当打开一个URL时,JLRoutes就可以遍历这个全局的map,通过url来执行对应的block。
内部实现
namespace
routeControllersMap是一个NSDictionary类型的单例,key是namespace,value是一个array,里面包含当前namespace下所有的routes。
命名空间也对应我们的URL scheme。
globalRoutes返回全局命名空间
+ (instancetype)globalRoutes {
return [self routesForScheme:kJLRoutesGlobalNamespaceKey];
}
routesForScheme返回指定scheme对应的命名空间,如果不存在就创建一个
+ (instancetype)routesForScheme:(NSString *)scheme {
JLRoutes *routesController = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
routeControllersMap = [[NSMutableDictionary alloc] init];
});
if (!routeControllersMap[scheme]) {
routesController = [[self alloc] init];
routesController.namespaceKey = scheme;
routeControllersMap[scheme] = routesController;
}
routesController = routeControllersMap[scheme];
return routesController;
}
添加Route
注册一个Route到global scheme namespace,并设置其优先级(默认优先级是0),block返回一个bool,如果返回YES表示当前匹配成功,如果返回NO表示继续匹配其他Route(_JLRoute对象)
一个内部类,用下划线开头命名_JLRoute,其结构如下:
@interface _JLRoute : NSObject
@property (nonatomic, weak) JLRoutes *parentRoutesController;
@property (nonatomic, strong) NSString *pattern;
@property (nonatomic, strong) BOOL (^block)(NSDictionary *parameters);
@property (nonatomic, assign) NSUInteger priority;
@property (nonatomic, strong) NSArray *patternPathComponents;
- (NSDictionary *)parametersForURL:(NSURL *)URL components:(NSArray *)URLComponents;
@end
addRoute这个方法对原始routePattern字符串做一个加工和过滤的操作,如去掉圆括号
- (void)addRoute:(NSString *)routePattern priority:(NSUInteger)priority handler:(BOOL (^)(NSDictionary *parameters))handlerBlock {
// if there's a pair of parenthesis, process optionals, trim the parenthesis, put it on trimmedRoute
NSString *trimmedRoute = routePattern;
// repeat until no parenthesis pair is found
while ([trimmedRoute rangeOfString:@")" options:NSBackwardsSearch].location > [trimmedRoute rangeOfString:@"(" options:NSBackwardsSearch].location) {
//Build route with the optionals
NSString *patternWithOptionals = [trimmedRoute stringByReplacingOccurrencesOfString:@"(" withString:@""];
patternWithOptionals = [patternWithOptionals stringByReplacingOccurrencesOfString:@")" withString:@""];
[self registerRoute:patternWithOptionals priority:priority handler:handlerBlock];
//Build route without optionals
NSRange rangeOfLastParentheses = [trimmedRoute rangeOfString:@"(" options:NSBackwardsSearch];
NSRange rangeToRemove = NSMakeRange(rangeOfLastParentheses.location, trimmedRoute.length - rangeOfLastParentheses.location);
NSString *patternWithLastOptionalRemoved = [trimmedRoute stringByReplacingCharactersInRange:rangeToRemove withString:@""];
//Remove any parenthesis for other optionals that might still be in the route
NSString *patternWithoutOptionals = [patternWithLastOptionalRemoved stringByReplacingOccurrencesOfString:@"(" withString:@""];
patternWithoutOptionals = [patternWithoutOptionals stringByReplacingOccurrencesOfString:@")" withString:@""];
[self registerRoute:patternWithoutOptionals priority:priority handler:handlerBlock];
trimmedRoute = patternWithLastOptionalRemoved;
}
//Only register original route if trimmedRoute haven't been modified.
if (trimmedRoute == routePattern) {
[self registerRoute:routePattern priority:priority handler:handlerBlock];
}
}
registerRoute这个方法就是把_JLRoute插入到routeControllersMap[kJLRoutesGlobalNamespaceKey]这个list中的时候,用到了插入排序的思想,priority高的在前面。这样enum这个_JLRoute List的时候,如果match pattern,就return,自然就解决了「路径匹配优先级」的问题。
- (void)registerRoute:(NSString *)routePattern priority:(NSUInteger)priority handler:(BOOL (^)(NSDictionary *parameters))handlerBlock {
_JLRoute *route = [[_JLRoute alloc] init];
route.pattern = routePattern;
route.priority = priority;
route.block = [handlerBlock copy];
route.parentRoutesController = self;
if (!route.block) {
route.block = [^BOOL (NSDictionary *params) {
return YES;
} copy];
}
if (priority == 0 || self.routes.count == 0) {
[self.routes addObject:route];
} else {
NSArray *existingRoutes = self.routes;
NSUInteger index = 0;
BOOL addedRoute = NO;
// search through existing routes looking for a lower priority route than this one
// 找到一个优先级低的,采用插入排序
for (_JLRoute *existingRoute in existingRoutes) {
if (existingRoute.priority < priority) {
// if found, add the route after it
[self.routes insertObject:route atIndex:index];
addedRoute = YES;
break;
}
index++;
}
// if we weren't able to find a lower priority route, this is the new lowest priority route (or same priority as self.routes.lastObject) and should just be added
if (!addedRoute)
[self.routes addObject:route];
}
}