1. 程式人生 > >iOS應用架構談(5) 元件化方案

iOS應用架構談(5) 元件化方案

簡述

前幾天的一個晚上在infoQ的微信群裡,來自蘑菇街的Limboy做了一個分享,講了蘑菇街的元件化之路。我不認為這條元件化之路蘑菇街走對了。分享後我私聊了Limboy,Limboy似乎也明白了問題所在,我答應他我會把我的方案寫成文章,於是這篇文章就出來了。

另外,按道理說元件化方案也屬於iOS應用架構談的一部分,但是當初構思架構談時,我沒打算寫元件化方案,因為我忘了還有這回事兒。。。後來寫到view的時候才想起來,所以在view的那篇文章最後補了一點內容。而且覺得這個元件化方案太簡單,包括實現元件化方案的元件也很簡單,程式碼算上註釋也才100行,我就偷懶放過了,畢竟寫一篇文章好累的啊。

蘑菇街的原文地址在這裡:《蘑菇街 App 的元件化之路》,沒有耐心看完原文的朋友,我在這裡簡要介紹一下蘑菇街的元件化是怎麼做的:

  1. App啟動時例項化各元件模組,然後這些元件向ModuleManager註冊Url,有些時候不需要例項化,使用class註冊。
  2. 當元件A需要呼叫元件B時,向ModuleManager傳遞URL,引數跟隨URL以GET方式傳遞,類似openURL。然後由ModuleManager負責排程元件B,最後完成任務。

這裡的兩步中,每一步都存在問題。

第一步的問題在於,在元件化的過程中,註冊URL並不是充分必要條件,元件是不需要向元件管理器註冊Url的。而且註冊了Url之後,會造成不必要的記憶體常駐,如果只是註冊Class,記憶體常駐量就小一點,如果是註冊例項,記憶體常駐量就大了。至於蘑菇街註冊的是Class還是例項,Limboy分享時沒有說,文章裡我也沒看出來,也有可能是我看漏了。不過這還並不能算是致命錯誤,只能算是小缺陷。

真正的致命錯誤在第二步。在iOS領域裡,一定是元件化的中介軟體為openUrl提供服務,而不是openUrl方式為元件化提供服務。

什麼意思呢?

也就是說,一個App的元件化方案一定不是建立在URL上的,openURL的跨App呼叫是可以建立在元件化方案上的。當然,如果App還沒有元件化,openURL方式也是可以建立的,就是醜陋一點而已。

為什麼這麼說?

因為元件化方案的實施過程中,需要處理的問題的複雜度,以及拆解、排程業務的過程的複雜度比較大,單純以openURL的方式是無法勝任讓一個App去實施元件化架構的。如果在給App實施元件化方案的過程中是基於openURL的方案的話,有一個致命缺陷:非常規物件無法參與本地元件間排程。關於非常規物件

我會在詳細講解元件化方案時有一個辨析。

實際App場景下,如果本地元件間採用GET方式的URL呼叫,就會產生兩個問題:

  • 根本無法表達非常規物件

比如你要呼叫一個圖片編輯模組,不能傳遞UIImage到對應的模組上去的話,這是一個很悲催的事情。 當然,這可以通過給方法新開一個引數,然後傳遞過去來解決。比如原來是:

[a openUrl:"http://casa.com/detail?id=123&type=0"];

同時就也要提供這樣的方法:

[a openUrl:"http://casa.com/detail" params:@{
    @"id":"123",
    @"type":"0",
    @"image":[UIImage imageNamed:@"test"]
}]

如果不像上面這麼做,複雜引數和非常規引數就無法傳遞。如果這麼做了,那麼事實上這就是拆分遠端呼叫和本地呼叫的入口了,這就變成了我文章中提倡的做法,也是蘑菇街方案沒有做到的地方。

