组件化在蘑菇街
零、说点什么吧
周末是一个轻松的日子,于是决定写点什么。名字取得比较大,暂时也没有想到应该怎么命名。刚刚开始仔细的看了一下 MGJRouter 中的代码,所以一边看,也就一边的做点笔记。现在看完了,整理了一下就发出来了。都是以我看代码的顺序来描述的,建议各位大神自行下载代码细细品读!!
也是因为看了一圈的组件化、我更不知道 组件化 到底是什么。尽然这样、那就忘记组件化,看看在代码方面是如何实现组件化的。仅仅是通过 Demo 看大神们的代码到底使用了什么技术点。
蘑菇街的代码,在这里:MGJRouter 下载下来一起看看吧。
本介绍,仅仅是针对 Demo 的代码。
一、Demo 的概要
打开项目 MGJRouterDemo,看到两个控制器 DemoListViewController 与 DemoDetailViewController,结合项目运行起来的效果很好的能看出这两个控制器的实现逻辑。
1.1 List 控制器的列表数据源
当前就控制器的数据源是 titleWithHandlers 与 titles,仔细研究发现 DemoListViewController 中 +registerWithTitle: handler: 被调用于 DemoDetailViewController 中的 +load 方法中。
看完这个逻辑,发现了一个特点:在整个APP 的运行中仅创建了一个 DemoDetailViewController 对象,是一个 target 与 block 相结合的一个优秀技巧。很值得学习与借鉴。
1.2 Detail 控制器的实现
这个有点绕、但是经典就是经典。首先你会发现在 .h 文件中几乎什么都没有,但是能做出不同的显示。这个还是要归功于 +load 中 target 与 block 的巧妙使用。
二、MGJRouter 核心实现
先瞄一眼 MGJRouter 的头文件,清一色的 Class 方法,对于一个工具 Class,我也喜欢这么去设计。首先是因为这样使用特别的方便,其次尽然是工具就尽量不要拖泥带水的搞一些属性在那里。当然、技术是不能一概而论的,但是很多的时候也会发现很多的小伙伴根本不会写工具。关于 MGJRouter 还有一个巧妙的设计, 那就是本质是一个单例,只是没有被暴露。在我看这个代码之前,我还以为这种方式是我的独创,我之前也会这么干。看到一个单例的第一个正常反应是感觉看一下是否重写了类似 -init 这样的方法。可喜可贺,MGJRouter 是一个单纯的单例,没有重写这些方法。
大概看了一下头文件中各个方法的简单注释介绍,看完之后就可以开始分析具体的实现了。具体的入口的都在 DemoDetailViewController 中。
demoBasicUsage 方法
这个方法的原型如下:
- (void)demoBasicUsage
{
[MGJRouter registerURLPattern:@"mgj://foo/bar" toHandler:^(NSDictionary *routerParameters) {
[self appendLog:@"匹配到了 url,以下是相关信息"];
[self appendLog:[NSString stringWithFormat:@"routerParameters:%@", routerParameters]];
}];
[MGJRouter openURL:@"mgj://foo/bar"];
}
上面的代码中做了两件事:注册与调用。在这里我们要注意的是,在很多的时候我们在看优秀的代码的时候,可以先看方法,然后猜想一下大神的逻辑是什么。看到上面的代码,我的猜想是:通过一个 URL 注册信息,后期会通过这个 URL 找到注册的这些信息。,那么问题来了,到底是通过什么样的规则来注册呢?这些信息注册到哪里了呢?带着这些问题,我们开始打断点、并运行代码。
这里需要说明一点:这里仅仅是一个示例,并不是说注册就应该与具体的 openURL:写到一起。如果说在实际的项目开发中写到一起,那还叫 组件化 么?
2.1 注册实现
当代码运行起来之后,你的断点可能会在这里停留一会了:
pathComponentsFromURL:
我们先来看一看 pathComponentsFromURL: 方法中都做了什么事:
- (NSArray*)pathComponentsFromURL:(NSString*)URL
{
NSMutableArray *pathComponents = [NSMutableArray array];
if ([URL rangeOfString:@"://"].location != NSNotFound) {
NSArray *pathSegments = [URL componentsSeparatedByString:@"://"];
// 如果 URL 包含协议,那么把协议作为第一个元素放进去
[pathComponents addObject:pathSegments[0]];
// 如果只有协议,那么放一个占位符
URL = pathSegments.lastObject;
if (!URL.length) {
[pathComponents addObject:MGJ_ROUTER_WILDCARD_CHARACTER];
}
}
for (NSString *pathComponent in [[NSURL URLWithString:URL] pathComponents]) {
if ([pathComponent isEqualToString:@"/"]) continue;
if ([[pathComponent substringToIndex:1] isEqualToString:@"?"]) break;
[pathComponents addObject:pathComponent];
}
return [pathComponents copy];
}
这个方法,就是对一个字符串做一个解析操作,那么是如何解析的呢?我先把本 Demo 中用到的解析结果,在这里 Copy 一下:
mgj://foo/bar 对应的是:@[@"mgj", @"foo", @"bar"]
mgj://category/家居 对应的结果是:@[@"mgj"]
mgj://search/:query 对应的结果是:@[@"mgj", @"search", @":query"]
mgj:// 对应的结果是:@[@"mgj", @"~"] 是不是有一种通配符的感觉,对、没错。
如果对上面的结果有歧义,那么可以从上依次的学习一下这个方法中用到的方法:
NSString 中的 pathComponentsFromURL: 方法
关于这个方法,我给出的一个示例如下:
// 定义一个字符串
NSString* URL = @"Coder";
NSArray *pathSegments = [URL componentsSeparatedByString:@"://"];
NSLog(@"%@", pathSegments);
/** 打印结果:
(
Coder
)
*/
// 修改字符串
URL = @"Coder://";
pathSegments = [URL componentsSeparatedByString:@"://"];
NSLog(@"%@", pathSegments);
/** 打印结果:
(
Coder,
""
)
*/
// 修改字符串
URL = @"Coder://HG";
pathSegments = [URL componentsSeparatedByString:@"://"];
NSLog(@"%@", pathSegments);
/** 打印结果:
(
Coder,
HG
)
*/
综上可知:这是一个NSString 与 NSArray 之间的转换。具体的转换规则在上面的这个示例中已经完全的介绍了。上面的是 NSString 转换成 NSArray ,那么 NSArray 又是如何转成 NSString 的呢?
NSURL 的 pathComponents 方法
关于这个方法,我给出的示例代码如下:
// 定义一个字符串
NSURL* URL = [NSURL URLWithString:@"CoderHG/iOS/8968"];
// 执行方法
NSArray* pathComponents = [URL pathComponents];
// 打印
NSLog(@"%@", pathComponents);
/** 打印结果:
(
CoderHG,
iOS,
8968
)
*/
// 修改字符串 带中文
URL = [NSURL URLWithString:@"CoderHG/iOS/8968/朱鸿"];
pathComponents = [URL pathComponents];
// 打印
NSLog(@"%@", pathComponents);
/** 打印结果:
nil
*/
综上所述:这个方法是针对字符串中带有 / 而言的,一旦 URL 中带有中文,则返回为 nil。
NSString 的 substring 方法
关于这个方法,我给出的示例代码如下:
// 定义一个字符串
NSString* coder = @"coder";
NSString* substringToIndex = [coder substringToIndex:1];
NSString* substringFromIndex = [coder substringFromIndex:3];
NSLog(@"To = %@, From = %@", substringToIndex, substringFromIndex);
/** 打印结果:
To = c, From = er
*/
// 直接 crash
NSString* crashString = [coder substringToIndex:10];
NSLog(@"%@", crashString);
终上所述:就是一个字符剪切的方法,但是一定要注意 crash 的情况。
addURLPattern: 方法
上一个方法执行结束之后,就会回到这个方法中来, 这个方法很有意思,具体如下:
- (NSMutableDictionary *)addURLPattern:(NSString *)URLPattern
{
NSArray *pathComponents = [self pathComponentsFromURL:URLPattern];
NSMutableDictionary* subRoutes = self.routes;
for (NSString* pathComponent in pathComponents) {
if (![subRoutes objectForKey:pathComponent]) {
subRoutes[pathComponent] = [[NSMutableDictionary alloc] init];
}
subRoutes = subRoutes[pathComponent];
}
return subRoutes;
}
当从 pathComponentsFromURL: 中返回 pathComponents 数据之后, 对 self.routes 做了一系列的赋值处理。其中 routes 是一个关键的字典,就是用来承载注册信息的,具体是如何承载的呢?
主要是的代码就是在哪个 for 语句中,这是又是一个很奇妙的设计方式。第一次注册 mgj://foo/bar 后的样子是这个样子的:
这个数据结构,看起来 玄之又玄 ,弄了一个字典中的字典。
总之,就是通过 pathComponents 中的元素,在 self.routes 中布了一个局,里面什么都没有,都是空字典。 还有一个特点是,将最后一个字典做了 return 了。欲知有何用途,请看下面分解。
通过上面的方法之后, 会到这个方法中来:
addURLPattern: andHandler:
这个方法的原型是:
- (void)addURLPattern:(NSString *)URLPattern andHandler:(MGJRouterHandler)handler
{
NSMutableDictionary *subRoutes = [self addURLPattern:URLPattern];
if (handler && subRoutes) {
subRoutes[@"_"] = [handler copy];
}
}
通过 addURLPattern: 返回的 subRoutes 是根据 URLPattern 在 self.routes 中注册的最后一个字典。
看了这个方法,终于明白返回最后一个字典的原因就是将 注册的 handler 放到通过 _ 作为 key 值放到这个字典中。
到这里,一个简单的注册流程,就走完了。那么问题来了,这个注册流程到底都干了什么事情呢?总结如下:
通过 URLPattern 按照一定的规则解析成一个数组,然后将数组又按照一定的规则布局于 self.routes 中,最后将注册的 handler 放到通过 _ 作为 key 值放到最后一个字典中。
那么问题又来了:注册之后,又该如何使用呢?我的猜想是这样的:
注册仅仅是通过一定的规则,将注册的信息暂存于 self.routes 中,这些信息将服务于后期的 openURL: 操作。具体是如何服务的呢?请看下回分解。
以上是注册的整个流程,接下来看一下在调用 openURL 中又做了什么事?
2.2 openURL 流程
代码一路执行,最终到这里看到了一个莫名的转换,先来看看:
image.png
字符串的 stringByAddingPercentEscapesUsingEncoding: 方法,就这个方法,我给出的示例代码如下:
NSString* URL = @"鸿哥最帅";
NSLog(@"%@", URL);
// 打印结果: 鸿哥最帅
URL = [URL stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
NSLog(@"%@", URL);
// 打印结果: %E9%B8%BF%E5%93%A5%E6%9C%80%E5%B8%85
URL = [URL stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
NSLog(@"%@", URL);
// 打印结果:鸿哥最帅
这个方法就是一个字符转义用的,主要是处理中文的情况。
下一步,跳进这个方法中来:
image.png
这里需要注意的是,这个方法传进来的 URL 是经过转义的。在往下,是一个 for 循环。主要就是查看当前传进来的 URL 是否被注册过,如果在 self.routes 任意一个节点中没有找到像匹配的节点,那么就直接返回 nil。在这个 for 循环中的技术点,数组排序的很简单,可以忽略,先来看一下这个方法:
+ (BOOL)checkIfContainsSpecialCharacter:(NSString *)checkedString {
NSCharacterSet *specialCharactersSet = [NSCharacterSet characterSetWithCharactersInString:specialCharacters];
return [checkedString rangeOfCharacterFromSet:specialCharactersSet].location != NSNotFound;
}
看方法的命名就知道,这个方法是用来检查 checkedString 是否包含特殊字符串的 ** specialCharacters(/?&.),的确,NSCharacterSet** 这东西挺厉害的,至少面试的时候面试官可能会这样的问:你用过 OC 中的集合么?如果说你回答了字典与数组,那么就等于面试官什么都没有问,如果说你回答了 NSSet,在强调一下 NSCharacterSet,恐怕面试官会爱上你的。关于这个,我给出的示例代码如下:
NSCharacterSet *specialCharactersSet = [NSCharacterSet characterSetWithCharactersInString:specialCharacters];
NSString* text = @"CoderHG";
NSRange rang = [text rangeOfCharacterFromSet:specialCharactersSet];
NSLog(@"%zd, %zd", rang.location, rang.length);
// 打印结果: 9223372036854775807, 0
text = @"Coder?HG";
rang = [text rangeOfCharacterFromSet:specialCharactersSet];
NSLog(@"%zd, %zd", rang.location, rang.length);
// 打印结果: 5, 1
text = @"C/?oder/?HG?";
rang = [text rangeOfCharacterFromSet:specialCharactersSet options:NSBackwardsSearch];
NSLog(@"%zd, %zd", rang.location, rang.length);
// 打印结果: 11, 1
关于 NSURLComponents,我的示例代码如下:
NSString* url = @"http://coder.com?coder=iOS&name=HG";
// Extract Params From Query.
NSArray<NSURLQueryItem *> *queryItems = [[NSURLComponents alloc] initWithURL:[[NSURL alloc] initWithString:url] resolvingAgainstBaseURL:false].queryItems;
NSMutableDictionary* parameters = [NSMutableDictionary dictionary];
for (NSURLQueryItem *item in queryItems) {
parameters[item.name] = item.value;
}
NSLog(@"%@", parameters);
/** 打印结果:
{
coder = iOS;
name = HG;
}
*/
明白了,原来是为了获取 URL 后面的参数的。想当年某些人使用一个一个的截取,然后一个一个的去拼接的。
2.3 注销操作
注销方法 +deregisterURLPattern:
主要是这个方法:
- (void)removeURLPattern:(NSString *)URLPattern
{
NSMutableArray *pathComponents = [NSMutableArray arrayWithArray:[self pathComponentsFromURL:URLPattern]];
// 只删除该 pattern 的最后一级
if (pathComponents.count >= 1) {
// 假如 URLPattern 为 a/b/c, components 就是 @"a.b.c" 正好可以作为 KVC 的 key
NSString *components = [pathComponents componentsJoinedByString:@"."];
NSMutableDictionary *route = [self.routes valueForKeyPath:components];
if (route.count >= 1) {
NSString *lastComponent = [pathComponents lastObject];
[pathComponents removeLastObject];
// 有可能是根 key,这样就是 self.routes 了
route = self.routes;
if (pathComponents.count) {
NSString *componentsWithoutLast = [pathComponents componentsJoinedByString:@"."];
route = [self.routes valueForKeyPath:componentsWithoutLast];
}
[route removeObjectForKey:lastComponent];
}
}
}
在注册分析的过程中已经发现,所谓的注册就是通过 URL 按照一定的规则将消息存于 self.routes 中,那么销毁正好相反。
至此整个流程也就差不多了, 还有没介绍到的,其实都有包括了。
三、想法
看完之后,也发现在 MGJRouter 中也没有什么高深莫测的技术点,但是这是一个很经典的代码。经典于设计思想、经典于使用小技术解决大问题。
建议大家下载源码仔细品读,会让你收获更多!
很多的时候我们不是不懂技术,而是不知道如何是使用我们已知的技术。