组件化方案
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];
小结:
每个项目都有它适合的方式,并非有一个方法能适用所有公司的场景,就像设计
模式一样,看情况而定。
一开始就将项目拆分过细也不是好事,增加工作量,效率低下。我们可以在模块
变得需要拆分时,再进行优化。