另外,在本地呼叫中使用URL的方式其實是不必要的,如果業務工程師在本地間排程時需要給出URL,那麼就不可避免要提供params,在呼叫時要提供哪些params是業務工程師很容易懵逼的地方。。。在文章下半部分給出的demo程式碼樣例已經說明了業務工程師在本地間呼叫時,是不需要知道URL的,而且demo程式碼樣例也闡釋瞭如何解決業務工程師遇到傳params容易懵逼的問題。

  • URL註冊對於實施元件化方案是完全不必要的,且通過URL註冊的方式形成的元件化方案,拓展性和可維護性都會被打折

註冊URL的目的其實是一個服務發現的過程,在iOS領域中,服務發現的方式是不需要通過主動註冊的,使用runtime就可以了。另外,註冊部分的程式碼的維護是一個相對麻煩的事情,每一次支援新呼叫時,都要去維護一次註冊列表。如果有呼叫被棄用了,是經常會忘記刪專案的。runtime由於不存在註冊過程,那就也不會產生維護的操作,維護成本就降低了。

由於通過runtime做到了服務的自動發現,拓展呼叫介面的任務就僅在於各自的模組,任何一次新介面新增,新業務新增,都不必去主工程做操作,十分透明。

小總結

蘑菇街採用了openURL的方式來進行App的元件化是一個錯誤的做法,使用註冊的方式發現服務是一個不必要的做法。而且這方案還有其它問題,隨著下文對元件化方案介紹的展開,相信各位自然心裡有數。

正確的元件化方案

先來看一下方案的架構圖

             --------------------------------------
             | [CTMediator sharedInstance]        |
             |                                    |
             |                openUrl:       <<<<<<<<<  (AppDelegate)  <<<<  Call From Other App With URL
             |                                    |
             |                   |                |
             |                   |                |
             |                   |/               |
             |                                    |
             |                parseUrl            |
             |                                    |
             |                   |                |
             |                   |                |
.................................|...............................
             |                   |                |
             |                   |                |
             |                   |/               |
             |                                    |
             |  performTarget:action:params: <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<  Call From Native Module
             |                                    |
             |                   |                |
             |                   |                |
             |                   |                |
             |                   |/               |
             |                                    |
             |             -------------          |
             |             |           |          |
             |             |  runtime  |          |
             |             |           |          |
             |             -------------          |
             |               .       .            |
             ---------------.---------.------------
                           .           .
                          .             .
                         .               .
                        .                 .
                       .                   .
                      .                     .
                     .                       .
                    .                         .
-------------------.-----------      ----------.---------------------
|                 .           |      |          .                   |
|                .            |      |           .                  |
|               .             |      |            .                 |
|              .              |      |             .                |
|                             |      |                              |
|           Target            |      |           Target             |
|                             |      |                              |
|         /   |   \           |      |         /   |   \            |
|        /    |    \          |      |        /    |    \           |
|                             |      |                              |
|   Action Action Action ...  |      |   Action Action Action ...   |
|                             |      |                              |
|                             |      |                              |
|                             |      |                              |
|Business A                   |      | Business B                   |
-------------------------------      --------------------------------

這幅圖是元件化方案的一個簡化版架構描述,主要是基於Mediator模式和Target-Action模式,中間採用了runtime來完成呼叫。這套元件化方案將遠端應用呼叫和本地應用呼叫做了拆分,而且是由本地應用呼叫為遠端應用呼叫提供服務,與蘑菇街方案正好相反。

呼叫方式

先說本地應用呼叫,本地元件A在某處呼叫[[CTMediator sharedInstance] performTarget:targetName action:actionName params:@{...}]CTMediator發起跨元件呼叫,CTMediator根據獲得的target和action資訊,通過objective-C的runtime轉化生成target例項以及對應的action選擇子,然後最終呼叫到目標業務提供的邏輯,完成需求。

