iOS组件化曾经是业界的热门话题,现在很少有人再提起这个话题。网上也有很多关于组件化的文章和想法。最经典的就是卡萨神和蘑菇街关于组件化的争论。想想看这些文章的时候,组件化的思想是多么的棒,我觉得他们说的很有道理,而且casa大师在很多思想上应该给了我很大的启发。以及两位大架构师级别的剑拔弩张是否让你真正明白了组件化的重要性。是否在你内心深处引起共鸣?最近看到一个项目,让我对组件化有了更多的思考。1、为什么要组件化,组件化有什么好处?为什么要组件化?看了很多优秀的文章,你肯定会问这个问题。组件化能给我们带来多大的好处?作为一个小公司,就行业而言,涉及组件化的机会非常少,没有大厂的工作经历很难理解透彻组件化。我们可能认为我们的业务模块不够,或者我们没有了解它的好处。其实组件化最大的好处就是每个组件、每个模块都可能独立成为一个app,有自己的生命周期。这样就可以分成不同的业务组模块进行处理。听说京东有一个专门负责消息模块的团队,一个专门负责广告模块的团队,还有一个专门负责发现模块的团队。你会发现,如果没有好的Component思维,这样的多团队协作是非常困难的,维护这个项目的开发迭代已经很困难了。说了这么多,组件化是什么样子的呢?那我就跟着我的脚步,学习分析,讨论。2.组件化的核心思想组件化的核心思想也是我们组件化的基本框架,即如何实现组件化,或者说如何从架构层、业务层等多个层面来实现架构。为了实现组件化,其实就是建立一个中间转换工具。也可以理解为路由,通过路由的思想实现跨服务的数据通信,从而在一定程度上降低各层数据的耦合度。减少各个业务层和其他层面的导入耦合。3、目前实现的组件化方法目前有以下三种思路:ProcotolschemeURLroutingschemetarget-actionschemeProcotolprotocolregistrationscheme关于procotol协议注册方案,很少有人用,也很少有人分享,我也在里面看到这个项目,所以我研究了一下。通过JJProtocolManager作为中间转换。+(void)registerModuleProvider:(id)providerforProtocol:(Protocol*)protocol;+(id)moduleProviderForProtocol:(Protocol*)protocol;所有组件提供的协议和组件提供的服务由中间件统一管理,每个组件提供的协议和服务之间存在一一对应关系。例如:在JJLoginProvider中:应用启动时会调用load方法,注册到JJProtocolManager中。JJLoginProvider遵循JJLoginProvider协议,可以根据业务需要对外提供一些方法。+(void)load{[JJProtocolManagerregisterModuleProvider:[selfnew]forProtocol:@protocol(JJLoginProtocol)];}-(UIViewController*)viewControllerWithInfo:(id)userInfoneedNew:(BOOL)needNewcallback:(JJModuleCallbackBlock)callback{CLoginViewController*vc=[[CLoginViewControlleralloc]init];vc.jj_moduleCallbackBlock=callback;vc.jj_moduleUserInfo=userInfo;returnvc;}这样就可以在需要登录业务模块的地方直接通过JJProtocolManager获取JJLoginProtocol对应的服务提供者JJLoginProvider.如下:idprovider=[JJProtocolManagermoduleProviderForProtocol:@protocol(JJWebviewVCModuleProtocol)];UIViewController*vc=[providerviewControllerWithInfo:objneedNew:YEScallback:^(idinfo){if(callback){callback(info);}}];vc.hidesBottomBarWhenPushed=YES;[self.currentNavpushViewController:vcanimated:YES];URL路由方案最经典的URL路由方案就是蘑菇街的路由组件化,调用方法,调用参数,封装回调方法通过theurl去url,然后通过解析url得到方法名和参数,最后通过消息转发机制调用方法。下面是蘑菇街的路由方法:(如果想在这里了解更多,可以去蘑菇街的路由组件化详细学习)[MGJRouterregisterURLPattern:@"mgj://detail?id=:id"toHandler:^(NSDictionary*routerParameters){NSNumber*id=routerParameters[@"id"];//createviewcontrollerwithid//pushviewcontroller}];在首页调用[MGJRouteropenURL:@"mgj://detail?id=404"]打开对应的详情页。这里可以看到,我们使用url短链的方式,将参数拼接到url查询部分,这样我们就可以通过解析url中的scheme、host、path、query得到需要调用的controller这边走。什么样的参数,以便推送或呈现新页面。解析scheme,host,path核心代码:NSString*scheme=[nsUrlscheme];//解析schemeNSString*module=[nsUrlhost];NSString*action=[[nsUrlpath]stringByReplacingOccurrencesOfString:@"/"withString:@"_"];if(action&&[actionlength]&&[actionhasPrefix:@"_"]){action=[actionstringByReplacingCharactersInRange:NSMakeRange(0,1)withString:@""];}NSString*query=nil;NSArray*pathInfo=[nsUrl.absoluteStringcomponentsSeparatedByString:@"?"];if(pathInfo.count>1){query=[pathInfoobjectAtIndex:1];}解析查询的核心代码:NSMutableDictionary*parameters=nil;NSString*parametersString=query;NSArray*paramStringArr=[parametersStringcomponentsSeparatedByString:@"&"];if(paramStringArr&&[paramStringArrcount]>0){parameters=[NSMutableDictionarydictionary];for(NSString*paramStringinparamStringArr){NSArray*paramArr=[paramStringcomponentsSeparatedByString:@"="];if(paramArr.count>1){NSString*key=[paramArrobjectAtIindex:0];NSString*value=[paramArrobjectAtIndex:1];parameters[key]=[JJRouterunescapeURIComponent:value];}}}returnparameters;这样我们就可以实现组件化,但是有时候我们会遇到如果图片编辑模块无法将UIImage传给对应的模块,这里就需要传入一个新的参数进去。为了解决这个问题,其实我们可以直接把参数丢给下面的arg处理+(nullableid)openURL:(nonnullNSString*)urlStringarg:(nullableid)argerror:(NSError*__nullable*__nullable)errorcompletion:(nullableJJRouterCompletion)完成例如:Action*action=[Actionnew];action.type=JJ_WebView;Params*params=[[Paramsalloc]init];//params.pageID=JJ_LOGIN;action.params=params;NSDictionary*parms=@{Jump_Key_Action:action,Jump_Key_Param:@{WebUrlString:@"http://www.baidu.com",Name:@"小二"},Jump_Key_Callback:[JJFunccallback:^(id_Nullableobject){NSLog(@"%@",object);}]};//ActionJump(parms);[JJRouteropenURL:@"router://JJActionService/showWebVC"arg:parmserror:nilcompletion:parms[Jump_Key_Callback]];}我看过的项目,这个是通过url组件化解析和protocol协议注册,但是没有像蘑菇街一样注册支持哪些URL类型target-actionplantarget-actionplan是基于学习casamaster,CTMediator。卡萨大师认为,要表达非常规的物体是不可能的。如果使用url组件化,需要添加类似UIImageURL注册这样的参数,对于组件化方案的实现完全没有必要,通过URL注册形成的组件化方案的可扩展性和可维护性都会打折扣。蘑菇街没有拆分远程调用和本地互调用需要在app启动时注册URL响应者//理论上页面间的跳转只需要打开一个URL即可。所以对于一个组件,你只需要定义“支持哪些URL”,比如详情页,你大概可以这样做[MGJRouterregisterURLPattern:@"mgj://detail?id=:id"toHandler:^(NSDictionary*routerParameters){NSNumber*id=routerParameters[@"id"];//createviewcontrollerwithid//pushviewcontroller}];casa的组件化主要是基于Mediator模式和Target-Action模式,使用runtime来完成调用。这种组件化方案将远程应用调用和本地应用调用拆分开来,本地应用调用为远程应用调用提供服务,与蘑菇街方案刚好相反。调用方式:先说本地应用调用。本地组件A在某处调用[[CTMediatorsharedInstance]performTarget:targetNameaction:actionNameparams:@{...}]发起对CTMediator的跨组件调用。CTMediator利用获取到的目标和动作信息,通过Objective-C的运行时转换生成目标实例和对应的动作选择器,最终调用目标业务提供的逻辑完成需求。在远程应用调用中,远程应用使用openURL,iOS系统根据info.plist中的scheme配置找到可以响应该URL的应用(在我们讨论的当前上下文中,这是你自己的应用),而应用程序通过AppDelegate接收到URL后,调用CTMediator的openUrl:方法将接收到的URL信息传入。当然CTMediator也可以通过openUrl:options:方法接收后面的options,看你本地业务执行逻辑的充要条件是否包含option数据。传入URL后,CTMediator通过解析URL将请求路由到对应的target和action,后面的过程就变成了上面提到的调用本地应用的过程,最后完成响应。请求的路由操作很少使用本地文件来记录路由表。服务器经常处理这种业务。在服务器领域,正则表达式基本用于路由分析。App中的路由分析可以做的更简单,通过制定URL规范就可以完成。最简单的方式是scheme://target/action。简单地做一个字符串处理,从URL中提取目标和动作信息。提取。例如:/**这里是登录模块的目标**/#import"CTMediator+ModuleLogin.h"NSString*constkCTMediatorTargetA=@"A";NSString*constkCTMediatorActionLoginViewController=@"showLoginController";@implementationCTMediator(ModuleLogin)-(UIViewController*)push_viewControllerForLogin{UIViewController*vc=[selfperformTarget:kCTMediatorTargetAaction:kCTMediatorActionLoginViewControllerparams:nilshouldCacheTarget:NO];if([vcisKindOfClass:[UIViewControllerclass]]){//viewcontroller交付后,外界可以选择是推送还是presentreturnvc;}else{//这里处理异常场景,如何处理取决于产品return[[UIViewControlleralloc]init];}}/**登录模块的动作**/-(UIViewController*)Action_showLoginController:(NSDictionary*)param{JJLoginViewController*vc=[[JJLoginViewControlleralloc]init];returnvc;}看起来target-action路由方案更清晰,但这正是你需要的。接下来target-action的核心代码是:/**if([targetrespondsToSelector:action])判断target是否可以响应action方法,只要能执行这段核心代码,core的main函数代码:**/-(id)safePerformAction:(SEL)actiontarget:(NSObject*)targetparams:(NSDictionary*)params{////创建一个函数签名,这个签名可以是任意的,但是需要注意签名函数的参数个数要和调用NSMethodSignature*methodSig=[targetmethodSignatureForSelector:action]一致;if(methodSig==nil){returnnil;}//获取返回类型constchar*retType=[methodSigmethodReturnType];//判断返回值类型if(strcmp(retType,@encode(void))==0){//InitializeNSInvocation*invocationbysignature=[NSInvocationinvocationWithMethodSignature:methodSig];//如果这条消息有参数需要传入,那么需要按照下面的方法设置参数。需要注意的是atIndex的下标必须从2开始。原因为:01个参数已被target和selector占用[invocationsetArgument:?msatIndex:2];//设置selector[invocationsetSelector:action];//设置target[invocationsetTarget:target];//消息调用[invocationinvoke];returnnil;}if(strcmp(retType,@encode(NSInteger))==0){NSInvocation*invocation=[NSInvocationinvocationWithMethodSignature:methodSig];[invocationsetArgument:?msatIndex:2];[invocationsetSelector:action];[invocationsetTarget:target];[invocationinvoke];NSIntegerresult=0;[invocationgetReturnValue:&result];return@(result);}if(strcmp(retType,@encode(BOOL))==0){NSInvocation*invocation=[NSInvocationinvocationWithMethodSignature:methodSig];[invocationsetArgument:?msatIndex:2];[invocationsetSelector:action];[invocationsetTarget:target];[invocationinvoke];BOOLresult=0;[invocationgetReturnValue:&result];return@(result);}if(strcmp(retType,@encode(CGFloat))==0){NSInvocation*invocation=[NSInvocationinvocationWithMethodSignature:methodSig];[invocationsetArgument:?msatIndex:2];[invocationsetSelector:action];[invocationsetTarget:target];[invocationinvoke];CGFloatresult=0;[invocationgetReturnValue:&result];return@(result);}if(strcmp(retType,@encode(NSUInteger))==0){NSInvocation*invocation=[NSInvocationinvocationWithMethodSignature:methodSig];[invocationsetArgument:?msatIndex:2];[invocationsetSelector:action];[invocationsetTarget:target];[invocationinvoke];NSUIntegerresult=0;[invocationgetReturn]Value:&resultreturn@(result);}#pragmaclangdiagnosticpush#pragmaclangdiagnosticignored"-Warc-performSelector-leaks"return[targetperformSelector:actionwithObject:params];#pragmaclangdiagnosticpop}总结:CTMediator是根据获取到的target和action信息通过objective-C运行时转换生成的目标实例和对应的action选择器,最终调用目标业务提供的逻辑完成需求下面是Git地址的三种实现方式:https://github.com/lumig/JJRouterDemo彩蛋://url编码格式foo://example.com:8042/over/there?name=ferret#nose\_/\__________/\________/\_________/\__/|||||schemeauthoritypathqueryfragmentsscheme://host.domain:port/path/filenamescheme-定义Internet服务的类型。最常见的类型是httphost——定义域主机(http默认主机为www)domain——定义互联网域名,如w3school.com.cn:port——定义主机上的端口号(默认端口号http的为80)path-定义服务器上的路径(如果省略,文件必须在网站的根目录下)。filename-定义文档/资源的名称