路由方案
App内部的路由设计,主要需要解决2个问题:
1、各个页面和组件之间的跳转问题。
2、各个组件之间相互调用。
比较常见的路由方案
URLRouter
Target-Action
Protocol-Class
URLRouter
主要步骤是定制匹配规则,URL进行解析处理,路由匹配,逻辑处理及回调;
一个URL的结构字段解析:
主流开源库
JLRoutes
JLRoutes结构图:
JLRoutes全局会保存一个Map,这个Map会以scheme为Key,JLRoutes为Value。所以在routeControllerMap里面每个scheme都是唯一的
在每个JLRoutes的数组里面,会按照路由的优先级进行排列,优先级高的排列在前面
路由注册:
- (void)addRoute:(NSString *)routePattern priority:(NSUInteger)priority handler:(BOOL (^)(NSDictionary<NSString *, id> *parameters))handlerBlock
{
NSArray <NSString *> *optionalRoutePatterns = [JLRParsingUtilities expandOptionalRoutePatternsForPattern:routePattern];
JLRRouteDefinition *route = [[JLRGlobal_routeDefinitionClass alloc] initWithPattern:routePattern priority:priority handlerBlock:handlerBlock];
if (optionalRoutePatterns.count > 0) {
// there are optional params, parse and add them
for (NSString *pattern in optionalRoutePatterns) {
JLRRouteDefinition *optionalRoute = [[JLRGlobal_routeDefinitionClass alloc] initWithPattern:pattern priority:priority handlerBlock:handlerBlock];
[self _registerRoute:optionalRoute];
[self _verboseLog:@"Automatically created optional route: %@", optionalRoute];
}
return;
}
[self _registerRoute:route];
}
- (void)_registerRoute:(JLRRouteDefinition *)route
{
if (route.priority == 0 || self.mutableRoutes.count == 0) {
[self.mutableRoutes addObject:route];
} else {
NSUInteger index = 0;
BOOL addedRoute = NO;
// search through existing routes looking for a lower priority route than this one
for (JLRRouteDefinition *existingRoute in [self.mutableRoutes copy]) {
if (existingRoute.priority < route.priority) {
// if found, add the route after it
[self.mutableRoutes 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.mutableRoutes addObject:route];
}
}
[route didBecomeRegisteredForScheme:self.scheme];
}
路由查找:
首先根据外部传进来的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;
JLRRouteRequestOptions options = [self _routeRequestOptions];
JLRRouteRequest *request = [[JLRRouteRequest alloc] initWithURL:URL options:options additionalParameters:parameters];
for (JLRRouteDefinition *route in [self.mutableRoutes copy]) {
// check each route for a matching response
JLRRouteResponse *response = [route routeResponseForRequest:request];
if (!response.isMatch) {
continue;
}
[self _verboseLog:@"Successfully matched %@", route];
if (!executeRouteBlock) {
// if we shouldn't execute but it was a match, we're done now
return YES;
}
[self _verboseLog:@"Match parameters are %@", response.parameters];
// Call the handler block
didRoute = [route callHandlerBlockWithParameters:response.parameters];
if (didRoute) {
// if it was routed successfully, we're done - otherwise, continue trying to route
break;
}
}
if (!didRoute) {
[self _verboseLog:@"Could not find a matching route"];
}
// if we couldn't find a match and this routes controller specifies to fallback and its also not the global routes controller, then...
if (!didRoute && self.shouldFallbackToGlobalRoutes && ![self _isGlobalRoutesController]) {
[self _verboseLog:@"Falling back to global routes..."];
didRoute = [[JLRoutes globalRoutes] _routeURL:URL withParameters:parameters executeRouteBlock:executeRouteBlock];
}
// 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;
}
- (JLRRouteResponse *)routeResponseForRequest:(JLRRouteRequest *)request
{
BOOL patternContainsWildcard = [self.patternPathComponents containsObject:@"*"];
if (request.pathComponents.count != self.patternPathComponents.count && !patternContainsWildcard) {
// definitely not a match, nothing left to do
return [JLRRouteResponse invalidMatchResponse];
}
NSDictionary *routeVariables = [self routeVariablesForRequest:request];
if (routeVariables != nil) {
// It's a match, set up the param dictionary and create a valid match response
NSDictionary *matchParams = [self matchParametersForRequest:request routeVariables:routeVariables];
return [JLRRouteResponse validMatchResponseWithParameters:matchParams];
} else {
// nil variables indicates no match, so return an invalid match response
return [JLRRouteResponse invalidMatchResponse];
}
}
//关键匹配代码
- (NSDictionary <NSString *, NSString *> *)routeVariablesForRequest:(JLRRouteRequest *)request
{
NSMutableDictionary *routeVariables = [NSMutableDictionary dictionary];
BOOL isMatch = YES;
NSUInteger index = 0;
for (NSString *patternComponent in self.patternPathComponents) {
NSString *URLComponent = nil;
BOOL isPatternComponentWildcard = [patternComponent isEqualToString:@"*"];
if (index < [request.pathComponents count]) {
URLComponent = request.pathComponents[index];
} else if (!isPatternComponentWildcard) {
// URLComponent is not a wildcard and index is >= request.pathComponents.count, so bail
isMatch = NO;
break;
}
if ([patternComponent hasPrefix:@":"]) {
// this is a variable, set it in the params
NSAssert(URLComponent != nil, @"URLComponent cannot be nil");
NSString *variableName = [self routeVariableNameForValue:patternComponent];
NSString *variableValue = [self routeVariableValueForValue:URLComponent];
// Consult the parsing utilities as well to do any other standard variable transformations
BOOL decodePlusSymbols = ((request.options & JLRRouteRequestOptionDecodePlusSymbols) == JLRRouteRequestOptionDecodePlusSymbols);
variableValue = [JLRParsingUtilities variableValueFrom:variableValue decodePlusSymbols:decodePlusSymbols];
routeVariables[variableName] = variableValue;
} else if (isPatternComponentWildcard) {
// match wildcards
NSUInteger minRequiredParams = index;
if (request.pathComponents.count >= minRequiredParams) {
// match: /a/b/c/* has to be matched by at least /a/b/c
routeVariables[JLRouteWildcardComponentsKey] = [request.pathComponents subarrayWithRange:NSMakeRange(index, request.pathComponents.count - index)];
isMatch = YES;
} else {
// not a match: /a/b/c/* cannot be matched by URL /a/b/
isMatch = NO;
}
break;
} else if (![patternComponent isEqualToString:URLComponent]) {
// break if this is a static component and it isn't a match
isMatch = NO;
break;
}
index++;
}
if (!isMatch) {
// Return nil to indicate that there was not a match
routeVariables = nil;
}
return [routeVariables copy];
}
Target-Action
主流开源库
CTMediator
传统的中间人Mediator的模式是这样的,每个页面或者组件都会依赖中间者,各个组件之间互相不再依赖,组件间调用只依赖中间者Mediator,Mediator还会依赖其他组件,形成了相互依赖。
CTmediator最终去掉了中间者Mediator对组件的依赖,各个组件之间互相不再依赖,组件间调用只依赖中间者Mediator,Mediator不依赖其他任何组件。结构如下:
主要思想是利用了Target-Action简单粗暴的思想,利用Runtime解决解耦的问题。
关键代码:
- (id)performTarget:(NSString *)targetName action:(NSString *)actionName params:(NSDictionary *)params shouldCacheTarget:(BOOL)shouldCacheTarget
{
if (targetName == nil || actionName == nil) {
return nil;
}
NSString *swiftModuleName = params[kCTMediatorParamsKeySwiftTargetModuleName];
// generate target
NSString *targetClassString = nil;
if (swiftModuleName.length > 0) {
targetClassString = [NSString stringWithFormat:@"%@.Target_%@", swiftModuleName, targetName];
} else {
targetClassString = [NSString stringWithFormat:@"Target_%@", targetName];
}
NSObject *target = [self safeFetchCachedTarget:targetClassString];
if (target == nil) {
Class targetClass = NSClassFromString(targetClassString);
target = [[targetClass alloc] init];
}
// generate action
NSString *actionString = [NSString stringWithFormat:@"Action_%@:", actionName];
SEL action = NSSelectorFromString(actionString);
if (target == nil) {
// 这里是处理无响应请求的地方之一,这个demo做得比较简单,如果没有可以响应的target,就直接return了。实际开发过程中是可以事先给一个固定的target专门用于在这个时候顶上,然后处理这种请求的
[self NoTargetActionResponseWithTargetString:targetClassString selectorString:actionString originParams:params];
return nil;
}
if (shouldCacheTarget) {
[self safeSetCachedTarget:target key:targetClassString];
}
if ([target respondsToSelector:action]) {
return [self safePerformAction:action target:target params:params];
} else {
// 这里是处理无响应请求的地方,如果无响应,则尝试调用对应target的notFound方法统一处理
SEL action = NSSelectorFromString(@"notFound:");
if ([target respondsToSelector:action]) {
return [self safePerformAction:action target:target params:params];
} else {
// 这里也是处理无响应请求的地方,在notFound都没有的时候,这个demo是直接return了。实际开发过程中,可以用前面提到的固定的target顶上的。
[self NoTargetActionResponseWithTargetString:targetClassString selectorString:actionString originParams:params];
@synchronized (self) {
[self.cachedTarget removeObjectForKey:targetClassString];
}
return nil;
}
}
}
targetName就是调用接口的Object,actionName就是调用方法的SEL,params是参数,shouldCacheTarget代表是否需要缓存,如果需要缓存就把target存起来,Key是targetClassString,Value是target。
Protocol-Class
通过协议和类绑定,核心思想和代理传值是一样的,遵循协议,实现协议中的方法。
[ModuleManager registerClass:ClassA forProtocol:ProtocolA] 的结果就是在 MM 内部维护的 dict 里新加了一个映射关系。
[ModuleManager classForProtocol:ProtocolA] 的返回结果就是之前在 MM 内部 dict 里 protocol 对应的 class,使用方不需要关心这个 class 是个什么东东,反正实现了 ProtocolA 协议,拿来用就行。
组件化注意事项
模块粒度的划分
对于 iOS 这种面向对象编程的开发模式来说,我们应该遵循以下五个原则,即 SOLID 原则。
单一功能原则:对象功能要单一,不要在一个对象里添加很多功能。
开闭原则:扩展是开放的,修改是封闭的。
里氏替换原则:子类对象是可以替代基类对象的。
接口隔离原则:接口的用途要单一,不要在一个接口上根据不同入参实现多个功能。
依赖反转原则:方法应该依赖抽象,不要依赖实例。iOS 开发就是高层业务方法依赖于协议。
一个成熟的项目如何进行组件化:
APP内部解耦,划分底层基础库,中间件,及上层业务组件。
最好先拆解成APP内部本地的组件库,解决组件之间的依赖及颗粒度的细化。待本地各个组件库结构稳定之后,再落地成单独远程组件库。这样做的好处是,涉及多个远程组件库的调整操作比较麻烦,比如把一个库的文件迁移到另一个库。往往业务开发和组件库的拆分是同步进行的,如果上来就独立远程pod库,会导致代码合并比较麻烦,还极易丢失代码。
组件库最好使用tag号,如果直接使用分支,该分支上可能任意一个组件库的小改动都可能会导致整个项目编译报错。
组件库的podSpec文件内部依赖的其他pod库不要限定版本,在项目的podfile文件中再进行版本限定。这样可以统一版本管理,避免每个组件库依赖的相同pod库版本不一致,带来的pod install失败。 Demo地址:gitee.com/llhlj/route…