在遠端應用呼叫中,遠端應用通過openURL的方式,由iOS系統根據info.plist裡的scheme配置找到可以響應URL的應用(在當前我們討論的上下文中,這就是你自己的應用),應用通過AppDelegate接收到URL之後,呼叫CTMediatoropenUrl:方法將接收到的URL資訊傳入。當然,CTMediator也可以用openUrl:options:的方式順便把隨之而來的option也接收,這取決於你本地業務執行邏輯時的充要條件是否包含option資料。傳入URL之後,CTMediator通過解析URL,將請求路由到對應的target和action,隨後的過程就變成了上面說過的本地應用呼叫的過程了,最終完成響應。

針對請求的路由操作很少會採用本地檔案記錄路由表的方式,服務端經常處理這種業務,在服務端領域基本上都是通過正則表示式來做路由解析。App中做路由解析可以做得簡單點,制定URL規範就也能完成,最簡單的方式就是scheme://target/action這種,簡單做個字串處理就能把target和action資訊從URL中提取出來了。

元件僅通過Action暴露可呼叫介面

所有元件都通過元件自帶的Target-Action來響應,也就是說,模組與模組之間的介面被固化在了Target-Action這一層,避免了實施元件化的改造過程中,對Business的侵入,同時也提高了元件化介面的可維護性。

            --------------------------------
            |                              |
            |           Business A         |
            |                              |
            ---  ----------  ----------  ---
              |  |        |  |        |  |
              |  |        |  |        |  |
   ...........|  |........|  |........|  |...........
   .          |  |        |  |        |  |          .
   .          |  |        |  |        |  |          .
   .        ---  ---    ---  ---    ---  ---        .
   .        |      |    |      |    |      |        .
   .        |action|    |action|    |action|        .
   .        |      |    |      |    |      |        .
   .        ---|----    -----|--    --|-----        .
   .           |             |        |             .
   .           |             |        |             .
   .       ----|------     --|--------|--           .
   .       |         |     |            |           .
   .       |Target_A1|     |  Target_A2 |           .
   .       |         |     |            |           .
   .       -----------     --------------           .
   .                                                .
   .                                                .
   ..................................................

大家可以看到,虛線圈起來的地方就是用於跨元件呼叫的target和action,這種方式避免了由BusinessA直接提供元件間呼叫會增加的複雜度,而且任何元件如果想要對外提供呼叫服務,直接掛上target和action就可以了,業務本身在大多數場景下去進行元件化改造時,是基本不用動的。

複雜引數和非常規引數,以及元件化相關設計思路

這裡我們需要針對術語做一個理解上的統一:

複雜引數是指由普通型別的資料組成的多層級引數。在本文中,我們定義只要是能夠被json解析的型別就都是普通型別,包括NSNumber, NSString, NSArray, NSDictionary,以及相關衍生型別,比如來自系統的NSMutableArray或者你自己定義的都算。

總結一下就是:在本文討論的場景中,複雜引數的定義是由普通型別組成的具有複雜結構的引數。普通型別的定義就是指能夠被json解析的型別。

非常規引數是指由普通型別以外的型別組成的引數,例如UIImage等這些不能夠被json解析的型別。然後這些型別組成的引數在文中就被定義為非常規引數

總結一下就是:非常規引數是包含非常規型別的引數。非常規型別的定義就是不能被json解析的型別都叫非常規型別。

邊界情況:

  • 假設多層級引數中有存在任何一個內容是非常規引數,本文中這種引數就也被認為是非常規引數。
  • 如果某個型別當前不能夠被json解析,但通過某種轉化方式能夠轉化成json,那麼這種型別在場景上下文中,我們也稱為普通型別。

舉個例子就是通過json描述的自定義view。如果這個view能夠通過某個元件被轉化成json,那麼即使這個view本身並不是普通型別,在具有轉化器的上下文場景中,我們依舊認為它是普通型別。

  • 如果上下文場景中沒有轉化器,這個view就是非常規型別了。
  • 假設轉化出的json不能夠被還原成view,比如元件A有轉化器,元件B中沒有轉化器,因此在元件間呼叫過程中json在B元件裡不能被還原成view。在這種呼叫方向中,只要呼叫者能將非常規型別轉化成json的,我們就依然認為這個view是普通型別。如果呼叫者是元件A,轉化器在元件B中,A傳遞view引數時是沒辦法轉化成json的,那麼這個view就被認為是非常規型別,哪怕它在元件B中能夠被轉化成json。

