一种 App 内路由系统的设计

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

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

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

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

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

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

路由注册

路由的注册需要支持 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;

这里有几个注意点:

  1. UI 解耦合。注册时使用一个 RTRoutable 协议,而不是一个 UIViewController。虽然来说,一个 App 内大部分情况下路由就是 Navigation 的 push 与 pop 操作,但不能限制只能这么做。例如,用路由来实现事件打点,或是只是一个简单的弹窗通知。在这一点上 ABRouterroutable-iosHHRouterUIViewController 耦合太强,使用会受限,当然使用简单也是其优点,具体需要按真实的使用场景权衡;而 JLRoutesDeepLinkKitMGJRouter 会比较灵活;
  2. 路由是一个过程。路由的匹配是可以同步完成,但是路由的动作通常是包含动画的,如常用的 push pop 与 present 操作,在这一个过程中路由系统如果又收到新的路由请求,可能导致界面错乱,所以路由动作完成后,需要有一个机制通知到路由系统,当前路由动作已经完成,可以接收下个路由请求。因此 RTRoutableRTRouteCompleteBlock 上都设计了 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 上找到