手機版 | wap版 | 網站主頁 | HOME | 3G網頁
<button id="brbvl"><acronym id="brbvl"></acronym></button>

<dd id="brbvl"></dd>
<button id="brbvl"></button>
      1. <progress id="brbvl"></progress>
        <tbody id="brbvl"><track id="brbvl"></track></tbody>
        <em id="brbvl"><tr id="brbvl"></tr></em>

        iOS 組件化方案探索 [複製鏈接]

        2018-4-8 10:30
        EmailLi 閱讀:4815 評論:2 贊:2
        Tag:  

        首先我覺得」組件」在這裡不太合適,因為按我理解組件是指比較小的功能塊,這些組件不需要多少組件間通信,沒什麼依賴,也就不需要做什麼其他處理,面向對象就能搞定。而這裡提到的是較大粒度的業務功能,我們習慣稱為」模塊」。為了方便表述,下面模塊和組件代表同一個意思,都是指較大粒度的業務模塊。

        一個 APP 有多個模塊,模塊之間會通信,互相調用,例如微信讀書有 書籍詳情 想法列表 閱讀器 發現卡片 等等模塊,這些模塊會互相調用,例如 書籍詳情要調起閱讀器和想法列表,閱讀器要調起想法列表和書籍詳情,等等,一般我們是怎樣調用呢,以閱讀器為例,會這樣寫:

        #import "WRBookDetailViewController.h"
        #import "WRReviewViewController.h"
        @implementation WRReadingViewController
        - (void)gotoDetail {
         WRBookDetailViewController *detailVC = [[WRBookDetailViewController alloc] initWithBookId:self.bookId];
         [self.navigationController pushViewController:detailVC animated:YES];
        }
        
        - (void)gotoReview {
         WRReviewViewController *reviewVC = [[WRReviewViewController alloc] initWithBookId:self.bookId reviewType:1];
         [self.navigationController pushViewController:reviewVC animated:YES];
        }
        @end
        

        看起來挺好,這樣做簡單明了,沒有多餘的東西,項目初期推薦這樣快速開發,但到了項目越來越龐大,這種方式會有什麼問題呢?顯而易見,每個模塊都離不開其他模塊,互相依賴粘在一起成為一坨:

        component1

        這樣揉成一坨對測試/編譯/開發效率/後續擴展都有一些壞處,那怎麼解開這一坨呢。很簡單,按軟體工程的思路,下意識就會加一個中間層:

        component2

        叫他 Mediator Manager Router 什麼都行,反正就是負責轉發信息的中間層,暫且叫他 Mediator。

        看起來順眼多了,但這裡有幾個問題:

        1. Mediator 怎麼去轉發組件間調用?
        2. 一個模塊只跟 Mediator 通信,怎麼知道另一個模塊提供了什麼介面?
        3. 按上圖的畫法,模塊和 Mediator 間互相依賴,怎樣破除這個依賴?

        方案1

        對於前兩個問題,最直接的反應就是在 Mediator 直接提供介面,調用對應模塊的方法:

        //Mediator.m
        #import "BookDetailComponent.h"
        #import "ReviewComponent.h"
        @implementation Mediator
        + (UIViewController *)BookDetailComponent_viewController:(NSString *)bookId {
         return [BookDetailComponent detailViewController:bookId];
        }
        + (UIViewController *)ReviewComponent_viewController:(NSString *)bookId reviewType:(NSInteger)type {
         return [ReviewComponent reviewViewController:bookId type:type];
        }
        @end
        
        //BookDetailComponent 組件
        #import "Mediator.h"
        #import "WRBookDetailViewController.h"
        @implementation BookDetailComponent
        + (UIViewController *)detailViewController:(NSString *)bookId {
         WRBookDetailViewController *detailVC = [[WRBookDetailViewController alloc] initWithBookId:bookId];
         return detailVC;
        }
        @end
        
        //ReviewComponent 組件
        #import "Mediator.h"
        #import "WRReviewViewController.h"
        @implementation ReviewComponent
        + (UIViewController *)reviewViewController:(NSString *)bookId type:(NSInteger)type {
         UIViewController *reviewVC = [[WRReviewViewController alloc] initWithBookId:bookId type:type];
         return reviewVC;
        }
        @end
        

        然後在閱讀模塊里:

        //WRReadingViewController.m
        #import "Mediator.h"
        @implementation WRReadingViewController
        - (void)gotoDetail:(NSString *)bookId {
         UIViewController *detailVC = [Mediator BookDetailComponent_viewControllerForDetail:bookId];
         [self.navigationController pushViewController:detailVC];
        
         UIViewController *reviewVC = [Mediator ReviewComponent_viewController:bookId type:1];
         [self.navigationController pushViewController:reviewVC];
        }
        @end
        

        這就是一開始架構圖的實現,看起來顯然這樣做並沒有什麼好處,依賴關係並沒有解除,Mediator 依賴了所有模塊,而調用者又依賴 Mediator,最後還是一坨互相依賴,跟原來沒有 Mediator 的方案相比除了更麻煩點其他沒區別。

        那怎麼辦呢。

        怎樣讓Mediator解除對各個組件的依賴,同時又能調到各個組件暴露出來的方法?對於OC有一個法寶可以做到,就是runtime反射調用:

        //Mediator.m
        @implementation Mediator
        + (UIViewController *)BookDetailComponent_viewController:(NSString *)bookId {
         Class cls = NSClassFromString(@"BookDetailComponent");
         return [cls performSelector:NSSelectorFromString(@"detailViewController:") withObject:@{@"bookId":bookId}];
        }
        + (UIViewController *)ReviewComponent_viewController:(NSString *)bookId type:(NSInteger)type {
         Class cls = NSClassFromString(@"ReviewComponent");
         return [cls performSelector:NSSelectorFromString(@"reviewViewController:") withObject:@{@"bookId":bookId, @"type": @(type)}];
        }
        @end
        

        這下 Mediator 沒有再對各個組件有依賴了,你看已經不需要 #import 什麼東西了,對應的架構圖就變成:

        component3

        只有調用其他組件介面時才需要依賴 Mediator,組件開發者不需要知道 Mediator 的存在。

        等等,既然用runtime就可以解耦取消依賴,那還要Mediator做什麼?組件間調用時直接用runtime介面調不就行了,這樣就可以沒有任何依賴就完成調用:

        //WRReadingViewController.m
        @implementation WRReadingViewController
        - (void)gotoReview:(NSString *)bookId {
         Class cls = NSClassFromString(@"ReviewComponent");
         UIViewController *reviewVC = [cls performSelector:NSSelectorFromString(@"reviewViewController:") withObject:@{@"bookId":bookId, @"type": @(1)}];
         [self.navigationController pushViewController:reviewVC];
        }
        @end
        

        這樣就完全解耦了,但這樣做的問題是:

        1. 調用者寫起來很噁心,代碼提示都沒有,每次調用寫一坨。
        2. runtime方法的參數個數和類型限制,導致只能每個介面都統一傳一個 NSDictionary。這個 NSDictionary里的key value是什麼不明確,需要找個地方寫文檔說明和查看。
        3. 編譯器層面不依賴其他組件,實際上還是依賴了,直接在這裡調用,沒有引入調用的組件時就掛了

        把它移到Mediator后:

        1. 調用者寫起來不噁心,代碼提示也有了。
        2. 參數類型和個數無限制,由 Mediator 去轉就行了,組件提供的還是一個 NSDictionary 參數的介面,但在Mediator 里可以提供任意類型和個數的參數,像上面的例子顯式要求參數 NSString *bookId 和 NSInteger type
        3. Mediator可以做統一處理,調用某個組件方法時如果某個組件不存在,可以做相應操作,讓調用者與組件間沒有耦合。

        到這裡,基本上能解決我們的問題:各組件互不依賴,組件間調用只依賴中間件Mediator,Mediator不依賴其他組件。接下來就是優化這套寫法,有兩個優化點:

        1. Mediator 每一個方法里都要寫 runtime 方法,格式是確定的,這是可以抽取出來的。
        2. 每個組件對外方法都要在 Mediator 寫一遍,組件一多 Mediator 類的長度是恐怖的。

        優化后就成了 casa 的方案,target-action 對應第一點,target就是class,action就是selector,通過一些規則簡化動態調用。Category 對應第二點,每個組件寫一個 Mediator 的 Category,讓 Mediator 不至於太長。這裡有個demo

        總結起來就是,組件通過中間件通信,中間件通過 runtime 介面解耦,通過 target-action 簡化寫法,通過 category 感官上分離組件介面代碼。

        方案2

        回到 Mediator 最初的三個問題,蘑菇街用的是另一種方式解決:註冊表的方式,用URL表示介面,在模塊啟動時註冊模塊提供的介面,一個簡化的實現:

        //Mediator.m 中間件
        @implementation Mediator
        typedef void (^componentBlock) (id param);
        @property (nonatomic, storng) NSMutableDictionary *cache
        - (void)registerURLPattern:(NSString *)urlPattern toHandler:(componentBlock)blk {
         [cache setObject:blk forKey:urlPattern];
        }
        
        - (void)openURL:(NSString *)url withParam:(id)param {
         componentBlock blk = [cache objectForKey:url];
         if (blk) blk(param);
        }
        @end
        
        //BookDetailComponent 組件
        #import "Mediator.h"
        #import "WRBookDetailViewController.h"
        + (void)initComponent {
         [[Mediator sharedInstance] registerURLPattern:@"weread://bookDetail" toHandler:^(NSDictionary *param) {
         WRBookDetailViewController *detailVC = [[WRBookDetailViewController alloc] initWithBookId:param[@"bookId"]];
         [[UIApplication sharedApplication].keyWindow.rootViewController.navigationController pushViewController:detailVC animated:YES];
         }];
        }
        
        //WRReadingViewController.m 調用者
        //ReadingViewController.m
        #import "Mediator.h"
        
        + (void)gotoDetail:(NSString *)bookId {
         [[Mediator sharedInstance] openURL:@"weread://bookDetail" withParam:@{@"bookId": bookId}];
        }
        

        這樣同樣做到每個模塊間沒有依賴,Mediator 也不依賴其他組件,不過這裡不一樣的一點是組件本身和調用者都依賴了Mediator,不過這不是重點,架構圖還是跟方案1一樣。

        各個組件初始化時向 Mediator 註冊對外提供的介面,Mediator 通過保存在內存的表去知道有哪些模塊哪些介面,介面的形式是 URL->block

        這裡拋開URL的遠程調用和本地調用混在一起導致的問題,先說只用於本地調用的情況,對於本地調用,URL只是一個表示組件的key,沒有其他作用,這樣做有三個問題:

        1. 需要有個地方列出各個組件里有什麼 URL 介面可供調用。蘑菇街做了個後台專門管理。
        2. 每個組件都需要初始化,內存里需要保存一份表,組件多了會有內存問題。
        3. 參數的格式不明確,是個靈活的 dictionary,也需要有個地方可以查參數格式。

        第二點沒法解決,第一點和第三點可以跟前面那個方案一樣,在 Mediator 每個組件暴露方法的轉介面,然後使用起來就跟前面那種方式一樣了。

        拋開URL不說,這種方案跟方案1的共同思路就是:Mediator 不能直接去調用組件的方法,因為這樣會產生依賴,那我就要通過其他方法去調用,也就是通過 字元串->方法 的映射去調用。runtime 介面的 className + selectorName -> IMP 是一種,註冊表的 key -> block 是一種,而前一種是 OC 自帶的特性,后一種需要內存維持一份註冊表,這是不必要的。

        現在說回 URL,組件化是不應該跟 URL 扯上關係的,因為組件對外提供的介面主要是模塊間代碼層面上的調用,我們先稱為本地調用,而 URL 主要用於 APP 間通信,姑且稱為遠程調用。按常規思路者應該是對於遠程調用,再加個中間層轉發到本地調用,讓這兩者分開。那這裡這兩者混在一起有什麼問題呢?

        如果是 URL 的形式,那組件對外提供介面時就要同時考慮本地調用和遠程調用兩種情況,而遠程調用有個限制,傳遞的參數類型有限制,只能傳能被字元串化的數據,或者說只能傳能被轉成 json 的數據,像 UIImage 這類對象是不行的,所以如果組件介面要考慮遠程調用,這裡的參數就不能是這類非常規對象,介面的定義就受限了。

        用理論的話來說就是,遠程調用是本地調用的子集,這裡混在一起導致組件只能提供子集功能,無法提供像方案1那樣提供全集功能。所以這個方案是天生有缺陷的,對於遺漏的這部分功能,蘑菇街使用了另一種方案補全,請看方案3。

        方案3

        蘑菇街為了補全本地調用的功能,為組件多加了另一種方案,就是通過 protocol-class 註冊表的方式。首先有一個新的中間件:

        //ProtocolMediator.m 新中間件
        @implementation ProtocolMediator
        @property (nonatomic, storng) NSMutableDictionary *protocolCache
        - (void)registerProtocol:(Protocol *)proto forClass:(Class)cls {
         NSMutableDictionary *protocolCache;
         [protocolCache setObject:cls forKey:NSStringFromProtocol(proto)];
        }
        
        - (Class)classForProtocol:(Protocol *)proto {
         return protocolCache[NSStringFromProtocol(proto)];
        }
        @end
        

        然後有一個公共Protocol文件,定義了每一個組件對外提供的介面:

        //ComponentProtocol.h
        @protocol BookDetailComponentProtocol <NSObject>
        - (UIViewController *)bookDetailController:(NSString *)bookId;
        - (UIImage *)coverImageWithBookId:(NSString *)bookId;
        @end
        
        @protocol ReviewComponentProtocol <NSObject>
        - (UIViewController *)ReviewController:(NSString *)bookId;
        @end
        

        再在模塊里實現這些介面,並在初始化時調用 registerProtocol 註冊。

        //BookDetailComponent 組件
        #import "ProtocolMediator.h"
        #import "ComponentProtocol.h"
        #import "WRBookDetailViewController.h"
        + (void)initComponent
        {
         [[ProtocolMediator sharedInstance] registerProtocol:@protocol(BookDetailComponentProtocol) forClass:[self class];
        }
        
        - (UIViewController *)bookDetailController:(NSString *)bookId {
         WRBookDetailViewController *detailVC = [[WRBookDetailViewController alloc] initWithBookId:param[@"bookId"]];
         return detailVC;
        }
        
        - (UIImage *)coverImageWithBookId:(NSString *)bookId {
         ….
        }
        

        最後調用者通過 protocol 從 ProtocolMediator 拿到提供這些方法的 Class,再進行調用:

        //WRReadingViewController.m 調用者
        //ReadingViewController.m
        #import "ProtocolMediator.h"
        #import "ComponentProtocol.h"
        + (void)gotoDetail:(NSString *)bookId {
         Class cls = [[ProtocolMediator sharedInstance] classForProtocol:BookDetailComponentProtocol];
         id bookDetailComponent = [[cls alloc] init];
         UIViewController *vc = [bookDetailComponent bookDetailController:bookId];
         [[UIApplication sharedApplication].keyWindow.rootViewController.navigationController pushViewController:vc animated:YES];
        }
        

        這種思路有點繞,這個方案跟剛才兩個最大的不同就是,它不是直接通過 Mediator 調用組件方法,而是通過 Mediator 拿到組件對象,再自行去調用組件方法。

        結果就是組件方法的調用是分散在各地的,沒有統一的入口,也就沒法做組件不存在時的統一處理。組件1調用了組件2的方法,如果用前面兩種方式,組件間是沒有依賴的,組件1+Mediator可以單獨抽離出來,只需要在Mediator里做好調用組件2方法時的異常處理就行。而這種方法組件1對組件2的調用分散在各個地方,沒法做這些處理,在不修改組件1代碼的情況下,組件1和組件2是分不開的。

        當然你也可以在這上面跟方案1一樣在 Mediator 對每一個組件介面 wrapper 一層,那這樣這種方案跟方案1比除了更複雜點,其他沒什麼區別。

        在 protocol-class 這個方案上,主要存在的問題就是分散調用導致耦合,另外實現上會有一些繞,其他就沒什麼了。casa 說的 「protocol對業務產生了侵入,且不符合黑盒模型。」 其實並沒有這麼誇張,實際上 protocol 對外提供組件方法,跟方案1在 Mediator wrapper 對外提供組件方法是差不多的。

        最後

        蘑菇街在一個項目里同時用了方案2和方案3兩種方式,會讓寫組件的人不知所措,新增一個介面時不知道該用方案2的方式還是方案3的方式,可能這個在蘑菇街內部會通過一些文檔規則去規範,但其實是沒有必要的。可能是蘑菇街作為電商平台一開始就注重APP頁面間跳轉的概念,每個模塊已經有一個對應的URL,於是組件化時自然想到通過URL的方式表示組件,後續發現URL方式的限制,於是加上方案3的方式,這也是正常的探索過程。

        上面論述下方案1確實比方案2+方案3簡單明了,沒有 註冊表常駐內存/參數傳遞限制/調用分散 這些缺點,方案1多做的一步是需要對所有組件方法進行一層 wrapper,但若想要明確提供組件的方法和參數類型,解耦統一處理,方案2和方案3同樣需要多加這層。


        我來說兩句
        您需要登錄后才可以評論 登錄 | 立即註冊
        facelist
        所有評論(2)
        linMairs 2018-4-10 18:17
        這裡面Demo的鏈接是沒有的
        回復
        關閉

        每日頭條

        通過郵件訂閱最新 Code4App 信息
        上一條 /4 下一條

        廣告投放| 台湾互聯網違法和不良信息舉報中心|中國互聯網舉報中心|Github|申請友鏈|手機版|Code4App ( 粵ICP備15117877號-1 )