然後我來解釋一下為什麼應該由本地元件間呼叫來支援遠端應用呼叫:

在遠端App呼叫時,遠端App是不可能通過URL來提供非常規引數的,最多隻能以json string的方式經過URLEncode之後再通過GET來提供複雜引數,然後再在本地元件中解析json,最終完成呼叫。在元件間呼叫時,通過performTarget:action:params:是能夠提供非常規引數的,於是我們可以知道,遠端App呼叫時的上下文環境以及功能是本地元件間呼叫時上下文環境以及功能的子集

因此這個邏輯註定了必須由本地元件間呼叫來為遠端App呼叫來提供服務,只有符合這個邏輯的設計思路才是正確的元件化方案的設計思路,其他跟這個不一致的思路一定就是錯的。因為邏輯上子集為父集提供服務說不通,所以強行這麼做的話,用一個成語來總結就叫做倒行逆施。

另外,遠端App呼叫和本地元件間呼叫必須要拆分開,遠端App呼叫只能走CTMediator提供的專用遠端的方法,本地元件間呼叫只能走CTMediator提供的專用本地的方法,兩者不能通過同一個介面來呼叫。

這裡有兩個原因:

  • 遠端App呼叫處理入參的過程比本地多了一個URL解析的過程,這是遠端App呼叫特有的過程。這一點我前面說過,這裡我就不細說了。

  • 架構師沒有充要條件條件可以認為遠端App呼叫對於無響應請求的處理方式和本地元件間呼叫無響應請求的處理方式在未來產品的演進過程中是一致的

在遠端App呼叫中,使用者通過url進入app,當app無法為這個url提供服務時,常見的辦法是展示一個所謂的404介面,告訴使用者"當前沒有相對應的內容,不過你可以在app裡別的地方再逛逛"。這個場景多見於使用者使用的App版本不一致。比如有一個URL只有1.1版本的app能完整響應,1.0版本的app雖然能被喚起,但是無法完成整個響應過程,那麼1.0的app就要展示一個404了。

在元件間呼叫中,如果遇到了無法響應的請求,就要分兩種場景考慮了。

場景1


如果這種無法響應的請求發生場景是在開發過程中,比如兩個元件同時在開發,元件A呼叫元件B時,元件B還處於舊版本沒有釋出新版本,因此響應不了,那麼這時候的處理方式可以相對隨意,只要能體現B模組是舊版本就行了,最後在RC階段統測時是一定能夠發現的,只要App沒發版,怎麼處理都來得及。

場景2


如果這種無法響應的請求發生場景是在已釋出的App中,有可能展示個404就結束了,那這就跟遠端App呼叫時的404處理場景一樣。但也有可能需要為此做一些額外的事情,有可能因為做了額外的事情,就不展示404了,展示別的頁面了,這一切取決於產品經理。

那麼這種場景是如何發生的呢?

我舉一個例子:當用戶在1.0版本時收藏了一個東西,然後使用者升級App到1.1版本。1.0版本的收藏專案在本地持久層存入的資料有可能是會跟1.1版本收藏時存入的資料是不一致的。此時使用者在1.1版本的app中對1.0版本收藏的東西做了一些操作,觸發了本地元件間呼叫,這個本地間呼叫又與收藏專案本身的資料相關,那麼這時這個呼叫就是有可能變成無響應呼叫,此時的處理方式就不見得跟以前一樣展示個404頁面就結束了,因為使用者已經看到了收藏了的東西,結果你還告訴他找不到,使用者立刻懵逼。。。這時候的處理方式就會用很多種,至於產品經理會選擇哪種,你作為架構師是沒有辦法預測的。如果產品經理提的需求落實到架構上,對呼叫入口產生要求然而你的架構又沒有拆分呼叫入口,對於你的選擇就只有兩個:要麼打回產品需求,要麼加個班去拆分呼叫入口。

