组件化方案 App路由能解决哪些问题? 1.3D-Touch功能或者点击推送消息,要求外部跳转到App内 部一个很深层次的一个界面。 比如微信的3D-Touch可以直接跳转到“我的二维码”。“我的二 维码”界面在我的里面的第三级界面。或者再极端一点,产品需 求给了更加变态的需求,要求跳转到App内部第十层的界面, 怎么处理? 2.自家的一系列App之间如何相互跳转? 如果自己App有几个,相互之间还想相互跳转,怎么处理? 3.如何解除App组件之间和App页面之间的耦合性? 随着项目越来越复杂,各个组件,各个页面之间的跳转逻辑关 联性越来越多,如何能优雅的解除各个组件和页面之间的耦合 性? 4.如何能统一iOS和Android两端的页面跳转逻辑?甚至如何能 统一三端的请求资源的方式? 项目里面某些模块会混合ReactNative,Weex,H5界面,这些 界面还会调用Native的界面,以及Native的组件。那么,如何 能统一Web端和Native端请求资源的方式? 5.如果使用了动态下发配置文件来配置App的跳转逻辑,那么 如果做到iOS和Android两边只要共用一套配置文件? 6.如果App出现bug了,如何不用JSPatch,就能做到简单的热 修复功能? 比如App上线突然遇到了紧急bug,能否把页面动态降级成 H5,ReactNative,Weex?或者是直接换成一个本地的错误界 面? 7.如何在每个组件间调用和页面跳转时都进行埋点统计?每个 跳转的地方都手写代码埋点?利用Runtime AOP ? 8.如何在每个组件间调用的过程中,加入调用的逻辑检查,令 牌机制,配合灰度进行风控逻辑? 9.如何在App任何界面都可以调用同一个界面或者同一个组 件?只能在AppDelegate里面注册单例来实现? 比如App出现问题了,用户可能在任何界面,如何随时随地的 让用户强制登出?或者强制都跳转到同一个本地的error界面? 或者跳转到相应的H5,ReactNative,Weex界面?如何让用户 在任何界面,随时随地的弹出一个View ? 什么是路由呢? 1. URL Scheme方式 iOS系统是默认支持URL Scheme的 在App的info.plist里面添加URL types - URL Schemes // 打开邮箱 mailto:// // 给110拨打电话 tel://110 // 手机QQ mqq:// // 微信 weixin:// // 新浪微博 sinaweibo:// 微信内限制 scheme 访问,同时也限制外链网站直接访问,需要跳转到系统浏览 2. Universal Links方式 iOS 9.0新增加了一项功能,也是跳转功能 设置需要3步: 1. 需要Xcode 开启Associated Domains服务,并设置Domains,注意必须要 applinks:开头。 2.域名必须要支持HTTPS 3.上传内容是Json格式的文件,文件名为apple-app-site-association到自己域 名的根目录下,或者.well-known目录下 问题:文件间相互依赖,导致维护能力变差 根据前面跳转app系统功能,我们参考设计路由跳转方式来解耦合,通过URI JLRoutes github Star 3189 1.注册一个Router [[JLRoutes globalRoutes] addRoute:@"/:object/:primaryKey" handler:^BOOL(NSDictionary *parameters) { NSString *object = parameters[@"object"]; NSString *primaryKey = parameters[@"primaryKey"]; // 。。。 stuff return YES; }]; 注册后会按优先级高低加入对应数组中,后续跳转时取 2. 让Router 跳转url NSURL *editPost = [NSURL URLWithString:@"ele://post/halfrost? debug=true&foo=bar"]; [[UIApplication sharedApplication] openURL:editPost]; 去对应列表中匹配是否存在 JLRoutes 还维护一个全局表,如果在当前规则没有找到到话,就降级去全局表寻 找: didRoute = [[JLRoutes globalRoutes] _routeURL:URL withParameters:parameters executeRouteBlock:executeRouteBlock]; 3. 获得待处理数据 路由解析成功后,会返回对应信息: { "object": "post", "action": "halfrost", "debug": "true", "foo": "bar", "JLRouteURL": "ele://post/halfrost?debug=true&foo=bar", "JLRoutePattern": "/:object/:action", "JLRouteScheme": "JLRoutesGlobalRoutesScheme" } 另外,还支持可选参数解析:/the(/foo/:a)(/bar/:b) JLRoutes 会帮我们默认注册如下4条路由规则: /the/foo/:a/bar/:b /the/foo/:a /the/bar/:b /the routable-ios 1.注册 UINavigationController *nav = [[UINavigationController alloc] initWithRootViewController:vc]; [[Routable sharedRouter] map:@"user/:id" toController:[ViewController class]]; [[Routable sharedRouter] map:@"sec/:str" toController: [SecViewController class]]; [[Routable sharedRouter] setNavigationController:nav]; 其实通过map值经过UPRouterOptions类的转换,把他当做key映射到字典 里,value是控制器,管理这哈希表 2.调用 [[Routable sharedRouter] open:@"sec/12" animated:YES extraParams:@{@"temp":@"1"}]; 3.处理参数 //在SecViewController 里实现一下router初始化方法,接一下传过来的字典, 就ok了 - (id)initWithRouterParams:(NSDictionary *)params { if ((self = [self initWithNibName:nil bundle:nil])) { } return self; } 支持类方法和对象方法 HHRouter 1. 注册和调用 -(void)map:(NSString*)route toControllerClass:(Class)controllerClass; -(UIViewController*)matchController:(NSString*)route; 需要主持block时: -(void)map:(NSString*)route toBlock:(HHRouterBlock)block; -(HHRouterBlock)matchBlock:(NSString*)route; // 返回block,手动调用 -(id)callBlock:(NSString*)route; // 匹配结果有block 立即调用 matchController 实现 - (UIViewController*)matchController:(NSString*)route { NSDictionary*params =[selfparamsInRoute:route]; Class controllerClass = params[@"controller_class"]; UIViewController*viewController =[[controllerClass alloc]init]; if([viewController respondsToSelector:@selector(setParams:)]) { [viewController performSelector:@selector(setParams:)withObject:[params copy]]; } return viewController; } 2. VC实现 实现 setParams: 方法。 获取路由传过来的参数,设置控制器对应的数据 MGJRouter 1. 注册 [MGJRouter registerURLPattern:@"ele://name/:name" toHandler:^(NSDictionary *routerParameters) { void (^completion)(NSString *) = routerParameters[MGJRouterParameterCompletion]; if (completion) { completion(@"完成了"); } }]; 2.调用 [MGJRouter openURL:@"mgj://foo/bar"]; 反向传值: [MGJRouter openURL:@"ele://name/halfrost/?age=20" withUserInfo:@{@"user_id": @1900} completion:^(id result) { NSLog(@"result = %@",result); }]; 3.匹配结果 { MGJRouterParameterCompletion = "<__NSGlobalBlock__: 0x107ffe680>"; MGJRouterParameterURL = "ele://name/halfrost/?age=20"; MGJRouterParameterUserInfo = { "user_id" = 1900; }; age = 20; block = "<__NSMallocBlock__: 0x608000252120>"; name = halfrost; } 注意: hard code 的url 通过宏集中管理 组件间解耦 蘑菇街为了区分开页面间调用和组件间调用,于是想出了一种新的方法。用 Protocol的方法来进行组件间的调用。 通过 ModuleProtocolManager 单例 管理文件,管理协议的注册和调用 1 注册&实现 @interface DetailModuleEntry()<DetailModuleEntryProtocol> @end @implementation DetailModuleEntry + (void)load { [ModuleProtocolManager registServiceProvide:[[self alloc] init] forProtocol:@protocol(DetailModuleEntryProtocol)]; } - (UIViewController *)detailViewControllerWithId:(NSString*)Id Name: (NSString *)name { DetailViewController *detailVC = [[DetailViewController alloc] initWithId:id Name:name]; return detailVC; } @end 2.调用 id< DetailModuleEntryProtocol > DetailModuleEntry = [ModuleProtocolManager serviceProvideForProtocol:@protocol(DetailModuleEntryProtocol)]; UIViewController *detailVC = [DetailModuleEntry detailViewControllerWithId:@“详情界面” Name:@“我的购物车”]; [self.navigationController pushViewController:detailVC animated:YES]; 58 & ajk Router 目前看58 实现和蘑菇街大致一样,只是有些细节区别 普通页面跳转url 硬编码控制器对应写在info文件中 组件之间也是通过协议调用,叫IOC 1.注册 硬编码,每个业务模块在创建自己的router.info 文件,每个控制器对应一个定义 好的key ,通过 key:class 配置plist 文件 2.调用 app交互过程返回的数据,会下发对应的 actionUrl 地址 调用时,就是通过router 调用这个下发的 actionUrl , Router 内部解析url, 然后根据info文件获取对应的class。 调用对应class 实现的 initWithRouterParams: exterParam:将对应的数据传 3. vc 实现 + initWithRouterParams: exterParam: 方法,创建对象,并设置数 据,返回 一个 vc 实例对象 58 IOC: 业务模块解耦合 组件之间的调用采用协议方法,也是需要注册 公共组件 CommonBussiness 新房业务组件 AFModule 二手房 ESFModule 等等 CommonBussiness 新房protocol 文件,声明协议 二手房protocol 文件,声明协议 等等 RouterManager 单例 实现注册对应的协议方法 检查: debug下对必须实现的协议方法进行判断,如果还没有注册,或者对应的方法没 有实现,就asster 报错 如何判断协议方法类型? TODO 各业务 1. 实现自己业务对外协议方法 2. 向公共组件注册协议 调用 通过公共组件的协议调用 CTMediator 前面我们介绍不管是router 还是 组件之间协议manager ,都是作为一个中介者 维护者和其它page的关系。 CTMediator 整个框架只有一个文件: .h定义: @interface CTMediator : NSObject + (instancetype)sharedInstance; // 远程App调用入口 - (id)performActionWithUrl:(NSURL *)url completion:(void(^)(NSDictionary *info))completion; // 本地组件调用入口 - (id)performTarget:(NSString *)targetName action:(NSString *)actionName params:(NSDictionary *)params shouldCacheTarget: (BOOL)shouldCacheTarget; - (void)releaseCachedTargetWithTargetName:(NSString *)targetName; @end .m 核心细节: 部分1 - (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]; } ... 发现和其他Router不同的是,这里是通过在传入类名前 拼接 一个字符串 "Target_%@" ,然后通过字符串转class来调用通用方法。 而被调用的方法需要满足 "Action_%@:" 格式 也就是说,CTMediator 并没有细节实现,只是要求使用这个功能的模块 创建一 个 Target_classX 形式文件,自己实现,我只负责调用。 实现: 假设有控制器 WGPersonInfoViewController 接着需要在业务代码再创建一个文件: Target_WGPersonInfoViewController 接着,在Target_WGPersonInfoViewController 文件里实现的方法 需要是 "Action_%@:" 模式 结果像这样: @interface Target_WGPersonInfoViewController : NSObject - (UIViewController *)Action_PersonInfoViewController:(NSDictionary *)param; @end 每错,每个文件都要有对应的 Target开头文件。 调用前准备: 还不能之间使用 CTMediator 调用,CTMediator 设计的一个核心点就是 摆脱 中间件的臃肿,避免成为垃圾桶。所以,每个业务模块根据自己业务创建 不同的 CTMediator 分类文件,自定义的调用方法都在这个分类里,自己创建,自己维 护。 创建 CTMediator+TAPersonInfo 如下: #import <CTMediator/CTMediator.h> @interface CTMediator (TAPersonInfo) - (UIViewController *)personInfoWithName:(NSString *)name age: (NSInteger)age; @end .m 文件: #import "CTMediator+TAPersonInfo.h" @implementation CTMediator (TAPersonInfo) - (UIViewController *)personInfoWithName:(NSString *)name age: (NSInteger)age{ NSMutableDictionary *dic = [[NSMutableDictionary alloc] init]; [dic setValue:name forKey:@"name"]; [dic setValue:@(age) forKey:@"age"]; return [self performTarget:@"WGPersonInfoViewController" action:@"PersonInfoViewController" params:dic shouldCacheTarget:NO]; } @end 调用: 先导入 #import "CTMediator+TAPersonInfo.h" 然后 调用 其方法即可获得控制器对象: UIViewController *personVC = [[CTMediator sharedInstance] personInfoWithName:@"小王" age:10]; 小结: 每个项目都有它适合的方式,并非有一个方法能适用所有公司的场景,就像设计 模式一样,看情况而定。 一开始就将项目拆分过细也不是好事,增加工作量,效率低下。我们可以在模块 变得需要拆分时,再进行优化。