iOS组件化之路由设计(Router)
前言:
随着用户的需求越来越多,对App的用户体验也变的要求越来越高。为了更好的应对各种需求:
①App架构:开发人员从软件工程的角度,将App架构由原来简单的MVC变成MVVM,VIPER等复杂架构。更换适合业务的架构,是为了后期能更好的维护项目。
②版本快速迭代:但是用户依旧不满意,继续对开发人员提出了更多更高的要求,不仅需要高质量的用户体验,还要求快速迭代,最好一天出一个新功能,而且用户还要求不更新就能体验到新功能。为了满足用户需求,于是开发人员就用H5,ReactNative,Weex等技术对已有的项目进行改造。
③组件化:项目架构也变得更加的复杂,纵向的会进行分层,网络层,UI层,数据持久层。每一层横向的也会根据业务进行组件化。
尽管这样做了以后会让开发更加有效率,更加好维护,但是如何解耦各层,解耦各个界面和各个组件,降低各个组件之间的耦合度,如何能让整个系统不管多么复杂的情况下都能保持“高内聚,低耦合”的特点?
1.引子
大前端React和Vue.路由的作用主要是保证视图和URL的同步。当用户在页面进行操作的时候,应用会在若干个交互状态中切换,路由则可以记录下某些重要的状态,比如用户查看一个网站,用户是否登录、在访问网站的哪一个页面。用户可以通过手动输入或者与页面进行交互改变URL,然后通过同步或者异步的方式向服务端发送请求获取资源,成功后重新绘制UI。
2.App路由能解决那些问题:
1->点击推送消息,要求外部跳转到App内部一个很深层次的一个界面。比如微信的3D-Touch可以直接跳转到“我的二维码”。“我的二维码”界面在我的里面的第三级界面。或者再极端一点,产品需求给了更加变态的需求,要求跳转到App内部第十层的界面,怎么处理?
2->如何解除App组件之间和App页面之间的耦合性?
3->如何能统一iOS和Android两端的页面跳转逻辑?甚至如何能统一三端的请求资源的方式?
4->如果App出现bug了,如何不用JSPatch,就能做到简单的热修复功能?
5->如何在每个组件间调用和页面跳转时都进行埋点统计?每个跳转的地方都手写代码埋点?利用Runtime AOP ?
6->如何在App任何界面都可以调用同一个界面或者同一个组件?只能在AppDelegate里面注册单例来实现?
7->比如App出现问题了,用户可能在任何界面,如何随时随地的让用户强制登出?或者强制都跳转到同一个本地的error界面?或者跳转到相应的H5,ReactNative,Weex界面?如何让用户在任何界面,随时随地的弹出一个View ?
3.APP跳转实现
1->URL Scheme方式
比如说,在iPhone的Safari浏览器上面输入如下的命令,会自动打开一些App:
//打开邮箱: mailto://
关于系统功能跳转的URL汇总列表:https://www.jianshu.com/p/32ca4bcda3d1
2.Universal Links 方式
iOS 9.0新增加了一项功能是Universal Links,使用这个功能可以使我们的App通过HTTP链接来启动App。
1.如果安装过App,不管在微信里面http链接还是在Safari浏览器,还是其他第三方浏览器,都可以打开App。
2.如果没有安装过App,就会打开网页。
具体设置:
1.App需要开启Associated Domains服务,并设置Domains,注意必须要applinks:开头。
2.域名必须要支持HTTPS。
上传内容是Json格式的文件,文件名为apple-app-site-association到自己域名的根目录下,或者.well-known目录下。iOS自动会去读取这个文件。具体的文件内容请查看官方文档。
如果App支持了Universal Links方式,那么可以在其他App里面直接跳转到我们自己的App里面。如下图,点击链接,由于该链接会Matcher到我们设置的链接,所以菜单里面会显示用我们的App打开。
在浏览器里面也是一样的效果,如果是支持了Universal Links方式,访问相应的URL,会有不同的效果。如下图:
4.App内组件路由设计
主要解决:
①各个页面和组件之间的跳转问题:
②各个组件之间相互调用
代码高复用、方便测试
如何设计一个路由
Route实现
主工程与首页模块、分类、登录模块不直接建立关联,而是先通过router(路由)与你要调用的模块建立关系 ,从而实现各个模块的解耦和复用、
①.OCTarget_index类(解耦+交互)
#import <Foundation/Foundation.h> NS_ASSUME_NONNULL_BEGIN @interface OCTarget_Index : NSObject - (id)action_home:(NSDictionary*)params; @end NS_ASSUME_NONNULL_END #import "OCTarget_Index.h" #import <UIKit/UIKit.h> @implementation OCTarget_Index - (id)action_home:(NSDictionary*)params { UIViewController *homeVC = [UIViewController new]; homeVC.title = @"首页"; return homeVC; } @end
2.代码实现:
HKOCRouter.h
#import <UIKit/UIKit.h> NS_ASSUME_NONNULL_BEGIN @interface HKOCRouter : UIView +(instancetype)shareInstance; - (id)openUrl:(NSString *)urlStr; //返回值id,外部调用,通过target 和 action 来唯一确认一个类里面的方法 - (id)performTarget:(NSString*)targetName action:(NSString*)actionName param:(NSDictionary*)params; @end NS_ASSUME_NONNULL_END
HKOCRouter.m
#import "HKOCRouter.h" @implementation HKOCRouter +(instancetype)shareInstance { static HKOCRouter * mediator; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ mediator = [[HKOCRouter alloc] init]; }); return mediator; } - (id)openUrl:(NSString *)urlStr { NSURL *url = [NSURL URLWithString:urlStr]; NSMutableDictionary *params = [[NSMutableDictionary alloc] init]; //查询 NSString *urlString = [url query]; //切割字符串 for (NSString *param in [urlString componentsSeparatedByString:@"&"]) { NSArray *elts = [param componentsSeparatedByString:@"="]; if (elts.count<2) continue; id firstEle = [elts firstObject]; id lastEle = [elts lastObject]; if (firstEle && lastEle) { [params setObject:lastEle forKey:firstEle]; } } NSString *actionName = [url.path stringByReplacingOccurrencesOfString:@"/" withString:@""]; if ([actionName hasPrefix:@"native"]) { return @(NO); } id result = [self performTarget:url.host action:actionName param:params]; return result; } -(id)performTarget:(NSString *)targetName action:(NSString *)actionName param:(NSDictionary *)params { //这个目标的类名字符串 NSString * targetClassString = [NSString stringWithFormat:@"OCTarget_%@",targetName]; NSString *actionMethodString = [NSString stringWithFormat:@"action_%@",actionName]; Class targetClass = NSClassFromString(targetClassString); NSObject *target = [[targetClass alloc] init]; SEL action = NSSelectorFromString(actionMethodString); //判断 if ([target respondsToSelector:action]) { return [self safePerformAction:action target:target param:params]; }else { SEL action = NSSelectorFromString(@"notFound:"); if ([target respondsToSelector:action]) { return [self safePerformAction:action target:target param:params]; }else { return nil; } } } //1.通过对象调用指定的方法 //2.传参 - (id)safePerformAction:(SEL)action target:(NSObject*)target param:(NSDictionary*)params { NSMethodSignature *methodSig = [target methodSignatureForSelector:action]; if (methodSig == nil) { return nil; } //获取这个方法返回值的地址 const char *retType = [methodSig methodReturnType]; //id 是可以返回任意对象,所以我们单独处理基本变量。 NSInteger Bool Void if (strcmp(retType, @encode(NSInteger)) == 0) { NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig]; //为什么传2?前面0个1这两个位置已经被target和action给占了 [invocation setArgument:¶ms atIndex:2]; [invocation setTarget:target]; [invocation setSelector:action]; [invocation invoke]; NSInteger result = 0; [invocation getReturnValue:&result]; return @(result); } if (strcmp(retType, @encode(BOOL)) == 0) { NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig]; //为什么传2?前面0个1这两个位置已经被target和action给占了 [invocation setArgument:¶ms atIndex:2]; [invocation setTarget:target]; [invocation setSelector:action]; [invocation invoke]; NSInteger result = 0; [invocation getReturnValue:&result]; return @(result); } #pragma clang diagnostic push #pragma clang diagnostic ignored "-Warc-performSelector-leaks" return [target performSelector:action withObject:target withObject:params]; #pragma clang diagnostic pop } @end
使用:
UIViewController * vc = [[HKOCRouter shareInstance] openUrl:@"http://Index/home:"];
Index为组件索引;home为actionName;“:”,冒号表示带参数.