當然,架構師可以選擇打回產品經理的需求,最終挑選一個自己的架構能夠承載的需求。但是,如果這種是因為你早期設計架構時挖的坑而打回的產品需求,你不覺得丟臉麼?

鑑於遠端app呼叫和本地元件間呼叫下的無響應請求處理方式不同,以及未來不可知的產品演進,拆分遠端app呼叫入口和本地元件間呼叫入口是功在當代利在千秋的事情。

元件化方案中的去model設計

元件間呼叫時,是需要針對引數做去model化的。如果元件間呼叫不對引數做去model化的設計,就會導致業務形式上被元件化了,實質上依然沒有被獨立

假設模組A和模組B之間採用model化的方案去呼叫,那麼呼叫方法時傳遞的引數就會是一個物件。

如果物件不是一個面向介面的通用物件,那麼mediator的引數處理就會非常複雜,因為要區分不同的物件型別。如果mediator不處理引數,直接將物件以範型的方式轉交給模組B,那麼模組B必然要包含物件型別的宣告。假設物件宣告放在模組A,那麼B和A之間的元件化只是個形式主義。如果物件型別宣告放在mediator,那麼對於B而言,就不得不依賴mediator。但是,大家可以從上面的架構圖中看到,對於響應請求的模組而言,依賴mediator並不是必要條件,因此這種依賴是完全不需要的,這種依賴的存在對於架構整體而言,是一種汙染。

如果引數是一個面向介面的物件,那麼mediator對於這種引數的處理其實就沒必要了,更多的是直接轉給響應方的模組。而且介面的定義就不可能放在發起方的模組中了,只能放在mediator中。響應方如果要完成響應,就也必須要依賴mediator,然而前面我已經說過,響應方對於mediator的依賴是不必要的,因此引數其實也並不適合以面向介面的物件的方式去傳遞。

因此,使用物件化的引數無論是否面向介面,帶來的結果就是業務模組形式上是被元件化了,但實質上依然沒有被獨立。

在這種跨模組場景中,引數最好還是以去model化的方式去傳遞,在iOS的開發中,就是以字典的方式去傳遞。這樣就能夠做到只有呼叫方依賴mediator,而響應方不需要依賴mediator。然而在去model化的實踐中,由於這種方式自由度太大,我們至少需要保證呼叫方生成的引數能夠被響應方理解,然而在元件化場景中,限制去model化方案的自由度的手段,相比於網路層和持久層更加容易得多。

因為元件化天然具備了限制手段:引數不對就無法呼叫!無法呼叫時直接debug就能很快找到原因。所以接下來要解決的去model化方案的另一個問題就是:如何提高開發效率。

在去model的元件化方案中,影響效率的點有兩個:呼叫方如何知道接收方需要哪些key的引數?呼叫方如何知道有哪些target可以被呼叫?其實後面的那個問題不管是不是去model的方案,都會遇到。為什麼放在一起說,因為我接下來要說的解決方案可以把這兩個問題一起解決。

解決方案就是使用category

mediator這個repo維護了若干個針對mediator的category,每一個對應一個target,每個category裡的方法對應了這個target下所有可能的呼叫場景,這樣呼叫者在包含mediator的時候,自動獲得了所有可用的target-action,無論是呼叫還是引數傳遞,都非常方便。接下來我要解釋一下為什麼是category而不是其他:

  • category本身就是一種組合模式,根據不同的分類提供不同的方法,此時每一個元件就是一個分類,因此把每個元件可以支援的呼叫用category封裝是很合理的。

  • 在category的方法中可以做到引數的驗證,在架構中對於保證引數安全是很有必要的。當引數不對時,category就提供了補救的入口。

  • category可以很輕鬆地做請求轉發,如果不採用category,請求轉發邏輯就非常難做了。

  • category統一了所有的元件間呼叫入口,因此無論是在除錯還是原始碼閱讀上,都為工程師提供了極大的方便。

  • 由於category統一了所有的呼叫入口,使得在跨模組呼叫時,對於param的hardcode在整個App中的作用域僅存在於category中,在這種場景下的hardcode就已經變成和呼叫巨集或者呼叫宣告沒有任何區別了,因此是可以接受的。

