当前位置: 首页 > news >正文

如皋网站制作广州网站建设选哪家

如皋网站制作,广州网站建设选哪家,乐清人才网官方网站,南京建设网站多少钱路由是实现模块间解耦的一个有效工具。如果要进行组件化开发#xff0c;路由是必不可少的一部分。目前iOS上绝大部分的路由工具都是基于URL匹配的#xff0c;优缺点都很明显。这篇文章里将会给出一个更加原生和安全的设计#xff0c;这个设计的特点是#xff1a; 路由时用p…路由是实现模块间解耦的一个有效工具。如果要进行组件化开发路由是必不可少的一部分。目前iOS上绝大部分的路由工具都是基于URL匹配的优缺点都很明显。这篇文章里将会给出一个更加原生和安全的设计这个设计的特点是 路由时用protocol寻找模块可以对模块进行固定的依赖注入和运行时依赖注入支持不同模块间进行接口适配和转发因此无需和某个固定的protocol关联充分解耦的同时增加类型安全支持移除已执行的路由封装UIKit界面跳转方法可以一键跳转和移除支持storyboard支持其他任意模块可以检测界面跳转时的大部分错误如果你想要一个能够充分解耦、类型安全、有依赖注入功能的路由器那这个就是目前所能找到的最佳方案。 这个路由工具是为了实践VIPER模式而设计的目的是为VIPER提供依赖注入功能不过它也可以用于MVC、MVP、MVVM没有任何限制。 工具和Demo地址ZIKRouter。 Router的作用 首先我们需要梳理清楚为什么我们需要RouterRouter能带来什么好处解决什么问题我们需要一个什么样的Router 路由缺失时的情况 没有路由时界面跳转的代码就很容易产生模块间耦合。 iOS中执行界面跳转时用的是UIViewController上提供的跳转方法 [sourceViewController.navigationController pushViewController:destinationViewController animated:YES]; [sourceViewController presentViewController:destinationViewController animated:YES completion:nil]; 如果是直接导入destinationViewController的头文件进行引用就会导致和destinationViewController模块产生耦合。类似的一个模块引用另一个模块时也会产生这样的耦合。因此我们需要一个方式来获取destinationViewController但又不能对其产生直接引用。 这时候就需要路由提供的寻找模块的功能。以某种动态的方式获取目的模块。 那么路由是怎么解决模块耦合的呢在上一篇VIPER讲解里路由有这几个主要职责 寻找指定模块执行具体的路由操作声明模块的依赖声明模块的对外接口对模块内各部分进行依赖注入通过这几个功能就能实现模块间的完全解耦。 寻找模块 路由最重要的功能就是给出一种寻找某个指定模块的方案。这个方案是松耦合的获取到的模块在另一端可以随时被另一个相同功能的模块替换从而实现两个模块之间的解耦。 寻找模块的实现方式其实只有有限的几种 用一个字符串identifier来标识某个对应的界面URL Router、UIStoryboardSegue利用Objective-C的runtime特性直接调用目的模块的方法CTMediator用一个protocol来和某个界面进行匹配蘑菇街的第二种路由和阿里的BeeHive这样就可以更安全的对目的模块进行传参这几种方案的优劣将在之后逐一细说。 声明依赖和接口 一个模块A有时候需要使用其他模块的功能例如最通用的log功能不同的app有不同的log模块如果模块A对通用性要求很高log方法就不能在模块A里写死而是应该通过外部调用。这时这个模块A就依赖于一个log模块了。App在使用模块A的时候需要知道它的依赖从而在使用模块A之前对其注入依赖。 当通过cocoapods这样的包管理工具来配置不同模块间的依赖时一般模块之间是强耦合的模块是一一对应的当需要替换一个模块时会很麻烦容易牵一发而动全身。如果是一个单一功能模块的确需要依赖其他特定的各种库时那这样做没有问题。但是如果是一个业务模块中引用了另一个业务模块就应该尽量避免互相耦合。因为不同的业务模块一般是由不同的人负责应该避免出现一个业务模块的简单修改例如调整了方法或者属性的名字导致引用了它的业务模块也必须修改的情况。 这时候业务模块就需要在代码里声明自己需要依赖的模块让app在使用时提供这些模块从而充分解耦。 示例代码 protocol ZIKLoginServiceInput NSObject - (void)loginWithAccount:(NSString *)account password:(NSString *)password success:(void(^_Nullable)(void))successHandler error:(void(^_Nullable)(void))errorHandler; end interface ZIKNoteListViewController () //笔记界面需要登录后才能查看因此在头文件中声明让外部在使用的时候设置此属性 property (nonatomic, strong) idZIKLoginServiceInput loginService; end 这个声明依赖的工作其实是模块的Builder的职责。一个界面模块大部分情况下都不止有一个UIViewController也有其他一些Manager或者Service而这些角色都是有各自的依赖的都统一由模块的Builder声明再在Builder内部设置依赖。不过在上一篇文章的VIPER讲解里我们把Builder的职责也放到了Router里让每个模块单独提供一个自己的Router。因此在这里Router是一个离散的设计而不是一个单例Router掌管所有的路由。这样的好处就是每个模块可以充分定制和控制自己的路由过程。 可以声明依赖也就可以同时声明模块的对外接口。这两者很相似所以不再重复说明。 Builder和依赖注入 执行路由的同时用Builder进行模块构建构建的时候就对模块内各个角色进行依赖注入。当你调用某个模块的时候需要的不是某个简单的具体类而是一个构建完毕的模块中的某个具体类。在使用这个模块前模块需要做一些初始化的操作比如VIPER里设置各个角色之间的依赖关系就是一个初始化操作。因此使用路由去获取某个模块中的类必定需要通过模块的Builder进行。很多路由工具都缺失了这部分功能。 你可以把依赖注入简单地看成对目的模块传参。在进行界面跳转和使用某个模块时经常需要设置目的模块的一些参数例如设置delegate回调。这时候就必须调用一些目的模块的方法或者传递一些对象。由于每个模块需要的参数都不一样目前大部分Router都是使用字典包裹参数进行传递。但其实还有更好、更安全的方案下面将会进行详解。 你也可以把Router、Builder和Dependency Injector分开不过如果Router是一个离散型的设计那么都交给各自的Router去做也很合理同时能够减少代码量也能够提供细粒度的AOP。 现有的Router 梳理完了路由的职责现在来比较一下现有的各种Router方案。关于各个方案的具体实现细节我就不再展开看可以参考这篇详解的文章iOS 组件化 —— 路由设计思路分析。 URL Router 目前绝大多数的Router都是用一串URL来表示需要打开的某个界面代码上看来大概是这样 //注册某个URL和路由处理进行匹配保存 [URLRouter registerURL:settings handler:^(NSDictionary *userInfo) {UIViewController *sourceViewController userInfo[sourceViewController]; //获取其他参数 id param userInfo[param]; //获取需要的界面 UIViewController *settingViewController [[SettingViewController alloc] init]; [sourceViewController.navigationController pushViewController: settingViewController animated:YES]; }]; //调用路由 [URLRouter openURL:myapp://noteList/settings?debugtrue userInfo:params completion:^(NSDictionary *info) {}]; 传递一串URL就能打开noteList界面的settings界面用字典包裹需要传递的参数有时候还会把UIKit的push、present等方法进行简单封装提供给调用者。 这种方式的优点和缺点都很突出。 优点 极高的动态性 这是动态性最高的方案甚至可以在运行时随时修改路由规则指向不同的界面。也可以很轻松地支持多级页面的跳转。 如果你的app是电商类app需要经常做活动app内的跳转规则经常变动那么就很适合使用URL的方案。 统一多端路由规则 URL的方案是最容易跨平台实现的iOS、Andorid、web、PC都按照URL来进行路由时也就可以统一管理多端的路由规则降低多端各自维护和修改的成本让不懂技术的运营人员也可以简单快速地修改路由。 和上一条一样这也是一个和业务强相关的优点。如果你有统一多端的业务需求使用URL也很合适。 适配URL scheme iOS中的URL scheme可以跨进程通信从app外打开app内的某个指定页面。当app内的页面都能使用URL打开时也就直接兼容了URL scheme无需再做额外的工作。 缺点 不适合通用模块 URL Router的设计只适合UI模块不适合其他功能性模块的组件。功能性模块的调用并不需要如此强的动态特性除非是有模块热更新的需求否则一个模块的调用在一个版本里应该总是稳定不变的即便要进行模块间解耦也不应该用这种方式。 安全性差 字符串匹配的方式无法进行编译时检查当页面配置出错时只能在运行时才能发现。如果某个开发人员不小心在字符串里加了一个空格编译时也无法发现。你可以用宏定义来减少这种出错的几率。 维护困难 没有高效地声明接口的方式只能从文档里查找编写时必须仔细对照字符串及其参数类型。 传参通过字典来进行参数类型无法保证而且也无法准确地知道所调用的接口需要哪些参数。当目的模块进行了接口升级修改了参数类型和数量那所有用到的地方都要一一修改并且没有编译器的帮助你无法知道是否遗漏了某些地方。这将会给维护和重构带来极大的成本。 针对这个问题蘑菇街的选择是用另一个Router用protocol来获取目的模块再进行调用增加安全性。 Protocol Router 这个方案也很容易理解。把之前的字符串匹配改成了protocol匹配就能获取到一个实现了某个protocol的对象。 开源方案里只看到了BeeHive实现了这样的方式 idZIKLoginServiceInput loginService [[BeeHive shareInstance] createService:protocol(ZIKLoginServiceInput)]; 优点 安全性好维护简单 再对这个对象调用protocol中的方法就十分安全了。在重构和修改时有了编译器的类型检查效率更高。 适用于所有模块 Protocol更加符合OC和Swift原生的设计思想任何模块都可以使用而不局限于UI模块。 优雅地声明依赖 模块A需要用到登录模块但是它要怎么才能声明这种依赖关系呢如果使用Protocol Router那就只需要在头文件里定义一个属性 property (nonatomic, string) idZIKLoginServiceInput *loginService; 如果这个依赖是必需依赖而不是一个可选依赖那就添加到初始化参数里 interface ModuleA () - (instancetype)initWithLoginService:(idZIKLoginServiceInput)loginService; end 问题是如果这样的依赖很多那么初始化方法就会变得很长。因此更好的做法是由Builder进行固定的依赖注入再提供给外部。目前BeeHive并没有提供依赖注入的功能。 缺点 动态性有限 你可以维护一份protocol和模块的对照表使用动态的protocol来尝试动态地更改路由规则也可以在Protocol Router之上封装一层URL Router专门用于动态性的需求。 需要额外适配URL Scheme 使用了Protocol Router就需要再额外处理URL Scheme了。不过这样也是正常的解析URL Scheme本来就应该放到另一个单独的模块里。 Protocol是否会导致耦合 很多谈到这种方案的文章都会指出和URL Router相比Protocol Router会导致调用者引用目的模块的protocol因此会产生耦合。我认为这是对解耦的错误理解。 要想避免耦合首先要弄清楚我们需要什么程度的解耦。我的定义是模块A调用了模块B模块B的接口或者实现在做出简单的修改时或者模块B被替换为相同功能的模块C时模块A不需要进行任何修改。这时候就可以认为模块A和模块B是解耦的。 业务设计的互相关联 有些时候表达出两个模块之间的关联是有意义的。 当一个界面A需要展示一个登录界面时它可能需要向登录界面传递一个提示语参数用于在登录界面显示一串提示。这时候界面A在调用登录界面时是要求登录界面能够显示这个自定义提示语的在业务设计中就存在两个模块间的强关联性。这时候URL Router和Protocol Router没有任何区别包括下面将要提到的Target-Action路由方式都存在耦合但是Protocol Router通过简单地改善是可以把这部分耦合去除的。 URL Router [URLRouter openURL:login userInfo:{message:请登录查看笔记详情}]; Protocol Router: protocol LoginViewInput NSObject property (nonatomic, copy) NSString *message; end //获取登录界面进行设置 UIViewControllerLoginViewInput *loginViewController [ProtocolRouter destinationForProtocol:protocol(LoginViewInput)]; loginViewController.message 请登录查看笔记详情; 由于字典传参的原因URL Router只不过是把这种接口上的关联隐藏到了字典key里它在参数字典里使用message时就是在隐式地使用LoginViewInput的接口。 这种业务设计上导致的模块之间互相关联是不可避免的也是不需要去隐藏的。隐藏了反而会引来麻烦。如果登录界面的属性名字变了从NSString *message改成了NSString *notifyString那么URL Router在register的时候也必须修改传参时的代码。如果register是由登录界面自己执行和处理的而不是由App Context来处理的那么此时参数key是固定为notifyString的那就会要求所有调用者的传参key也修改为notifyString这种修改如果缺少编译器的帮助会很危险目前是用宏来减少这种修改导致的工作量。而Protocol Router在修改时就能充分利用编译器进行检查能够保证100%安全。 因此URL Router并不能做到解耦只是隐藏了接口关联而已。一旦遇到了需要修改或者重构的情况麻烦就出现了在替换宏的时候你还必须仔细检查有没有哪里有直接使用字符串的key。只是简单地修改名字还是可控的如果是需要增加参数呢这时候就根本无法检查哪里遗漏了参数传递了。这就是字典传参的坏处。 关于这部分的讨论也可以参考Peak大佬的文章iOS组件化方案。 Protocol Router在这种情况下也需要作出修改但是它能帮助你安全高效地进行重构。而且只要稍加改进也可以完全无需修改。解决方法就是把Protocol分离为Required Interface和Provided Interface。 Required Interface 和 Provided Interface 模块的接口其实是有Required Interface和Provided Interface的区别的。Required Interface就是调用者需要用到的接口Provided Interface就是实际的被调用者提供的接口。 在UML的组件图中就很明确地表现出了这两者的概念。下图中的半圆就是Required Interface框外的圆圈就是Provided Interface 那么如何实施Required Interface和Provided Interface上一篇文章里已经讨论过应该由App Context在一个adapter里进行接口适配从而使得调用者可以继续在内部使用Required Interfaceadapter负责把Required Interface和修改后的Provided Interface进行适配。 示例代码 protocol ModuleARequiredLoginViewInput NSObject property (nonatomic, copy) NSString *message; end //Module A中的调用代码 UIViewControllerModuleARequiredLoginViewInput *loginViewController [ZIKViewRouterToView(LoginViewInput) makeDestination]; loginViewController.message 请登录查看笔记详情; //Login Module Provided Interface protocol ProvidedLoginViewInput NSObject property (nonatomic, copy) NSString *notifyString; end //App Context 中的 Adapter用Objective-C的category或者Swift的extension进行接口适配 interface LoginViewController (ModuleAAdapte) ModuleARequiredLoginViewInput property (nonatomic, copy) NSString *message; end implementation LoginViewController (ModuleAAdapte) - (void)setMessage:(NSString *)message { self.notifyString message; } - (NSString *)message { return self.notifyString; } end 用category、extension、NSProxy等技术兼容新旧接口工作全部由模块的使用和装配者App Context完成。如果LoginViewController已经有了自己的message属性这时候就说明新的登录模块是不可兼容的必须有某一方做出修改。当然接口适配能做的事情是有限的例如一个接口从同步变成了异步那么这时候两个模块也是不能兼容的。 因此如果模块需要进行解耦那么它的接口在设计的时候就应该十分仔细尽量不要在参数中引入太多其他的模块依赖。 只有存在Required Interface和Provided Interface概念的设计才能做到彻底的解耦。目前的路由方案都缺失了这一部分。 Target-Action CTMediator的方案把对模块的调用封装到Target-Action中利用了Objective-C的runtime特性省略了Target-Action的注册和绑定工作直接通过CTMediator中介者调用目的模块的方法。 implementation CTMediator (CTMediatorModuleAActions) - (UIViewController *)CTMediator_viewControllerForDetail { UIViewController *viewController [self performTarget:kCTMediatorTargetA action:kCTMediatorActionNativFetchDetailViewController params:{key:value} shouldCacheTarget:NO ]; if ([viewController isKindOfClass:[UIViewController class]]) { // view controller 交付出去之后可以由外界选择是push还是present return viewController; } else { // 这里处理异常场景具体如何处理取决于产品 return [[UIViewController alloc] init]; } } end -performTarget:action:params:shouldCacheTarget:方法通过NSClassFromString获取目的模块提供的Target类再调用Target提供的Action实现了方法调用 implementation CTMediator - (id)performTarget:(NSString *)targetName action:(NSString *)actionName params:(NSDictionary *)params shouldCacheTarget:(BOOL)shouldCacheTarget { NSString *targetClassString [NSString stringWithFormat:Target_%, targetName]; NSString *actionString [NSString stringWithFormat:Action_%:, actionName]; Class targetClass; NSObject *target self.cachedTarget[targetClassString]; if (target nil) { targetClass NSClassFromString(targetClassString); target [[targetClass alloc] init]; } SEL action NSSelectorFromString(actionString); if (target nil) { // 这里是处理无响应请求的地方之一这个demo做得比较简单如果没有可以响应的target就直接return了。实际开发过程中是可以事先给一个固定的target专门用于在这个时候顶上然后处理这种请求的 return nil; } if (shouldCacheTarget) { self.cachedTarget[targetClassString] target; } if ([target respondsToSelector:action]) { return [self safePerformAction:action target:target params:params]; } else { // 有可能target是Swift对象 actionString [NSString stringWithFormat:Action_%WithParams:, actionName]; action NSSelectorFromString(actionString); 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.cachedTarget removeObjectForKey:targetClassString]; return nil; } } } } end 优点 实现简洁整个实现的代码量很少省略了路由注册的步骤可以减少一部分内存消耗和时间消耗但是也略微降低了调用时的性能使用场景不局限于界面模块所有模块都可以通过中介者调用缺点 在调用action时使用字典传参无法保证类型安全维护困难直接使用runtime互相调用难以明确地区分Required Interface和Provided Interface因此其实无法实现完全解耦。和URL Router一样在目的模块变化时调用模块也必须做出修改过于依赖runtime特性和Swift的类型安全设计是不兼容的也无法跨平台多端实现UIStoryboardSegue 苹果的storyboard其实也有一套路由API只不过它的局限性很大。在这里简单介绍一下 implementation SourceViewController- (void)showLoginViewController {//调用在storyboard中定义好的segue identifier [self performSegueWithIdentifier:presentLoginViewController sender:nil]; } //perform segue时的回调 - (BOOL)shouldPerformSegueWithIdentifier:(NSString *)identifier sender:(nullable id)sender { return YES; } //配置目的界面 - (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender { //用[segue destinationViewController]获取目的界面再对目的界面进行传参 } end UIStoryboardSegue是和storyboard一起使用的storyboard中定义好了一些界面跳转的参数比如转场方式push、present等在执行路由前执行路由的UIViewController会收到回调让调用者配置目的界面的参数。 在storyboard中连接segue其实是跳转到一个已经配置好界面的view controller也就是和View相关的参数都已经做好了依赖注入。但是自定义的依赖却还是需要在代码中注入所以又给了我们一个-prepareForSegue:sender:回调。 我不建议使用segue因为它会导致强耦合。但是我们可以借鉴UIStoryboardSegue的sourceViewController、destinationViewController、封装跳转逻辑到segue子类、对页面执行依赖注入等设计。 总结 总结了几个路由工具之后我的结论是路由的选择还是以业务需求为先。当对动态性要求极高、或者需要多平台统一路由则选择URL Router其他情况下我更倾向于使用Protocol Router。和Peak大大的结论一致。 Protocol Router目前并没有一个成熟的开源方案。因此我造了个轮子增加了上面提到的一些需求。 ZIKRouter的特性 离散式管理 每个模块都对应一个或者多个router子类在子类中管理各自的路由过程包括对象的生成、模块的初始化、路由状态管理、AOP等。路由时需要使用对应的router子类而不是一个单例router掌管所有的路由。如果想要避免引用子类带来的耦合可以用protocol动态获取router子类或者用父类泛型在调用者中代替子类。 采用离散式的设计的原因是想让各个模块对路由拥有充分的控制权。 一个router子类的简单实现如下 interface ZIKLoginViewRouter : ZIKViewRouter end implementation ZIKLoginViewRouter //app启动时注册对应的模块和Router //不使用load和initialize方法因为在Swift中已经不适用 (void)registerRoutableDestination { [self registerView:[ZIKLoginViewController class]]; [self registerViewProtocol:ZIKRoutableProtocol(ZIKLoginViewProtocol)]; } //执行路由时返回对应的viewController或者UIView - (id)destinationWithConfiguration:(ZIKViewRouteConfiguration *)configuration { UIStoryboard *sb [UIStoryboard storyboardWithName:Main bundle:nil]; ZIKLoginViewController *destination [sb instantiateViewControllerWithIdentifier:ZIKLoginViewController]; return destination; } //检查来自storyboard的界面是否需要让外界进行 (BOOL)destinationPrepared:(UIViewControllerZIKLoginViewProtocol *)destination { if (destination.loginService ! nil) { return YES; } return NO; } //初始化工作 - (void)prepareDestination:(UIViewControllerZIKLoginViewProtocol *)destination configuration:(__kindof ZIKViewRouteConfiguration *)configuration { if (destination.loginService nil) { //ZIKLoginService也可以用ZIKServiceRouter动态获取 destination.loginService [ZIKLoginService new]; } } //路由时的AOP回调 (void)router:(nullable ZIKViewRouter *)router willPerformRouteOnDestination:(id)destination fromSource:(id)source { } (void)router:(nullable ZIKViewRouter *)router didPerformRouteOnDestination:(id)destination fromSource:(id)source { } (void)router:(nullable ZIKViewRouter *)router willRemoveRouteOnDestination:(id)destination fromSource:(id)source { } (void)router:(nullable ZIKViewRouter *)router didRemoveRouteOnDestination:(id)destination fromSource:(id)source { } end 你甚至可以在不同情况下返回不同的destination而调用者对此完全不知情。例如一个alertViewRouter为了兼容UIAlertView和UIAlertController可以在router内部iOS8以上使用UIAlertControlleriOS8以下则使用UIAlertView。 一切路由的控制都在router类内部不需要模块做出任何额外的修改。 自由定义路由参数 路由的配置信息都存储在configuration里在调用者执行路由的时候传入。基本的跳转方法如下 //跳转到Login界面[ZIKLoginViewRouterperformFromSource:self //界面跳转时的源界面configuring:^(ZIKViewRouteConfiguration *config) {//跳转类型支持push、presentModally、presentAsPopover、performSegue、show、showDetail、addChild、addSubview、custom、getDestinationconfig.routeType ZIKViewRouteTypePush; config.animated NO; config.prepareDestination ^(idZIKLoginViewProtocol destination) { //跳转前配置界面 }; config.routeCompletion ^(idNoteEditorProtocol destination) { //跳转成功并结束处理 }; config.performerErrorHandler ^(ZIKRouteAction routeAction, NSError * error) { //跳转失败处理有失败的详细信息 }; }]; Configuration只能在初始化block里配置出了block以后就无法修改。你也可以用一个configuration子类添加更多自定义信息。 如果不需要复杂的配置也可以只用最简单的跳转 [ZIKLoginViewRouter performFromSource:self routeType:ZIKViewRouteTypePush]; 移除已执行的路由 你可以先初始化一个router再交给其他角色执行路由 //初始化router self.loginRouter [[ZIKLoginViewRouter alloc] initWithConfiguring:^(ZIKViewRouteConfiguration * _Nonnull config) {config.source self; config.routeType ZIKViewRouteTypePush; }]; //执行路由 if ([self.loginRouter canPerform] NO) { NSLog(此时无法执行路由:%,self.loginRouter); return; } [self.loginRouter performRouteWithSuccessHandler:^{ NSLog(performer: push success); } performerErrorHandler:^(ZIKRouteAction routeAction, NSError * _Nonnull error) { NSLog(performer: push failed: %,error); }]; 当你需要消除已经展示的界面或者销毁一个模块时可以调用移除路由方法一键移除 if ([self.loginRouter canRemove] NO) {NSLog(此时无法移除路由:%, self.loginRouter); return; } [self.loginRouter removeRouteWithSuccessHandler:^{ NSLog(performer: pop success); } performerErrorHandler:^(ZIKRouteAction routeAction, NSError * _Nonnull error) { NSLog(performer: pop failed,error:%,error); }]; 从而无需再区分调用pop、dismiss、removeFromParentViewController、removeFromSuperview等方法。 通过protocol获取对应模块 Protocol作为匹配标识 我们不想让外部引用ZIKLoginViewRouter头文件导致耦合调用者只需要获取一个符合了ZIKLoginViewProtocol的view controller因此只需要根据ZIKLoginViewProtocol获取到对应的router子类然后在子类上调用父类ZIKViewRouter提供的路由方法即可这样就可以做到隐藏子类。 使用ZIKViewRouterToView和ZIKViewRouterToModule宏即可通过protocol获取到对应的router子类并且子类返回的destination必定符合ZIKLoginViewProtocol [ZIKViewRouterToView(ZIKLoginViewProtocol)performFromSource:selfconfiguring:^(ZIKViewRouteConfiguration *config) {config.routeType ZIKViewRouteTypePush;config.prepareDestination ^(idZIKLoginViewProtocol destination) {//跳转前配置界面 }; config.routeCompletion ^(idZIKLoginViewProtocol destination) { //跳转成功并结束处理 }; }]; 这时候ZIKLoginViewProtocol就相当于LoginView模块的唯一identifier不能再用到其他view controller上。你可以用多个protocol注册同一个router用于区分requiredProtocol和providedProtocol。 多对一匹配 有时候一些第三方的模块或者系统模块并没有提供自己的router你可以为其封装一个router此时可以有多个不同的router管理同一个UIViewController或者UIView类。这些router可能提供了不同的功能比如同样是alertRouterrouterA可能是用于封装UIAlertControllerrouterB可能是用于兼容UIAlertView和UIAlertController这时候要如何区分并获取两个不同的router 像这种提供了独特功能的router需要你使用configuration的子类然后让子类conform对应功能的protocol。于是就可以根据configuration的protocol来获取对应的router [ZIKViewRouterToModule(ZIKCompatibleAlertConfigProtocol)performFromSource:selfconfiguring:^(ZIKViewRouteConfigurationZIKCompatibleAlertConfigProtocol * _Nonnull config) {config.routeType ZIKViewRouteTypeCustom;config.title Compatible Alert;config.message Test custom route for alert with UIAlertView and UIAlertController; [config addCancelButtonTitle:Cancel handler:^{ NSLog(Tap cancel alert); }]; [config addOtherButtonTitle:Hello handler:^{ NSLog(Tap hello button); }]; config.routeCompletion ^(id _Nonnull destination) { NSLog(show custom alert complete); }; }]; 如果模块自己提供了router并且这个router用于依赖注入不能被其他router替代可以声明本router为本模块的唯一指定router当有多个router尝试管理此模块时启动时就会产生断言错误。 依赖注入和依赖声明 固定依赖和运行时依赖 模块的依赖分为固定依赖和运行时参数依赖。 固定依赖就类似于VIPER各角色之间的依赖关系是一个模块中固定不变的依赖。这种依赖只需要在router内部的-prepareDestination:configuration:固定配置即可。 运行时依赖就是外部传入的参数由configuration负责传递然后同样是在-prepareDestination:configuration:中获取configuration并配置destination。你可以用一个configuration子类和router配对这样就能添加更多自定义信息。 如果依赖参数很简单也可以让router直接对destination进行配置声明router的destination遵守ZIKLoginViewProtocol让调用者在prepareDestination里设置destination。但是如果依赖涉及到了model对象的传递并且由于需要隔离View和Modeldestination不能接触到这些model对象这时候还是需要让configuration传递依赖在router内部再把model传给负责管理model的角色。 因此configuration和destination的protocol就负责依赖声明和暴露接口。调用者只需要传入protocol里要求的参数或者调用一些初始化方法即可至于router内部怎么使用和配置这些依赖调用者就不用关心了。 直接在头文件中声明 声明一个protocol是一个router的config protocol或者view protocol时只需要让这个protocol继承自ZIKViewConfigRoutable或者ZIKViewRoutable即可。这样所有的依赖声明都可以在头文件里明确表示不必再从文档中查找。 使用泛型指明特定router 一个模块可以直接在内部用ZIKViewRouterToModule和ZIKViewRouterToView动态获取router也可以在头文件里添加一个router属性让builder注入。 那么一个模块怎么向builder声明自己需要某个特定功能的router呢答案是父类泛型。 ZIKRouter支持用泛型指定参数类型。在OC中可以这样使用 //注意这个示例代码只是用于演示泛型的意思实际运行时必须要用一个ZIKViewRouter子类才可以 [ZIKViewRouterUIViewController *performFromSource:selfconfiguring:^(ZIKViewRouteConfigurationZIKLoginConfigProtocol *config) {config.routeType ZIKViewRouteTypePerformSegue;config.configureSegue(^(ZIKViewRouteSegueConfiguration *segueConfig) { segueConfig.identifier showLoginViewController; ); }]; ZIKViewRouterUIViewController *就是一个指定了泛型的类尖括号中指定了router的destination和configuration类型。这一串说明相当于是在声明这是一个destination为UIViewController类型用ZIKViewRouteConfigurationZIKLoginConfigProtocol *作为执行路由时的configuration的router类。你可以对configuration再添加protocol表明这个configuration必须遵守指定的protocol。 这时你就可以用父类泛型来声明一个router类这个router类的configuration符合特定的config protocol。而且在写的时候Xcode会给你自动补全。这是一种很好的隐藏子类的方式而且符合原生的语法。 但是由于OC中的类都是Class类型因此只能这样声明一个实例属性 property (nonatomic, strong) ZIKViewRouterUIViewController * *loginViewRouter; Builder只能注入一个router实例而不是一个router class。因此在OC里一般不这么使用。 但是在Swift这种类型安全语言中这种模式就能更好地发挥作用了你可以直接注入一个符合某个泛型的router //在Builder中注入alertRouter swiftSampleViewController.alertRouter Router.to(RoutableViewModuleZIKCompatibleAlertConfigProtocol()) class SwiftSampleViewController: UIViewController { //在Builder里注入alertRouterClass后就可以直接用这个routerClass执行路由var alertRouter: ViewRouterAny! IBAction func testInjectedRouter(_ sender: Any) { self.alertRouter.perform( from: self, configuring: { (config, prepareDestination, prepareModule) in prepareModule({ moduleConfig in //moduleConfig在类型推断时就是ZIKCompatibleAlertConfigProtocol无需在判断后再强制转换 moduleConfig.title Compatible Alert moduleConfig.message Test custom route for alert with UIAlertView and UIAlertController moduleConfig.addCancelButtonTitle(Cancel, handler: { print(Tap cancel alert) }) moduleConfig.addOtherButtonTitle(Hello, handler: { print(Tap Hello alert) }) }) } } } 声明了ViewRouterAny的属性后外部就可以直接注入一个对应的router。可以用这种设计模式来转移、集中获取router的职责。 Router可以在定义的时候限制自己的泛型 Objective-C: interface ZIKCompatibleAlertViewRouter : ZIKViewRouterUIViewController * end Swift: class ZIKCompatibleAlertViewRouter: ZIKViewRouterUIViewController { } 这样在传递的时候就可以让编译器检查router是否正确。 调用安全和类型安全 上面的演示已经展示了类型安全的处理由protocol和泛型共同完成了这个类型安全的设计。不过有一些问题还需要特别注意。 编译检查 使用ZIKViewRouterToModule和ZIKViewRouterToView时会对传入的protocol进行编译检查。保证传入的protocol是可路由的protocol不能随意滥用。具体用到的方式有些复杂而且在Objective-C和Swift上使用了两种方式来实现编译检查具体实现可以看源代码。 泛型的协变和逆变 Swift的自定义泛型不支持协变所以使用起来有点奇怪。 let alertRouterClass: ZIKViewRouterUIViewController.Type//编译错误//ZIKCompatibleAlertViewRouter.Type is ZIKViewRouterUIViewController.TypealertRouterClass ZIKCompatibleAlertViewRouter.self Swift的自定义泛型不支持子类型转为父类型因此把ZIKViewRouterUIViewController.Type赋值给ZIKViewRouterUIViewController.Type类型时就会出现编译错误。奇怪的是反过来逆变反而没有编译错误。而Swift原生的集合类型是支持协变的。从2015年开始就有人提议Swift对自定义泛型加入协变到现在也没支持。在Objective-C里自定义泛型是可以正常协变的。 因此在swift里使用了另一个类来包裹真正的router而这个类是可以随意指定泛型的。 用Adapter兼容接口 可以用不同的protocol获取到相同的router。也就是requiredProtocol和providedProtocol只要有声明都可以获取到同一个router。 首先检查requiredProtocol和providedProtocol确定两个接口提供的功能是一致的。否则无法兼容。 为Provided模块添加Required Interface requiredProtocol是外部的要求目的模块额外兼容的由App Context在ZIKViewAdapter的子类里进行接口兼容。 protocol ModuleARequiredLoginViewInput ZIKViewRoutable property (nonatomic, copy) NSString *message; end //Module A中的调用代码 UIViewControllerModuleARequiredLoginViewInput *loginViewController [ProtocolRouter destinationForProtocol:protocol(LoginViewInput)]; loginViewController.message 请登录查看笔记详情; //Login Module Provided Interface protocol ProvidedLoginViewInput NSObject property (nonatomic, copy) NSString *notifyString; end //ZIKEditorAdapter.hZIKViewAdapter子类 interface ZIKEditorAdapter : ZIKViewRouteAdapter end //ZIKEditorAdapter.m //用Objective-C的category、Swift的extension进行接口适配 interface LoginViewController (ModuleAAdapte) ModuleARequiredLoginViewInput property (nonatomic, copy) NSString *message; end implementation LoginViewController (ModuleAAdapte) - (void)setMessage:(NSString *)message { self.notifyString message; } - (NSString *)message { return self.notifyString; } end implementation ZIKEditorAdapter (void)registerRoutableDestination { //注册NoteListRequiredNoteEditorProtocol和ZIKEditorViewRouter匹配 [ZIKEditorViewRouter registerViewProtocol:ZIKRoutableProtocol(NoteListRequiredNoteEditorProtocol)]; } end 用中介者转发接口 如果遇到protocol里的一些delegate需要兼容 protocol ModuleARequiredLoginViewDelegate NSObject - (void)didFinishLogin; end protocol ModuleARequiredLoginViewInput ZIKViewRoutable property (nonatomic, copy) NSString *message; property (nonatomic, weak) idModuleARequiredLoginViewDelegate delegate; end protocol LoginViewDelegate NSObject - (void)didLogin; end protocol ProvidedLoginViewInput NSObject property (nonatomic, copy) NSString *notifyString; property (nonatomic, weak) idLoginViewDelegate delegate; end 这种情况在OC里可以hook-setDelegate:方法用NSProxy来进行消息转发把LoginViewDelegate的消息转发为对应的ModuleARequiredLoginViewDelegate中的消息。 不过Swift里就不适合这么干了相同方法有不同参数类型时可以用一个新的router代替真正的router在新的router里插入一个中介者负责转发接口 implementation ZIKEditorMediatorViewRouter(void)registerRoutableDestination {//注册NoteListRequiredNoteEditorProtocol和新的ZIKEditorMediatorViewRouter配对而不是目的模块中的ZIKEditorViewRouter //新的ZIKEditorMediatorViewRouter负责调用ZIKEditorViewRouter插入一个中介者 [self registerView:/* mediator的类*/]; [self registerViewProtocol:ZIKRoutableProtocol(NoteListRequiredNoteEditorProtocol)]; } - (id)destinationWithConfiguration:(ZIKViewRouteConfiguration *)configuration { //用ZIKEditorViewRouter获取真正的destination idProvidedLoginViewInput realDestination [ZIKEditorViewRouter makeDestination]; //获取一个负责转发ProvidedLoginViewInput和ModuleARequiredLoginViewInput的mediator idModuleARequiredLoginViewInput mediator MediatorForDestination(realDestination); return mediator; } end 一般来说并不需要立即把所有的protocol都分离为requiredProtocol和providedProtocol。调用模块和目的模块可以暂时共用protocol或者只是简单地改个名字在第一次需要替换模块的时候再对protocol进行分离。 封装UIKit跳转和移除方法 封装iOS的路由方法 ZIKViewRouter把UIKit中路由相关的方法 -pushViewController:animated:-presentViewController:animated:completion:UIPopoverController的-presentPopoverFromRect:inView:permittedArrowDirections:animated:UIPopoverPresentationController的配置-performSegueWithIdentifier:sender:-showViewController:sender:-showDetailViewController:sender:-addChildViewController:-addSubview:全都统一封装可以用枚举一键切换 [ZIKViewRouterToView(ZIKLoginViewProtocol)performFromSource:self routeType::ZIKViewRouteTypePush]; 对应的枚举值 ZIKViewRouteTypePushZIKViewRouteTypePresentModallyZIKViewRouteTypePresentAsPopoverZIKViewRouteTypePerformSegueZIKViewRouteTypeShowZIKViewRouteTypeShowDetailZIKViewRouteTypeAddAsChildViewControllerZIKViewRouteTypeAddAsSubviewZIKViewRouteTypeCustomZIKViewRouteTypeGetDestination移除路由时也不必再判断不同情况分别调用-popViewControllerAnimated:、-dismissViewControllerAnimated:completion:、-dismissPopoverAnimated:、-removeFromParentViewController、-removeFromSuperview等方法。 ZIKViewRouter会在内部自动调用对应的方法。 识别adaptative类型的路由 -performSegueWithIdentifier:sender:、-showViewController:sender:、-showDetailViewController:sender:这些adaptative的路由方法系统会根据不同的情况适配UINavigationController和UISplitViewController选择调用push、present或者其他方式。直接调用时无法明确知道最终调用的是哪个方法也就无法移除界面。 ZIKViewRouter可以识别这些路由方法在调用后真正执行的路由操作所以你现在也可以在使用这些方法后移除界面。 支持自定义路由 ZIKViewRouter也支持在子类中提供自定义的路由和移除路由方法。只要写好对应的协议即可。 关于extension里的跳转方法 App extension里还有一些特有的跳转方法比如Watch扩展里WKInterfaceController的-pushControllerWithName:context:和-popControllerShare扩展里SLComposeServiceViewController的-pushConfigurationViewController:和-popConfigurationViewController。 看了一下extension的种类有十几个懒得一个个去适配了。而且extension里的界面不会特别复杂不是特别需要路由工具。如果你需要适配extension可以自己增加也可以用ZIKViewRouteTypeCustom来适配。 支持storyboard ZIKViewRouter支持storyboard这也是和其他Router相比更强的地方。毕竟storyboard有时候也是很好用的当使用了storyboard的项目中途使用router的时候总不能为了适配router把所有使用storyboard的界面都重构吧 适配storyboard的原理是hook了所有UIViewController的-prepareForSegue:sender:方法检查destinationViewController是否遵守ZIKRoutableView协议如果遵守就说明是一个由router管理的界面获取注册的对应router类生成router实例对其进行依赖注入。如果destination需要传入动态参数就会调用sourceViewController的-prepareDestinationFromExternal:configuration:方法让sourceViewController传参。如果有多个router类注册了同一个view controller则取随机的一个router。 你不需要对现有的模块做任何修改就可以直接兼容。而且原来view controller中的-prepareForSegue:sender:也能照常使用。 AOP ZIKViewRouter会在一个界面执行路由和移除路由的时候对所有注册了此界面的router回调4个方法 (void)router:(nullable ZIKViewRouter *)router willPerformRouteOnDestination:(id)destination fromSource:(id)source { } (void)router:(nullable ZIKViewRouter *)router didPerformRouteOnDestination:(id)destination fromSource:(id)source { } (void)router:(nullable ZIKViewRouter *)router willRemoveRouteOnDestination:(id)destination fromSource:(id)source { } (void)router:(nullable ZIKViewRouter *)router didRemoveRouteOnDestination:(id)destination fromSource:(id)source { } 你可以在这些方法中检查界面是否配置正确。也可以用于AOP记录。 例如你可以为UIViewController这个所有view controller的父类注册一个router这样就可以监控所有的UIViewController子类的路由事件。 路由错误检查 ZIKRouter会在启动时进行所有router的注册这样就能检测出router是否有冲突、protocol是否和router正确匹配保证所有router都能正确工作。当检测到错误时断言将会失败。 ZIKViewRouter在执行界面路由时会检测并报告路由时的错误。例如 使用了错误的protocol执行路由执行路由时configuration配置错误不支持的路由方式router可以限制界面只能使用push、present等有限的跳转方式在其他界面的跳转过程中执行了另一个界面的跳转unbalanced transition错误会导致-viewWillAppear:、-viewDidAppear:、-viewWillDisAppear:、-viewDidDisappear:等事件的顺序发生错乱Source view controller此时的状态无法执行当前路由路由时container view controller配置错误segue在代理方法中被取消导致路由未执行重复执行路由基本上包含了界面跳转时会发生的大部分错误事件。 支持任意模块 ZIKRouter包含ZIKViewRouter和ZIKServiceRouter。ZIKViewRouter专门用于界面跳转ZIKServiceRouter则可以添加任意类进行实例获取。 你可以用ZIKServiceRouter管理需要的类并且ZIKServiceRouter增添了和ZIKViewRouter相同的动态性和泛型支持。 性能 为了错误检查、支持storyboard和注册ZIKViewRouter和ZIKServiceRouter会在app启动时遍历所有类进行hook和注册的工作。注册时只是把view class、protocol和router class的地址加入字典不会对内存有影响。 在release模式下iPhone6s机型上测试了5000个UIViewController以及5000个对应的router遍历所有类并且hook的耗时大约为15ms注册router的耗时大约为50ms。基本上不会遇到性能问题。 如果你不需要支持storyboard可以去掉view class和router class配对的注册去掉以后就无法自动为storyboard里的view controller创建router。至于protocol和router的注册目前似乎是无法避免的。 项目地址和Demo 简单来说ZIKRouter就是一个用于模块间路由基于接口进行模块发现和依赖注入的Router。它以原生的语法执行路由在OC和Swift中都能使用。 项目地址在ZIKRouter。里面包含了一个demo用于演示iOS中大部分的界面路由场景建议在横屏iPad上运行。 最后记得点个star~ Demo截图控制台的输出就是界面路由时的AOP回调  转载于:https://www.cnblogs.com/soulDn/p/10672625.html
http://www.pierceye.com/news/982951/

