iOS組件化-路由設計分析

組件化也是一個老生常談的話題了,本文主要說一下在組件化中,站比較重要的位置的路由設計
你的項目裏多是直接依賴了三方的路由組件,也多是本身根據項目的實際需求私人訂製了一套路由組件,下面我想經過幾個呼聲比較高的三方組件來聊一聊路由的設計和分析。這裏不推薦說你們用哪一個好哪一個很差,只是學習他們設計思想。就比如咱們看三方庫源碼,應該都是學習編程和設計的思想爲主。html

前言

隨着App的需求愈來愈多,業務愈來愈複雜,爲了更高效的迭代以及提升用戶體驗,下降維護成本,對一個更高效的框架的需求也愈來愈急切。
因此咱們可能都經歷過項目的重構、組件化,根據項目的實際需求,新的框架可能須要橫向,縱向不一樣粒度的分層,爲了之後更有效率的開發和維護。隨之而來的一個問題,如何保持「高內聚,低耦合」的特色,下面就來談談解決這個問題的一些思路。git

路由能解決哪些問題

列舉幾個平時開發中遇到的問題,或者說是需求:github

  1. 推送消息,或是網頁打開一個url須要跳轉進入App內部的某個頁面
  2. 其餘App,或者本身公司的別的App之間的相互跳轉
  3. 不一樣組件之間頁面的跳轉
  4. 如何統一兩端的頁面跳轉邏輯
  5. 線上某個頁面出現bug,如何能降級成一個其餘的H5或者錯誤頁面
  6. 頁面跳轉埋點
  7. 跳轉過程當中的邏輯檢查

以上這些問題,均可以經過設計一個路由來解決,下面帶着這些問題繼續看如何實現跳轉。編程

實現跳轉

經過上面的問題,咱們但願設計一套路由,實現App外部和內部的統一跳轉,因此先說一下App外部跳轉的實現。數組

URL Scheme

在info.plist裏面添加URL types - URL Schemes瀏覽器

而後在Safari中輸入這裏設置的URL Schemes就能夠直接打開App安全

- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication annotation:(id)annotation
{
    
}
複製代碼

經過上面這個方法就能夠監聽到外部App的調用,能夠根據須要作一些攔截或者其餘操做。bash

App也是能夠直接跳轉到系統設置的。好比有些需求要求檢測用戶有沒有開啓某些系統權限,若是沒有開啓就彈框提示,點擊彈框的按鈕直接跳轉到系統設置裏面對應的設置界面。架構

Universal links

Universal links這個功能可使咱們的App經過http連接來啓動。app

  • 若是安裝過App,不論是在Safari仍是其餘三方瀏覽器或別的軟件中,均可以打開App
  • 若是沒安裝過,就會打開網頁

設置方式:

注意必需要 applinks:開頭

以上就是iOS系統中App間跳轉的二種方式。

路由設計

說完App間的跳轉邏輯,接下來就進入重點,App內部的路由設計。
主要要解決兩個問題:

  • 各個組件之間相互調用,隨着業務愈來愈複雜,若是組件化的粒度不太合適,會致使組件愈來愈多,組件間不可避免的依賴也愈來愈多
  • 頁面和他所在組件之間的調用,組件內例如push一個VC ,就須要import這個類,從而致使強依賴,這樣寫死的代碼也沒法在出現線上bug的時候降級爲其餘頁面

綜合上面所說的兩個問題,咱們該如何設計一個路由呢?固然是先去看看別人造好的輪子-。-,下面會列舉幾個我在開發中用到過,以及參考過的輪子,有拿來主義直接使用的,也有借鑑人家思想本身封裝的,總之都值得學習。

Route分析

JLRoutes

JLRoutes目前GitHub上star5.3k,應該是星最多的路由組件了,因此咱們第一個分析他的設計思路。

  1. JLRoutes維護了一個全局的JLRGlobal_routeControllersMap,這個map以scheme爲key,JLRoutes爲value,因此每個scheme都是惟一的。
+ (instancetype)routesForScheme:(NSString *)scheme
{
    JLRoutes *routesController = nil;
    
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        JLRGlobal_routeControllersMap = [[NSMutableDictionary alloc] init];
    });
    
    if (!JLRGlobal_routeControllersMap[scheme]) {
        routesController = [[self alloc] init];
        routesController.scheme = scheme;
        JLRGlobal_routeControllersMap[scheme] = routesController;
    }
    
    routesController = JLRGlobal_routeControllersMap[scheme];
    
    return routesController;
}
複製代碼
  1. scheme能夠看作是一個URI,每個註冊進來的字符串都會進行切分處理