這裡是業務方使用category呼叫時的場景,大家可以看到非常方便,不用去記URL也不用糾結到底應該傳哪些引數。

    if (indexPath.row == 0) {
        UIViewController *viewController = [[CTMediator sharedInstance] CTMediator_viewControllerForDetail];

        // 獲得view controller之後,在這種場景下,到底push還是present,其實是要由使用者決定的,mediator只要給出view controller的例項就好了
        [self presentViewController:viewController animated:YES completion:nil];
    }

    if (indexPath.row == 1) {
        UIViewController *viewController = [[CTMediator sharedInstance] CTMediator_viewControllerForDetail];
        [self.navigationController pushViewController:viewController animated:YES];
    }

    if (indexPath.row == 2) {
        // 這種場景下,很明顯是需要被present的,所以不必返回例項,mediator直接present了
        [[CTMediator sharedInstance] CTMediator_presentImage:[UIImage imageNamed:@"image"]];
    }

    if (indexPath.row == 3) {
        // 這種場景下,引數有問題,因此需要在流程中做好處理
        [[CTMediator sharedInstance] CTMediator_presentImage:nil];
    }

    if (indexPath.row == 4) {
        [[CTMediator sharedInstance] CTMediator_showAlertWithMessage:@"casa" cancelAction:nil confirmAction:^(NSDictionary *info) {
            // 做你想做的事
            NSLog(@"%@", info);
        }];
    }

本文對應的demo展示瞭如何使用category來實現去model的元件呼叫。上面的程式碼片段也是摘自這個demo。

基於其他考慮還要再做的一些額外措施

基於安全考慮

我們需要防止黑客通過URL的方式呼叫本屬於native的元件,比如支付寶的個人財產頁面。如果在呼叫層級上沒有區分好,沒有做好安全措施,黑客就有通過safari檢視任何人的個人財產的可能。

安全措施其實有很多,大部分取決於App本身以及產品的要求。在架構層面要做的最基礎的一點就是區分呼叫是來自於遠端App還是本地元件,我在demo中的安全措施是採用給action新增native字首去做的,凡是帶有native字首的就都只允許本地元件呼叫,如果在url階段發現呼叫了字首為native的方法,那就可以採取響應措施了。這也是將遠端app呼叫入口和本地元件呼叫入口區分開來的重要原因之一。

當然,為了確保安全的做法有很多,但只要拆出遠端呼叫和本地呼叫,各種做法就都有施展的空間了。

基於動態排程考慮

動態排程的意思就是,今天我可能這個跳轉是要展示A頁面,但是明天可能同樣的跳轉就要去展示B頁面了。這個跳轉有可能是來自於本地元件間跳轉也有可能是來自於遠端app。

做這個事情的切點在本文架構中,有很多個:

  1. 以url parse為切點
  2. 以例項化target時為切點
  3. 以category排程方法為切點
  4. 以target下的action為切點

如果以url parse為切點的話,那麼這個動態排程就只能夠對遠端App跳轉產生影響,失去了動態排程本地跳轉的能力,因此是不適合的。

如果以例項化target時為切點的話,就需要在程式碼中針對所有target都做一次審查,看是否要被排程,這是沒必要的。假設10個呼叫請求中,只有1個要被動態排程,那麼就必須要審查10次,只有那1次審查通過了,才走動態排程,這是一種相對比較粗暴的方法。

如果以category排程方法為切點的話,那動態排程就只能影響到本地件元件的跳轉,因為category是隻有本地才用的,所以也不適合。