相关文章:

  • 网页设计制作音乐排行榜一键seo提交收录
  • 网站推广要我营业执照复印件conoha wordpress
  • 免费行情软件app网站排行高质量外链网站
  • 免费解析网站制作网站开发项目实战视频
  • 柳州网站建设工作室基金会网站开发方案
  • 龙海网站建设微网站如何建设
  • 手机视频网站怎么做贵阳专业做网站
  • 网站建设题库vps上的网站运行太慢
  • 化妆品网站优化沧州网站制作公司
  • 专业优定软件网站建设上海seo服务
  • 网站充值怎么做的c2c平台的产品类型
  • 阿里去要企业网站建设方案书手机设计房子的软件3d下载
  • 凡科网站登录入轻博客网站开发
  • wordpress微信机器人订阅号性价比高seo网站优化
  • 网站建设全网推广亚马逊seo搜索什么意思
  • 做网站_你的出路在哪里android app for wordpress
  • 代刷网网站建设成都建立网站
  • 建设网站的费用预算商城网站制作
  • 北京网络法庭2018年企业网站优化如何做
  • asp.net做网站的步骤网站维护的作用
  • 网站制作前期所需要准备wordpress邮箱配置文件
  • 网站建设网站排名怎么做赣州专业做网站
  • 吉林电商网站建设价格做网站需要每年都缴费吗
  • 怎样用dede搭建网站域名网址
  • 做网站编辑有前途怎么样才算是一个网站页面
  • 建设鲜花网站前的市场分析网店设计理念
  • 网站建设优化服务公司wordpress非代码方式添加备案号
  • asp网站安装到空间教育网站平面设计
  • 快速设计一个网站网站h标签
  • 怎么做百度联盟网站前端面试题