一种 App 内路由系统的设计
本文仅探讨怎样才是路由系统该有的设计,并不涉及具体实现
App 发展到一定程度时,页面越来越多,工程越来越大,合作开发的人也越来越多,这时就可能需要引入路由系统(当然,从项目一开始启动就接入路由是最好不过了)。路由系统提供了一种简单的方式,让用户在不同页面间浏览时就像在浏览器中访问网页一样,一个地址对应一个完整内容的页面(一般使用 RESTful 的风格),如:
Foo://users/nickname
,打开用户页面Foo://products/xxxx
,打开商品页面Foo://settings
,打开设置页面
路由系统使得开发者 A 在开发自己的页面时,不需要知道开发者 B 开的页面叫什么,甚至也不用引用 B 的头文件,只需知道 B 提供的 URL 格式就可跳转到 B 开发的页面。这在多人合作开发时相当有用。
目前开源社区有不少第三方实现的路由系统,如:
- https://github.com/joeldev/JLRoutes
- https://github.com/Huohua/HHRouter
- https://github.com/aaronbrethorst/ABRouter
- https://github.com/button/DeepLinkKit
- https://github.com/clayallsopp/routable-ios
- https://github.com/mogujie/MGJRouter
以上路由系统的实现各有特色与缺点,以下一起分析一下,怎样是一个好的路由设计。
路由注册
路由的注册需要支持 RESTFul 参数,一般来说,第一个参数是 pattern 第二个参数是一个路由对象,如:
@protocol RTRoutable <NSObject>
@optional
+ (BOOL)routerDidRoutePattern:(NSString *)pattern
withParameters:(id)parameters
complete:(RTRouteCompleteBlock)complete;
@end
+ (void)registerURLPattern:(NSString *)pattern withRoutable:(Class<RTRoutable>)routable;
+ (void)registerURLPattern:(NSString *)pattern forScheme:(NSString *)scheme withRoutable:(Class<RTRoutable>)routable;
当然最好支持 Block 的方式:
typedef void(^RTRouteCompleteBlock)(void);
+ (void)registerURLPattern:(NSString *)pattern withBlock:(BOOL(^)(id parameters, RTRouteCompleteBlock complete))handler;
+ (void)registerURLPattern:(NSString *)pattern forScheme:(NSString *)scheme withBlock:(BOOL(^)(id parameters, RTRouteCompleteBlock complete))handler;
这里有几个注意点:
- 与 UI 解耦合。注册时使用一个 RTRoutable 协议,而不是一个 UIViewController。虽然来说,一个 App 内大部分情况下路由就是 Navigation 的 push 与 pop 操作,但不能限制只能这么做。例如,用路由来实现事件打点,或是只是一个简单的弹窗通知。在这一点上 ABRouter、routable-ios 及 HHRouter 与 UIViewController 耦合太强,使用会受限,当然使用简单也是其优点,具体需要按真实的使用场景权衡;而 JLRoutes、DeepLinkKit、MGJRouter 会比较灵活;
- 路由是一个过程。路由的匹配是可以同步完成,但是路由的动作通常是包含动画的,如常用的 push pop 与 present 操作,在这一个过程中路由系统如果又收到新的路由请求,可能导致界面错乱,所以路由动作完成后,需要有一个机制通知到路由系统,当前路由动作已经完成,可以接收下个路由请求。因此 RTRoutable 及 RTRouteCompleteBlock 上都设计了 complete 参数。这一方面,所有已有第三方实现都有所欠缺。
一个常见的错误是,用户因某种原因被登出且又进行了一些操作后,多个地方发起了登录路由,于是 present 了两个或更多的登录界面。路由动作可能受条件影响。在 App 运行过程中,并不是所有匹配到的路由都可以正常执行,有些路由可能需要用户登录后才能执行,因此路由执行是否成功,需要返回一个布尔值反馈路由系统。这一点上 JLRoutes 胜出 支持多个 Scheme。App 内的 Web View Controller 一般是要能打开 HTTP 链接的,而不只是 App 自己的 Foo。同样 JLRoutes 胜出。 路由代理
路由系统的匹配过程最好能受外部控制,及处理路由开始或结束后的事情,因此需要设计一个路由的代理:
@protocol RTRouterDelegate <NSObject>
@optional
- (BOOL)router:(RTRouter *)router shouldRoutePattern:(NSString *)pattern withParameter:(id)parameter;
- (void)router:(RTRouter *)router willRoutePattern:(NSString *)pattern withParameter:(id)parameter;
- (void)router:(RTRouter *)router didRoutePattern:(NSString *)pattern withParameter:(id)parameter;
- (void)router:(RTRouter *)router didFailRoutePattern:(NSString *)pattern withParameter:(id)parameter;
@end
+ (void)setDelegate:(id<RTRouterDelegate>)delegate;
+ (id<RTRouterDelegate>)delegate;
这一点上,只有 MGJRouter 有实现(内部版本)
路由匹配
路由匹配在 Google 的 AngularJS 中,以及 NodeJS 的框架 ExpressJS 都有良好的实现,即匹配的优先级与注册的顺序无关,而与匹配程度有关。如,先注册了
Foo://user/:name/:bar
后注册了
Foo://user/:name/info
在打开 Foo://user/jack/info
时,应当先匹配后者。
实例
@interface LoginController : UIViewController
@end
@interface LoginController (Routable) <RTRoutable>
@end
@implementation LoginController (Routable)
+ (BOOL)routerDidRoutePattern:(NSString *)pattern withParameters:(id)parameters complete:(RTRouteCompleteBlock)complete
{
LoginController *loginVC = [[self alloc] init];
[[UIApplication sharedApplication].delegate.window.rootViewController presentViewController:loginVC
animated:YES
completion:complete];
return YES;
}
@end
@interface ProductController : UIViewController
@end
@implementation ProductController
+ (void)load
{
if (self == [ProductController class]) {
[RTRouter registerURLPattern:@"/products/:id" withBlock:^BOOL(id parameters, RTRouteCompleteBlock complete) {
if ([User currentUser].isLogin) {
ProductController *productController = [[self alloc] init];
UINavigationController *nav = (UINavigationController *)[UIApplication sharedApplication].delegate.window.rootViewController;
[nav pushViewController:productController
animated:YES];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(UINavigationControllerHideShowBarDuration * NSEC_PER_SEC)), dispatch_get_main_queue(), complete);
return YES;
}
return [RTRouter openURL:[NSURL URLWithString:@"Foo://login"]
parameter:nil
complete:complete];
}];
}
}
@end
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
[RTRouter registerURLPattern:@"/login" withRoutable:[LoginController class]];
[RTRouter registerURLPattern:@"/notice?message=:message"
withBlock:^BOOL(id parameters, RTRouteCompleteBlock complete) {
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Message"
message:parameters[@"message"]
delegate:nil
cancelButtonTitle:@"OK"
otherButtonTitles:nil];
[alert show];
dispatch_async(dispatch_get_main_queue(), complete);
return YES;
}];
return YES;
}
结语
本文涉及了多种使用场景,但是在真实的需求中,可能有些设计比较冗余或者缺失,仅作参考,具体实现还得依实际情况调整。本文的完整设计可以 在 Gist 上找到