天天看點

# iOS 元件化-路由解耦思想 JLRoutes 實戰篇(一)App内控制器跳轉

# iOS 元件化-路由解耦思想 JLRoutes 實戰篇(一)App内控制器跳轉

前言

元件化, Router 這些概念可能在幾年前還是比較新穎的概念, 至今相信絕大多數同學都對這些名詞已耳熟能詳, 筆者在真正接觸到 Router 并在項目使用之前, 也有讀過一些 元件化, Router 進行解耦的思想和架構的文章, 但是由于自己修行不夠, 加上沒能真正将其運用到項目進行實踐。 導緻每次讀完文章之後, 所了解的知識沒能真正轉化為可以解決問題的技能, 筆者有幸在項目中接觸并運用著名開源庫

JLRouter

來解決 App 内外所有頁面之間的跳轉邏輯, 經過這幾年的學習和使用, 将其記錄一邊鞏固知識, 寫出來跟大家一起學習, 加上看到網上分享關于使用元件化-Router 相關文章偏于理論, 很少有完整詳細Demo, 具體在項目中使用還需進一步深入研究, 是以有了此篇文章, 有什麼不對或需要補充的, 望大家多多指教。

此篇文章偏向實戰, 想深入學習 Router 思想的推薦霜神寫的

iOS 元件化 —— 路由設計思路分析

==Demo 在文章最👇==

為什麼 Router

路由基礎三問, 每次接觸新穎思想架構時, 我都會不禁的問自己這幾個問題, 希望通過下面幾個簡要的概括, 能很好的幫助大家了解 Router;

  • 路由是什麼,解決了什麼問題
# iOS 元件化-路由解耦思想 JLRoutes 實戰篇(一)App内控制器跳轉

上面一幅圖很形象的展示了項目中各個控制器子產品之間錯綜複雜的關系, 當我們在處理不當的情況下可能更加糟糕.

使用 Router 之後大概是這樣的;

# iOS 元件化-路由解耦思想 JLRoutes 實戰篇(一)App内控制器跳轉

打個比方, Router 就是跟我們日常使用的路由器一樣, App 内每個控制器可以想象成已經連接配接了這台路由器的不同裝置, 當然連接配接路由器時, 一般需要輸入密碼, Router 同樣的, 使用前需要每台裝置進行一次注冊, Router 在内部儲存每台裝置的 URL, 不同裝置之間需要互動時, 将消息發送到路由器中統一處理;

當控制器之間需要互動跳轉時, 隻需要将對應的 URL 位址發送到 Router 裡, Router 根據其注冊的 URL 來尋址到對方資訊, 然後負責執行個體化對象, 并傳參, 進行跳轉等工作, 各個控制器之間不需要互相依賴對方, 完美解決不同子產品之間耦合!

  • 為什麼要用路由來實作 VC 跳轉

    Router 能做的事情很多, 首先我們用它來解決棘手的控制器耦合關系,是一種非常有效的解決方案;

在 App 中控制器跳轉普遍分為 3 種, 模态跳轉Modal(presented/dismiss), 導航控制器跳轉(Push/pop), Storyboard 跳轉(Segue), 還有 UITabBarVC 主要制器 Index 切換;

除了正常的控制器之間跳轉之外, 還會有 3D Touch 指定跳轉到某個控制器中;

App 之間跳轉: URL Scheme, Universal Links方式;

可想而知 App 内不管是頁面切換, 外部調用, 都會涉及到控制器的跳轉, 切換等等;

下面引用常見場景來舉個栗子:

Router 前 僞代碼:

假如在沒有引入 Router 之前, 實作 A Push B, B Modal C 的場景: 一般做法都是在 A 中引入B, B 中引入 C, 然後在每次跳轉前都需要來一段寫死,
//A Push B   A 頁面跳轉至 B頁面, 并且設定相應 @perpeoty, callback 等;
#import "B"
B* BVC = [B new]; 
BVC.delegate = A;
BVC.name = @"jersey";
BVC.callback = ^ (id data) {
};
... 
...
... 對 b 設定一些業務相關參數, delegate,  callback 等等;
[A.nav pushVC: BVC animation: true]; 
// B -> C 
#import "C"
C* CVC = [C new];
[B presentVC: CVC];
[B presentVC: CVC animation: true completion: nil];           