以target下的action為切點是最適合的,因為動態排程在一般場景下都是有範圍的,大多數是活動頁需要動態排程,今天這個活動明天那個活動,或者今天活動正在進行明天活動就結束了,所以產生動態排程的需求。我們在可能產生動態排程的action中審查當前action是否需要被動態排程,在常規排程中就沒必要審查了,例如個人主頁的跳轉,商品詳情的跳轉等,這樣效率就能比較高。

大家會發現,如果要做類似這種效率更高的動態排程,target-action層被抽象出來就是必不可少的,然而蘑菇街並沒有抽象出target-action層,這也是其中的一個問題。

當然,如果你的產品要求所有頁面都是存在動態排程需求的,那就還是以例項化target時為切點去排程了,這樣能做到審查每一次排程請求,從而實現動態排程。

說完了排程切點,接下來要說的就是如何完成審查流程。完整的審查流程有幾種,我每個都列舉一下:

  1. App啟動時下載排程列表,或者定期下載排程列表。然後審查時檢查當前action是否存在要被動態排程跳轉的action,如果存在,則跳轉到另一個action
  2. 每一次到達新的action時,以action為引數呼叫API獲知是否需要被跳轉,如果需要被跳轉,則API告知要跳轉的action,然後再跳轉到API指定的action

這兩種做法其實都可以,如果產品對即時性的要求比較高,那麼採用第二種方案,如果產品對即時性要求不那麼高,第一種方案就可以了。由於本文的方案是沒有URL註冊列表的,因此伺服器只要給出原始target-action和對應跳轉的target-action就可以了,整個流程不是隻有註冊URL列表才能達成的,而且這種方案比註冊URL列表要更易於維護一些。

另外,說採用url rewrite的手段來進行動態排程,也不是不可以。但是這裡我需要辨析的是,URL的必要性僅僅體現在遠端App排程中,是沒必要蔓延到本地元件間呼叫的。這樣,當我們做遠端App的URL路由時(目前的demo沒有提供URL路由功能,但是提供了URL路由操作的接入點,可以根據業務需求插入這個功能),要關心的事情就能少很多,可以比較乾淨。在這種場景下,單純以URL rewrite的方式其實就與上文提到的以url parse為切點沒有區別了。

相比之下,蘑菇街的元件化方案有以下缺陷

  • 蘑菇街沒有拆分遠端呼叫和本地間呼叫

不拆分遠端呼叫和本地間呼叫,就使得後續很多手段難以實施,這個我在前文中都已經有論述了。另外再補充一下,這裡的拆分不是針對來源做拆分。比如通過URL來區分是遠端App呼叫還是本地呼叫,這只是區分了呼叫者的來源。

這裡說的區分是指:遠端呼叫走遠端呼叫路徑,也就是openUrl->urlParse->perform->target-action。本地元件間呼叫就走本地元件間呼叫路徑:perform->target-action。這兩個是一定要作區分的,蘑菇街方案並沒有對此做好區分。

  • 蘑菇街以遠端呼叫的方式為本地間呼叫提供服務

這是本末倒置的做法,倒行逆施導致的是未來架構難以為業務發展提供支撐。因為前面已經論述過,在iOS場景下,遠端呼叫的實現是本地呼叫實現的子集,只有大的為小提供服務,也就是本地呼叫為遠端呼叫提供服務,如果反過來就是倒行逆施了。

  • 蘑菇街的本地間呼叫無法傳遞非常規引數,複雜引數的傳遞方式非常醜陋

注意這裡複雜引數非常規引數的辨析。

由於採用遠端呼叫的方式執行本地呼叫,在前面已經論述過兩者功能集的關係,因此這種做法無法滿足傳遞非常規引數的需求。而且如果基於這種方式不變的話,複雜引數的傳遞也只能依靠經過urlencode的json string進行,這種方式非常醜陋,而且也不便於除錯。

  • 蘑菇街必須要在app啟動時註冊URL響應者

這個條件在元件化方案中是不必要條件,demo也已經證實了這一點。這個不必要的操作會導致不必要的維護成本,如果單純從