- (void)addRoute:(NSString *)routePattern priority:(NSUInteger)priority handler:(BOOL (^)(NSDictionary<NSString *, id> *parameters))handlerBlock
{
    NSArray <NSString *> *optionalRoutePatterns = [JLRParsingUtilities expandOptionalRoutePatternsForPattern:routePattern];
    JLRRouteDefinition *route = [[JLRGlobal_routeDefinitionClass alloc] initWithPattern:routePattern priority:priority handlerBlock:handlerBlock];
    
    if (optionalRoutePatterns.count > 0) {
        // there are optional params, parse and add them
        for (NSString *pattern in optionalRoutePatterns) {
            JLRRouteDefinition *optionalRoute = [[JLRGlobal_routeDefinitionClass alloc] initWithPattern:pattern priority:priority handlerBlock:handlerBlock];
            [self _registerRoute:optionalRoute];
            [self _verboseLog:@"Automatically created optional route: %@", optionalRoute];
        }
        return;
    }
    
    [self _registerRoute:route];
}
複製代碼
  1. 按優先級插入JLRoutes的數組中,優先級高的排列在前面
- (void)_registerRoute:(JLRRouteDefinition *)route
{
    if (route.priority == 0 || self.mutableRoutes.count == 0) {
        [self.mutableRoutes addObject:route];
    } else {
        NSUInteger index = 0;
        BOOL addedRoute = NO;
        
        // search through existing routes looking for a lower priority route than this one
        for (JLRRouteDefinition *existingRoute in [self.mutableRoutes copy]) {
            if (existingRoute.priority < route.priority) {
                // if found, add the route after it
                [self.mutableRoutes insertObject:route atIndex:index];
                addedRoute = YES;
                break;
            }
            index++;
        }
        
        // if we weren't able to find a lower priority route, this is the new lowest priority route (or same priority as self.routes.lastObject) and should just be added if (!addedRoute) { [self.mutableRoutes addObject:route]; } } [route didBecomeRegisteredForScheme:self.scheme]; } 複製代碼
  1. 查找路由
    根據URL初始化一個JLRRouteRequest,而後在JLRoutes的數組中依次查找,直到找到一個匹配的而後獲取parameters,執行Handler
- (BOOL)_routeURL:(NSURL *)URL withParameters:(NSDictionary *)parameters executeRouteBlock:(BOOL)executeRouteBlock
{
    if (!URL) {
        return NO;
    }
    
    [self _verboseLog:@"Trying to route URL %@", URL];
    
    BOOL didRoute = NO;
    
    JLRRouteRequestOptions options = [self _routeRequestOptions];
    JLRRouteRequest *request = [[JLRRouteRequest alloc] initWithURL:URL options:options additionalParameters:parameters];
    
    for (JLRRouteDefinition *route in [self.mutableRoutes copy]) {
        // check each route for a matching response
        JLRRouteResponse *response = [route routeResponseForRequest:request];
        if (!response.isMatch) {
            continue;
        }
        
        [self _verboseLog:@"Successfully matched %@", route];
        
        if (!executeRouteBlock) {
            // if we shouldn't execute but it was a match, we're done now
            return YES;
        }
        
        [self _verboseLog:@"Match parameters are %@", response.parameters];
        
        // Call the handler block
        didRoute = [route callHandlerBlockWithParameters:response.parameters];
        
        if (didRoute) {
            // if it was routed successfully, we're done - otherwise, continue trying to route break; } } if (!didRoute) { [self _verboseLog:@"Could not find a matching route"]; } // if we couldn't find a match and this routes controller specifies to fallback and its also not the global routes controller, then...
    if (!didRoute && self.shouldFallbackToGlobalRoutes && ![self _isGlobalRoutesController]) {
        [self _verboseLog:@"Falling back to global routes..."];
        didRoute = [[JLRoutes globalRoutes] _routeURL:URL withParameters:parameters executeRouteBlock:executeRouteBlock];
    }
    
    // if, after everything, we did not route anything and we have an unmatched URL handler, then call it
    if (!didRoute && executeRouteBlock && self.unmatchedURLHandler) {
        [self _verboseLog:@"Falling back to the unmatched URL handler"];
        self.unmatchedURLHandler(self, URL, parameters);
    }
    
    return didRoute;
}
複製代碼

CTMediator

CTMediator 目前github上star 3.3k ,這個庫特別的輕量級,只有一個類和一個category,一共也沒幾行代碼,更可的是做者還在關鍵代碼處添加了中文註釋以及比較詳細的example
主要思想是利用Target-Action,使用runtime實現解耦。這種模式每一個組件之間互不依賴,可是都依賴中間件進行調度。
頭文件中暴露了兩個分別處理遠程App和本地組件調用的方法

// 遠程App調用入口
- (id _Nullable)performActionWithUrl:(NSURL * _Nullable)url completion:(void(^_Nullable)(NSDictionary * _Nullable info))completion;
// 本地組件調用入口
- (id _Nullable )performTarget:(NSString * _Nullable)targetName action:(NSString * _Nullable)actionName params:(NSDictionary * _Nullable)params shouldCacheTarget:(BOOL)shouldCacheTarget;
複製代碼

對於遠程App,還作了一步安全處理,最後解析完也是一樣調用了本地組件處理的方法中

- (id)performActionWithUrl:(NSURL *)url completion:(void (^)(NSDictionary *))completion
{
    if (url == nil) {
        return nil;
    }
    
    NSMutableDictionary *params = [[NSMutableDictionary alloc] init];
    NSString *urlString = [url query];
    for (NSString *param in [urlString componentsSeparatedByString:@"&"]) {
        NSArray *elts = [param componentsSeparatedByString:@"="];
        if([elts count] < 2) continue;
        [params setObject:[elts lastObject] forKey:[elts firstObject]];
    }
    
    // 這裏這麼寫主要是出於安全考慮,防止黑客經過遠程方式調用本地模塊。這裏的作法足以應對絕大多數場景,若是要求更加嚴苛,也能夠作更加複雜的安全邏輯。
    NSString *actionName = [url.path stringByReplacingOccurrencesOfString:@"/" withString:@""];
    if ([actionName hasPrefix:@"native"]) {
        return @(NO);
    }
    
    // 這個demo針對URL的路由處理很是簡單,就只是取對應的target名字和method名字,但這已經足以應對絕大部份需求。若是須要拓展,能夠在這個方法調用以前加入完整的路由邏輯
    id result = [self performTarget:url.host action:actionName params:params shouldCacheTarget:NO];
    if (completion) {
        if (result) {
            completion(@{@"result":result});
        } else {
            completion(nil);
        }
    }
    return result;
}
複製代碼

對於無響應的請求還統一作了處理

- (void)NoTargetActionResponseWithTargetString:(NSString *)targetString selectorString:(NSString *)selectorString originParams:(NSDictionary *)originParams
{
    SEL action = NSSelectorFromString(@"Action_response:");
    NSObject *target = [[NSClassFromString(@"Target_NoTargetAction") alloc] init];
    
    NSMutableDictionary *params = [[NSMutableDictionary alloc] init];
    params[@"originParams"] = originParams;
    params[@"targetString"] = targetString;
    params[@"selectorString"] = selectorString;
    
    [self safePerformAction:action target:target params:params];
}
複製代碼
使用

具體使用須要下面幾個步驟 :

  • 對於每個業務線(或者組件),若是須要被其餘組件調度,那麼就要爲這個組件建立一個Target類,以Target_爲前綴命名
    這個類裏面就要添加上全部須要被其餘組件調度的方法,方法以Action_爲前綴命名。
  • 爲每個組件建立一個CTMediator的category
    這個category就是供調用方依賴完成調度的,這樣category中全部方法的調用就很統一,所有是performTarget: action: params: shouldCacheTarget:
  • 最終的調度邏輯都交給CTMediator
  • 調用方只須要依賴有調度需求的組件的category

感興趣的還能夠看一下做者的文章,詳細介紹了CTMediator的設計思想以及爲已有項目添加CTMediator
iOS應用架構談 組件化方案
在現有工程中實施基於CTMediator的組件化方案

MGJRouter

MGJRouter 目前github上star 2.2k

這個庫的由來:JLRoutes 的問題主要在於查找 URL 的實現不夠高效,經過遍歷而不是匹配。還有就是功能偏多。 HHRouter 的 URL 查找是基於匹配,因此會更高效,MGJRouter 也是採用的這種方法,但它跟 ViewController 綁定地過於緊密,必定程度上下降了靈活性。 因而就有了 MGJRouter。

/**
 *  保存了全部已註冊的 URL
 *  結構相似 @{@"beauty": @{@":id": {@"_", [block copy]}}}
 */
@property (nonatomic) NSMutableDictionary *routes;
複製代碼

MGJRouter是一個單例對象,在其內部維護着一個「URL -> block」格式的註冊表,經過這個註冊表來保存服務方註冊的block。使調用方能夠經過URL映射出block,並經過MGJRouter對服務方發起調用。

大概的使用流程以下:

  • 業務組件對外提供一個PublicHeader,在PublicHeader中聲明外部能夠調用的一系列URL
#ifndef MMCUserUrlDefines_h
#define MMCUserUrlDefines_h

/**
 description 個人我的中心頁
 
 @return MMCUserViewController
 */
#define MMCRouterGetUserViewController @"MMC://User/UserCenter"

/**
 description 個人消息列表
 
 @return MMCMessageListViewController
 */
#define MMCRouterGetMessageVC @"MMC://User/MMCMessageListViewController"
複製代碼
  • 在組件內部實現block的註冊工做,調用方經過URL對block發起調用
+ (void)registerGotoUserVC
{
    [MMCRouter registerURLPattern:MMCRouterGetUserViewController toHandler:^(NSDictionary *params) {
    
    }];
}
複製代碼
  • 經過openURL調用,能夠經過GET請求的方式在url後面拼接參數,也能夠經過param傳入一個字典
[MMCRouter openURL:MMCRouterGetUserViewController];
複製代碼
  • 除了跳轉,MGJRouter還提供了能夠返回一個對象的方法
+ (void)registerURLPattern:(NSString *)URLPattern toObjectHandler:(MGJRouterObjectHandler)handler
複製代碼

舉個例子:這個route就返回了一個控制器,能夠交給調用方自行處理。

+(void)registerSearchCarListVC{
    [MMCRouter registerURLPattern:MMCRouterGetSearchCarListController toObjectHandler:^id(NSDictionary *params) {
        NSDictionary *userInfo = [params objectForKey:MMCRouterParameterUserInfo];
        NSString *carType = [userInfo objectForKey:MMCRouterCarType];
        MMCSearchCarListViewController *vc = [[MMCSearchCarListViewController alloc] init];
        vc.strCarType = carType;
        return vc;
    }];
}
複製代碼
Protocol-class方案

根據上面介紹的MGJRouter的使用,不難看出存在URL硬編碼和參數侷限性的問題,爲了解決這些問題,蘑菇街又提出來Protocol方案。Protocol方案由兩部分組成,進行組件間通訊的ModuleManager類以及MGJComponentProtocol協議類。

經過中間件ModuleManager進行消息的調用轉發,在ModuleManager內部維護一張映射表,映射表由以前的"URL -> block"變成"Protocol -> Class"。

由於目前手裏的項目沒有用到這個,因此使用代碼就不貼了,感興趣的能夠自行百度。

優缺點

URL註冊方案

優勢:

  1. 最容易想到的最簡單的方式
  2. 能夠統一三端的調度
  3. 線上bug動態降級處理

缺點:

  1. 硬編碼,URL須要專門管理
  2. URL規則須要提早註冊
  3. 有常駐內存,可能發生內存問題

Protocol-Class方案

優勢:

  1. 無硬編碼
  2. 參數無限制,甚至能夠傳遞model

缺點:

  1. 增長了新的中間件以及不少protocol,調用編碼複雜
  2. route的跳轉邏輯分散在各個類中,很差維護

Target-Action方案

優勢:

  1. 利用runtime實現解耦調用,無需註冊
  2. 有必定的安全處理
  3. 統一App外部和組件間的處理

缺點:

  1. 須要爲每個組件另外建立一個category,做者建議category也是一個單獨的pod
  2. 調用內部也是硬編碼,要求Target_ ,Action_ 命名規則

最後想說的是,沒有最好的route,只有最適合你項目的route,根據本身項目的實際狀況,分析決定要使用哪種組件化方案。

相關文章
相關標籤/搜索