==Router 後 僞代碼:==

在引用了 Router 之後, 相同的場景下, 我們的代碼是這樣的; 在需要做跳轉的控制器引入我們封裝好的 ==JSDVCRouter(是針對 JLRouter 進行的一層封裝, 專門用于管理 App 跳轉的類, 在文章後面會詳細講解)== 即可.
// A Push B;
    #import "JSDVCRouter"
    [JSDVCRouter openURL: BVCPath info: @{@"delegate":self,@"name":@"jersey",@"callback":callback}];    
    // BVCPath: 表示我們對 B 控制器定義的路徑, 一般儲存在全局 Map 裡面, 每個 Path 映射目前控制器 Map 包含相關 title, class, needLog, 等參數;
    // B Modal C
    [JSDVCRouter openURL: C info: {kJSDRouteSegue: @"Modal"}]; // 控制器之間跳轉預設以 Push 實作, 當需要 Modal 時, 則傳遞一個參數;            

看到這裡相信認真閱讀的同學們已看出使用 Router 的好處:

1. 耦合度降低: A 控制器不需要知道 B 控制器的存在, 隻需要 import "JSDRouter", 由其去進行相應跳轉邏輯, 以及指派等等; 
2. 代碼閱讀性提高: 當然在剛剛接觸時, 看着會不大不習慣, 等接觸一段時間之後, 不僅減少了代碼行數, 同時可讀性還是很高的, 跟 push/pop, present/dismiss 說再見吧;
3. 提高代碼複用性: 每次控制器之間跳轉和指派等操作, 都需要重複性的 code 一次**(嚴重違背了: 可複用性原則)**, 通過 JSDRouter 将跳轉和指派等邏輯封裝起來, 一次 code, 終生受用;
4. 易于維護: 寫到這一點有點兒糾結, 當項目随着公司規模不斷壯大時, 控制器數量, 跳轉變得越加複雜, 跳轉方法和邏輯很容易變得越來越混亂, 後期管理起來比較困難。 使用 JSDVCRouter 單一職責的原則來專門負責 App 内所有的跳轉, 能非常有效的提高測試及後期維護, 當然成本是需要維護 RouterMap 同時完善 JSDVCRouter 内部邏輯; 
5. 動态化及靈活性: 使用 Router 時可以配合背景響應傳遞響應的 Key 來決定真正跳轉的頁面, 而不是寫死的方式來進行跳轉;
6. 待補充: 
           
  • 實作 Router 完成控制器跳轉, 至少需要幾個步驟?

    首次将控制器跳轉轉成 Router 方案

很簡單隻有 3個步驟, 如何需求變動不大的話, 幾乎一勞永逸;

  1. Map 表建立: 其是一個全局 Map, App 内相應的控制器定義好 Path, Router 可以根據 Path 映射相應控制器制定的 Map 内, Map 裡面最少包含目前控制器的參數如: {@"Class": @"控制器類名"}。相當于調用這個路由時,得到一組其綁定的 Map 作為參數, 通過 Class 來初始化執行個體;

    代碼結構如:

+ (NSDictionary *)configInfo
        return @{ JSDRouteHomeCenter: @{
                     @"class": @"JSDAHomeCenterVC",
                     @"name": @"首頁",
                     @"parameter": @"",
                     @"needLogin": @"0", },
                  JSDRouteUserLogin: @{
                     @"class": @"JSDUserLoginVC",
                     @"name": @"登陸",
                     @"parameter": @"",
                     @"needLogin": @"0", },
        };           
  1. 封裝 JLRouter; 為了友善使用,管理,以及後期遷移等!類似使用 AFNetwork, SDWebImage, MJRefresh 等有名的開源庫一樣, 由于開源庫提供功能非常豐富, 但是可能我們實際使用到的隻是它一兩個主要的功能來解決項目中存在的問題, 大家都會根據公司具體的業務場景或者使用習慣, 來對其進行一層甚至多層封裝一樣, 使其能更加适合實際要求;

    筆者對其進行了一層封裝 + Category 的形式: JSDVCRouter,JSDVCRouter + Add;

JSDVCRouter: 主要用于聲明 Router 調用接口;

