一种 App 内路由系统的设计

1,730 阅读4分钟

本文仅探讨怎样才是路由系统该有的设计,并不涉及具体实现

App 发展到一定程度时,页面越来越多,工程越来越大,合作开发的人也越来越多,这时就可能需要引入路由系统(当然,从项目一开始启动就接入路由是最好不过了)。路由系统提供了一种简单的方式,让用户在不同页面间浏览时就像在浏览器中访问网页一样,一个地址对应一个完整内容的页面(一般使用 RESTful 的风格),如:

  • Foo://users/nickname,打开用户页面
  • Foo://products/xxxx,打开商品页面
  • Foo://settings,打开设置页面

路由系统使得开发者 A 在开发自己的页面时,不需要知道开发者 B 开的页面叫什么,甚至也不用引用 B 的头文件,只需知道 B 提供的 URL 格式就可跳转到 B 开发的页面。这在多人合作开发时相当有用。

目前开源社区有不少第三方实现的路由系统,如:

以上路由系统的实现各有特色与缺点,以下一起分析一下,怎样是一个好的路由设计。

路由注册

路由的注册需要支持 RESTFul 参数,一般来说,第一个参数是 pattern 第二个参数是一个路由对象,如:

@protocol RTRoutable 
@optional
+ (BOOL)routerDidRoutePattern:(NSString *)pattern
               withParameters:(id)parameters
                     complete:(RTRouteCompleteBlock)complete;
@end


+ (void)registerURLPattern:(NSString *)pattern withRoutable:(Class)routable;
+ (void)registerURLPattern:(NSString *)pattern forScheme:(NSString *)scheme withRoutable:(Class)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;

这里有几个注意点:

  1. UI 解耦合。注册时使用一个 RTRoutable 协议,而不是一个 UIViewController。虽然来说,一个 App 内大部分情况下路由就是 Navigation 的 push 与 pop 操作,但不能限制只能这么做。例如,用路由来实现事件打点,或是只是一个简单的弹窗通知。在这一点上 ABRouterroutable-iosHHRouterUIViewController 耦合太强,使用会受限,当然使用简单也是其优点,具体需要按真实的使用场景权衡;而 JLRoutesDeepLinkKitMGJRouter 会比较灵活;
  2. 路由是一个过程。路由的匹配是可以同步完成,但是路由的动作通常是包含动画的,如常用的 push pop 与 present 操作,在这一个过程中路由系统如果又收到新的路由请求,可能导致界面错乱,所以路由动作完成后,需要有一个机制通知到路由系统,当前路由动作已经完成,可以接收下个路由请求。因此 RTRoutableRTRouteCompleteBlock 上都设计了 complete 参数。这一方面,所有已有第三方实现都有所欠缺。
    一个常见的错误是,用户因某种原因被登出且又进行了一些操作后,多个地方发起了登录路由,于是 present 了两个或更多的登录界面。
  3. 路由动作可能受条件影响。在 App 运行过程中,并不是所有匹配到的路由都可以正常执行,有些路由可能需要用户登录后才能执行,因此路由执行是否成功,需要返回一个布尔值反馈路由系统。这一点上 JLRoutes 胜出
  4. 支持多个 Scheme。App 内的 Web View Controller 一般是要能打开 HTTP 链接的,而不只是 App 自己的 Foo。同样 JLRoutes 胜出。

路由代理

路由系统的匹配过程最好能受外部控制,及处理路由开始或结束后的事情,因此需要设计一个路由的代理:

@protocol RTRouterDelegate 
@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)delegate;
+ (id)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) 
@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 上找到