组件化在蘑菇街

零、说点什么吧

周末是一个轻松的日子,于是决定写点什么。名字取得比较大,暂时也没有想到应该怎么命名。刚刚开始仔细的看了一下 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
 )
 */

综上可知:这是一个NSStringNSArray 之间的转换。具体的转换规则在上面的这个示例中已经完全的介绍了。上面的是 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 后的样子是这个样子的:

image.png

这个数据结构,看起来 玄之又玄 ,弄了一个字典中的字典。

总之,就是通过 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 是根据 URLPatternself.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 中也没有什么高深莫测的技术点,但是这是一个很经典的代码。经典于设计思想、经典于使用小技术解决大问题。

建议大家下载源码仔细品读,会让你收获更多!

很多的时候我们不是不懂技术,而是不知道如何是使用我们已知的技术。

谢谢!
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,716评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,558评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,431评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,127评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,511评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,692评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,915评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,664评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,412评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,616评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,105评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,424评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,098评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,096评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,869评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,748评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,641评论 2 271

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,100评论 18 139
  • 该文章属于<简书 — 刘小壮>原创,转载请注明: <简书 — 刘小壮> http://www.jianshu.co...
    Yiart阅读 4,532评论 3 49
  • 该文章属于刘小壮原创,转载请注明:刘小壮[https://www.jianshu.com/u/2de707c93d...
    刘小壮阅读 92,703评论 266 518
  • 前言 随着用户的需求越来越多,对App的用户体验也变的要求越来越高。为了更好的应对各种需求,开发人员从软件工程的角...
    一缕殇流化隐半边冰霜阅读 86,512评论 214 1,095
  • 花猫和小狗是一对好朋友,他们俩非常友好。花猫叫喵喵,因为它经常喵喵叫,小狗叫小黑,因为它的毛是黑色的。 ...
    菲菲hmf阅读 264评论 0 1