JSDVCRouter + Handle: 主要用于實作 Router 注冊, 處理控制器之間跳轉和參數指派代碼;

  1. 根據約定 Path 進行跳轉: 上面 1 2 都準備好之後, 即可輕松的進行控制器跳轉 [JSDVCRouter openURL:BVC];

業務變更後期維護

  1. Map 維護: 随着業務發展, 當有新的頁面加入時, 對 Map 添加一個指定的 Path 和綁定的相應參數;
  2. JSDVCRouter 維護: 其包含着真正對控制器初始化跳轉和指派的代碼這裡一般很少進行修改; 比如後期需支援跳轉到 H5, 處理 3D Touch, Universal Links 時來這裡進行維護;

實戰 Code!

寫到這裡, 筆者不知道上面講的對 Router 實作控制器跳轉的簡要介紹, 是否起到幫助初步接觸 Router 時的同學們, 希望下面通過 Code 的方式能讓大家更好的了解和使用起來!

下面詳細介紹筆者封裝 JLRoutes 實作控制器跳轉的三個類:

JSDVCRouterConfig

這個檔案主要用于管理所有 Router 映射到指定控制器類名(class), 以及相關參數的配置檔案(title,needLogin等), 具體配置根據實際項目需求進行即可;

  1. 為了編譯期能更好的檢查到錯誤, 使用 extern NSString const 聲明, 配合 NSString const 實作指定 Router URL, 使用的時候直接通過外部聲明的常量字元串來指定跳轉即可;
  1. 這樣管理 Router URL 能更加友善閱讀和維護, 如果直接使用 @"/login" 的方式來進行綁定可讀性差, 很容易出現粗心大意導緻的錯誤;

    代碼如下:

//App 内所有控制器
    extern NSString* const JSDVCRouteWebview;
    extern NSString* const JSDVCRouteLogin;
    @interface JSDVCRouterConfig : NSObject

    + (NSDictionary *)configMapInfo;
    
    @end
    
    //App 内相關控制器
    NSString* const JSDVCRouteWebview = @"/webView";
    NSString* const JSDVCRouteLogin = @"/login";
    @implementation JSDVCRouterConfig
    
    + (NSDictionary *)configMapInfo {
    
    return @{
        JSDVCRouteWebview: @{@"class": @"JSDWebViewVC",
                             @"title": @"WebView",
                             @"flags": @"",
                             @"needLogin": @"",
        },
        JSDVCRouteLogin: @{@"class": @"JSDLoginVC",
                           @"title": @"登入",
                           @"flags": @"",
                           @"needLogin": @"",
        },
        };
    @end            
JSDVCRouter

這個類内部實作的事情非常簡單, 繼承自 NSObject, 對外提供 注冊和調用 Router 接口, 在内部調用 JLRoutes 提供的接口;

在項目中所有跳轉均使用此類提供的接口來調用 Router;

一個是預設不帶任何參數 
另一個可以攜帶我們需要的參數(NSDictionary);           
[JSDVCRouter openURL:JSDVCRouteAppear]; //push 到 AppearVC; 
[JSDVCRouter openURL:JSDVCRouteAppear parameters:@{kJSDVCRouteSegue: kJSDVCRouteSegueModal, @"name": @"jersey"}]; // Modal 到 Appear VC 并攜帶參數 name;           

單獨封裝一個 JSDVCRouter 好處:

防止三方庫入侵. 其繼承自 NSObject 并不直接依賴于 JLRouter, 這樣在後期如果考慮更換三方庫, 或者自己封裝一套類似 JLRouter 提供的功能時, 隻需要對其修改即可, 其他地方均無需修改;
@interface JSDVCRouter : NSObject

    + (BOOL)openURL:(NSString *)url;//調用 Router;
    + (BOOL)openURL:(NSString *)url parameters:(NSDictionary *)parameters;
    
    + (void)addRoute:(NSString* )route handler:(BOOL (^)(NSDictionary *parameters))handlerBlock;//注冊 Router,調用 Router 時會觸發回調; 

    @end
    #define JSDRouterURL(string) [NSURL URLWithString:string]

    @implementation JSDVCRouter
    
    + (BOOL)openURL:(NSString *)url {
        
        return [self routeURL:url parameters:nil];
    }
    
    + (BOOL)openURL:(NSString *)url parameters:(NSDictionary *)parameters {
        
       return [self routeURL:url parameters:parameters];
    }
    
    + (void)addRoute:(NSString *)route handler:(BOOL (^)(NSDictionary * _Nonnull parameters))handlerBlock {
        
        [JLRoutes addRoute:route handler:handlerBlock];
    }

    #pragma mark - mark JLRouter
    
    + (BOOL)routeURL:(NSString*)url parameters:(NSDictionary *)parameters{
        
        return [JLRoutes routeURL:JSDRouterURL(url) withParameters:parameters];
    }
    
    @end
               

JSDVCRouter+Handle

真正注冊和調用 Router 時處理回調控制器跳轉和參數指派邏輯實作放在這裡。

注冊 Router : 對控制器内所有 Router 一一進行注冊以及 TabBarIndex 切換和 處理傳回 Router, 将回調統一轉發到定義的方法裡頭。

處理 Router: 也就是注冊好 Router 之後, 調用相應 Router 時, 我們在注冊時寫得回調方法, 這裡是執行控制器跳轉和傳參的邏輯。

關于控制器跳轉: 在觸發 Router 時, 我們能拿到 Router 映射到的 Map, 擷取到其 Class, 在通過 Class 來進行初始初始化執行個體, 這裡通過對 UIViewController Category 找到目前 visibleVC 來進行 Push 或 Modal, 我們也可以根據業務方傳遞過來的參數來決定進行 Push 或 Modal 以及是否需要執行動畫等等;

關于傳參: 傳遞過來的參數是字典的資料結構, 是以我們先檢測執行個體 VC 是否包含這個屬性, [vc respondsToSelector:NSSelectorFromString(key)], 如果 VC 有這個屬性則直接使用 KVC 的方式來進行指派, 為了防止在開發時, 傳入的字典 Key 與 VC 屬性不比對導緻一些 Bug, 添加一層 NSAssert,這樣能在開發過程中更快找到問題!

筆者自行封裝的控制器跳轉邏輯可能有考慮不周的地方, 主要還得根據具體業務需求來做具體判斷;

下面分别是注冊 Router 和比對到 Router 之後回調處理代碼, 有點長請耐心閱讀

Router 注冊, 将三種類型回調處理統一

@implementation JSDVCRouter (Handle)
    //注冊 Router,  控制器的跳轉 + UITabBarIndex 切換 + 頁面傳回
    + (void)load {
        [self performSelectorOnMainThread:@selector(registerRouter) withObject:nil waitUntilDone:false];
    }
    + (void)registerRouter {
    
    //擷取全局 RouterMapInfo
    NSDictionary* routerMapInfo = [JSDVCRouterConfig configMapInfo];
    
    // router 對應控制器路徑, 使用其來注冊 Route, 當調用目前 Route 時會執行回調; 回調參數 parameters: 在執行 Route 時傳入的參數;
    for (NSString* router in routerMapInfo.allKeys) {
        
        NSDictionary* routerMap = routerMapInfo[router];
        NSString* className = routerMap[kJSDVCRouteClassName];
        if (JSDIsString(className)) {
            /*注冊所有控制器 Router, 使用 [JSDVCRouter openURL:JSDVCRouteAppear]; push 到 AppearVC;
            [JSDVCRouter openURL:JSDVCRouteAppear parameters:@{kJSDVCRouteSegue: kJSDVCRouteSegueModal, @"name": @"jersey"}];  Modal 到 Appear VC 并攜帶參數 name;
             */
            [self addRoute:router handler:^BOOL(NSDictionary * _Nonnull parameters) {
                //執行路由比對成功之後,跳轉邏輯回調;
                /*執行 Route 回調; 處理控制器跳轉 + 傳參;
                ** routerMap: 目前 route 映射的  routeMap; 我們在 RouterConfig 配置的 Map;
                ** parameters: 調用 route 時, 傳入的參數;
                 */
                return [self executeRouterClassName:className routerMap:routerMap parameters:parameters];
            }];
        }
    }
    
    // 注冊 Router 到指定TabBar Index; 使用 [JSDVCRouter openURL:JSDVCRouteCafeTab] 切換到 Cafe Index
    [self addRoute:@"/rootTab/:index" handler:^BOOL(NSDictionary * _Nonnull parameters) {
        NSInteger index = [parameters[@"index"] integerValue];
        // 處理 UITabBarControllerIndex 切換;
        UITabBarController* tabBarVC = (UITabBarController* )[UIViewController jsd_rootViewController];
        if ([tabBarVC isKindOfClass:[UITabBarController class]] && index >= 0 && tabBarVC.viewControllers.count >= index) {
            UIViewController* indexVC = tabBarVC.viewControllers[index];
            if ([indexVC isKindOfClass:[UINavigationController class]]) {
                indexVC = ((UINavigationController *)indexVC).topViewController;
            }
            //傳參
            [self setupParameters:parameters forViewController:indexVC];
            tabBarVC.selectedIndex = index;
            return YES;
        } else {
            return NO;
        }
    }];
    // 注冊傳回上層頁面 Router, 使用 [JSDVCRouter openURL:kJSDVCRouteSegueBack] 傳回上一頁 或 [JSDVCRouter openURL:kJSDVCRouteSegueBack parameters:@{kJSDVCRouteBackIndex: @(2)}]  傳回前兩頁
    [self addRoute:kJSDVCRouteSegueBack handler:^BOOL(NSDictionary * _Nonnull parameters) {
        
        return [self executeBackRouterParameters:parameters];
    }];
    }           

Router 比對到之後回調: 執行個體化控制器, 參數指派, 頁面跳轉

#pragma mark - execute Router VC    
// 當查找到指定 Router 時, 觸發路由回調邏輯; 找不到已注冊 Router 則直接傳回 NO; 如需要的話, 也可以在這裡注冊一個全局未比對到 Router 執行的回調進行異常處理;
+ (BOOL)executeRouterClassName:(NSString *)className routerMap:(NSDictionary* )routerMap parameters:(NSDictionary* )parameters {
    // 攔截 Router 映射參數,是否需要登入才可跳轉;
    BOOL needLogin = [routerMap[kJSDVCRouteClassNeedLogin] boolValue];
    if (needLogin && !userIsLogin) {
        [JSDVCRouter openURL:JSDVCRouteLogin];
        return NO;
    }
    //統一初始化控制器,傳參和跳轉;
    UIViewController* vc = [self viewControllerWithClassName:className routerMap:routerMap parameters: parameters];
    if (vc) {
        [self gotoViewController:vc parameters:parameters];
        return YES;
    } else {
        return NO;
    }
}
// 根據 Router 映射到的類名執行個體化控制器;
+ (UIViewController *)viewControllerWithClassName:(NSString *)className routerMap:(NSDictionary *)routerMap parameters:(NSDictionary* )parameters {
    
    id vc = [[NSClassFromString(className) alloc] init];
    if (![vc isKindOfClass:[UIViewController class]]) {
        vc = nil;
    }
#if DEBUG
    //vc不是UIViewController
    NSAssert(vc, @"%s: %@ is not kind of UIViewController class, routerMap: %@",__func__ ,className, routerMap);
#endif
    //參數指派
    [self setupParameters:parameters forViewController:vc];
    
    return vc;
}
// 對 VC 參數指派
+ (void)setupParameters:(NSDictionary *)params forViewController:(UIViewController* )vc {
    
    for (NSString *key in params.allKeys) {
        BOOL hasKey = [vc respondsToSelector:NSSelectorFromString(key)];
        BOOL notNil = params[key] != nil;
        if (hasKey && notNil) {
            [vc setValue:params[key] forKey:key];
        }
        
#if DEBUG
    //vc沒有相應屬性,但卻傳了值
        if ([key hasPrefix:@"JLRoute"]==NO &&
            [key hasPrefix:@"JSDVCRoute"]==NO && [params[@"JLRoutePattern"] rangeOfString:[NSString stringWithFormat:@":%@",key]].location==NSNotFound) {
            NSAssert(hasKey == YES, @"%s: %@ is not property for the key %@",__func__ ,vc,key);
        }
#endif
    };
}
// 跳轉和參數設定;
+ (void)gotoViewController:(UIViewController *)vc parameters:(NSDictionary *)parameters {
    
    UIViewController* currentVC = [UIViewController jsd_findVisibleViewController];
    NSString *segue = parameters[kJSDVCRouteSegue] ? parameters[kJSDVCRouteSegue] : kJSDVCRouteSeguePush; //  決定 present 或者 Push; 預設值 Push
    BOOL animated = parameters[kJSDVCRouteAnimated] ? [parameters[kJSDVCRouteAnimated] boolValue] : YES;  // 轉場動畫;
    NSLog(@"%s 跳轉: %@ %@ %@",__func__ ,currentVC, segue,vc);
    
    if ([segue isEqualToString:kJSDVCRouteSeguePush]) { //PUSH
        if (currentVC.navigationController) {
            NSString *backIndexString = [NSString stringWithFormat:@"%@",parameters[kJSDVCRouteBackIndex]];
            UINavigationController* nav = currentVC.navigationController;
            if ([backIndexString isEqualToString:kJSDVCRouteIndexRoot]) {
                NSMutableArray *vcs = [NSMutableArray arrayWithObject:nav.viewControllers.firstObject];
                [vcs addObject:vc];
                [nav setViewControllers:vcs animated:animated];
                
            } else if ([backIndexString integerValue] && [backIndexString integerValue] < nav.viewControllers.count) {
                //移除掉指定數量的 VC, 在Push;
                NSMutableArray *vcs = [nav.viewControllers mutableCopy];
                [vcs removeObjectsInRange:NSMakeRange(vcs.count - [backIndexString integerValue], [backIndexString integerValue])];
                nav.viewControllers = vcs;
                [nav pushViewController:vc animated:YES];
            } else {
                [nav pushViewController:vc animated:animated];
            }
        }
        else { //由于無導航欄, 直接執行 Modal
            BOOL needNavigation = parameters[kJSDVCRouteSegueNeedNavigation] ? NO : YES;
            if (needNavigation) {
                UINavigationController* navigationVC = [[UINavigationController alloc] initWithRootViewController:vc];
                //vc.modalPresentationStyle = UIModalPresentationFullScreen;
                [currentVC presentViewController:navigationVC animated:YES completion:nil];
            }
            else {
                //vc.modalPresentationStyle = UIModalPresentationFullScreen;
                [currentVC presentViewController:vc animated:animated completion:nil];
            }
        }
    }
    else { //Modal
        BOOL needNavigation = parameters[kJSDVCRouteSegueNeedNavigation] ? parameters[kJSDVCRouteSegueNeedNavigation] : NO;
        if (needNavigation) {
            UINavigationController* navigationVC = [[UINavigationController alloc] initWithRootViewController:vc];
            //vc.modalPresentationStyle = UIModalPresentationFullScreen;
            [currentVC presentViewController:navigationVC animated:animated completion:nil];
        }
        else {
            //vc.modalPresentationStyle = UIModalPresentationFullScreen;
            [currentVC presentViewController:vc animated:animated completion:nil];
        }
    }
}           

能堅持看到這裡, 應該對 Router 進行控制器跳轉已經有了個不錯的了解!

待補充

App 内部跳轉除了, 頻繁的控制器之間切換外, 還有比如跳轉到 H5, 或者跳轉到 WebView 等;

App 外跳轉則包含 Scheme 啟動, 3D Touch, UniversalLink, 點選通知等都會觸發;

這些包含跳轉, 頁面切換的我們均可以統一使用 Router 來進行有效的管理, 使 App 變得更加動态化, 子產品之間耦合度更低;

  • [ ] 支援 H5 跳轉
  • [ ] 外部 Scheme 啟動 App
  • [ ] UniversalLink
  • [ ] 3D Touch Shortcut
  • [ ] 支援背景動态下發 RouterMap 配置

最後

希望此篇文章對您有所幫助,如有不對的地方,希望大家能留言指出糾正。

emmmmm,每次看到這麼長一串代碼, 要麼是直接跳過, 要麼就是認認真真看完之後馬上啟動 Xcode Coding 一遍, Commond + R 實踐一遍, 為了友善大家了解獻上

Demo

如果覺得對你有幫助, 麻煩大家給個 Star 謝謝😆!!!!!

學習的路上,與君共勉!!!
本文原創作者: Jersey . 歡迎轉載,請注明出處和 本文連結

參考-連結

iOS架構實踐幹貨 iOS應用架構談 元件化方案 蘑菇街 App 的元件化之路 實戰Demo

繼續閱讀