蘑菇街 App 的元件化之路
在元件化之前,蘑菇街 App 的程式碼都是在一個工程裡開發的,在人比較少,業務發展不是很快的時候,這樣是比較合適的,能一定程度地保證開發效率。
慢慢地程式碼量多了起來,開發人員也多了起來,業務發展也快了起來,這時單一工程開發模式就會顯露出一些弊端
- 耦合比較嚴重(因為沒有明確的約束,「元件」間引用的現象會比較多)
- 容易出現衝突(尤其是使用 Xib,還有就是 Xcode Project,雖說有 指令碼 可以改善)
- 業務方的開發效率不夠高(只關心自己的元件,卻要編譯整個專案,與其他不相干的程式碼糅合在一起)
為了解決這些問題,就採取了「元件化」策略。它能帶來這些好處
- 加快編譯速度(不用編譯主客那一大坨程式碼了)
- 自由選擇開發姿勢(MVC / MVVM / FRP)
- 方便 QA 有針對性地測試
- 提高業務開發效率
先來看下,元件化之後的一個大概架構
「元件化」顧名思義就是把一個大的 App 拆成一個個小的元件,相互之間不直接引用。那如何做呢?
實現方式
元件間通訊
以 iOS 為例,由於之前就是採用的 URL 跳轉模式,理論上頁面之間的跳轉只需 open 一個 URL 即可。所以對於一個元件來說,只要定義「支援哪些 URL」即可,比如詳情頁,大概可以這麼做的
[MGJRouter registerURLPattern:@"mgj://detail?id=:id" toHandler:^(NSDictionary *routerParameters) { NSNumber *id = routerParameters[@"id"]; // create view controller with id // push view controller }];
首頁只需呼叫 [MGJRouter openURL:@"mgj://detail?id=404"] 就可以開啟相應的詳情頁。
那問題又來了,我怎麼知道有哪些可用的 URL?為此,我們做了一個後臺專門來管理。
然後可以把這些短鏈生成不同平臺所需的檔案,iOS 平臺生成 .{h,m} 檔案,Android 平臺生成 .java 檔案,並注入到專案中。這樣開發人員只需在專案中開啟該檔案就知道所有的可用 URL 了。
目前還有一塊沒有做,就是引數這塊,雖然描述了短鏈,但真想要生成完整的 URL,還需要知道如何傳引數,這個正在開發中。
還有一種情況會稍微麻煩點,就是「元件A」要呼叫「元件B」的某個方法,比如在商品詳情頁要展示購物車的商品數量,就涉及到向購物車元件拿資料。
類似這種同步呼叫,iOS 之前採用了比較簡單的方案,還是依託於 MGJRouter ,不過新增了新的方法 – (id)objectForURL: ,註冊時也使用新的方法進行註冊
[MGJRouter registerURLPattern:@"mgj://cart/ordercount" toObjectHandler:^id(NSDictionary *routerParamters){ // do some calculation return @42; }]
使用時 NSNumber *orderCount = [MGJRouter objectForURL:@"mgj://cart/ordercount"] 這樣就拿到了購物車裡的商品數。
稍微複雜但更具通用性的方法是使用「協議」 <-> 「類」繫結的方式,還是以購物車為例,購物車元件可以提供這麼個 Protocol
@protocol MGJCart <NSObject> + (NSInteger)orderCount; @end
可以看到通過協議可以直接指定返回的資料型別。然後在購物車元件內再新建個類實現這個協議,假設這個類名為 MGJCartImpl ,接著就可以把它與協議關聯起來 [ModuleManager registerClass:MGJCartImpl forProtocol:@protocol(MGJCart)] ,對於使用方來說,要拿到這個 MGJCartImpl ,需要呼叫 [ModuleManager classForProtocol:@protocol(MGJCart)] 。拿到之後再呼叫 + (NSInteger)orderCount 就可以了。
那麼,這個協議放在哪裡比較合適呢?如果跟元件放在一起,使用時還是要先引入元件,如果有多個這樣的元件就會比較麻煩了。所以我們把這些公共的協議統一放到了 PublicProtocolDomain.h 下,到時只依賴這一個檔案就可以了。
Android 也是採用類似的方式。
元件生命週期管理
理想中的元件可以很方便地整合到主客中,並且有跟 AppDelegate 一致的回撥方法。這也是 ModuleManager 做的事情。
先來看看現在的入口方法
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { [MGJApp startApp]; [[ModuleManager sharedInstance] loadModuleFromPlist:[[NSBundle mainBundle] pathForResource:@"modules" ofType:@"plist"]]; NSArray *modules = [[ModuleManager sharedInstance] allModules]; for (id<ModuleProtocol> module in modules) { if ([module respondsToSelector:_cmd]) { [module application:application didFinishLaunchingWithOptions:launchOptions]; } } [self trackLaunchTime]; return YES; }
其中 [MGJApp startApp] 主要負責一些 SDK 的初始化。 [self trackLaunchTime] 是我們打的一個點,用來監測從 main 方法開始到入口方法呼叫結束花了多長時間。其他的都由 ModuleManager 搞定, loadModuleFromPlist:pathForResource: 方法會讀取 bundle 裡的一個 plist 檔案,這個檔案的內容大概是這樣的
每個 Module 都實現了 ModuleProtocol ,其中有一個 – (BOOL)applicaiton:didFinishLaunchingWithOptions: 方法,如果實現了的話,就會被呼叫。
還有一個問題就是,系統的一些事件會有通知,比如 applicationDidBecomeActive 會有對應的 UIApplicationDidBecomeActiveNotification ,元件如果要做響應的話,只需監聽這個系統通知即可。但也有一些事件是沒有通知的,比如 – application:didRegisterUserNotificationSettings: ,這時元件如果也要做點事情,怎麼辦?
一個簡單的解決方法是在 AppDelegate 的各個方法裡,手動調一遍元件的對應的方法,如果有就執行。
- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken { NSArray *modules = [[ModuleManager sharedInstance] allModules]; for (id<ModuleProtocol> module in modules) { if ([module respondsToSelector:_cmd]) { [module application:application didRegisterForRemoteNotificationsWithDeviceToken:deviceToken]; } } }
殼工程
既然已經拆出去了,那拆出去的元件總得有個載體,這個載體就是殼工程,殼工程主要包含一些基礎元件和業務SDK,這也是主工程包含的一些內容,所以如果在殼工程可以正常執行的話,到了主工程也沒什麼問題。不過這裡存在版本同步問題,之後會說到。
遇到的問題
元件拆分
由於之前的程式碼都是在一個工程下的,所以要單獨拿出來作為一個元件就會遇到不少問題。首先是元件的劃分,當時在定義元件粒度時也花了些時間討論,究竟是粒度粗點好,還是細點好。粗點的話比較有利於拆分,細點的話靈活度比較高。最終還是選擇粗一點的粒度,先拆出來再說。
假如要把詳情頁遷出來,就會發現它依賴了一些其他部分的程式碼,那最快的方式就是直接把程式碼拷過來,改個名使用。比較簡單暴力。說起來比較簡單,做的時候也是挺有挑戰的,因為正常的業務並不會因為「元件化」而停止,所以開發同學們需要同時兼顧正常的業務和元件的拆分。
版本管理
我們的元件包括第三方庫都是通過 Cocoapods 來管理的,其中元件使用了私有庫。之所以選擇 Cocoapods,一個是因為它比較方便,還有就是使用者基數比較大,且社群也比較活躍(活躍到了會時不時地觸發 Github 的 rate limit,導致長時間 clone 不下來··· 見此 ),當然也有其他的管理方式,比如 submodule / subtree,在開發人員比較多的情況下,方便、靈活的方案容易佔上風,雖然它也有自己的問題。主要有版本同步和更新/編譯慢的問題。
假如基礎元件做了個 API 介面升級,這個升級會對原有的介面做改動,自然就會升一箇中位的版本號,比如原先是 1.6.19,那麼現在就變成 1.7.0 了。而我們在 Podfile 裡都是用 ~ 指定的,這樣就會出現主工程的 pod 版本升上去了,但是殼工程沒有同步到,然後群裡就會各種反饋編譯不過,而且這個編譯不過的長尾有時能拖上兩三天。
然後我們就想了個辦法,如果不在殼工程裡指定基礎庫的版本,只在主工程裡指定呢,理論上應該可行,只要不出現某個基礎庫要同時維護多個版本的情況。但實踐中發現,殼工程有時會莫名其妙地升不上去,在 podfile 裡指定最新的版本又可以升上去,所以此路不通。
還有一個問題是 pod update 時間過長,經常會在 Analyzing Dependency 上卡 10 多分鐘,非常影響效率。後來排查下來是跟元件的 Podspec 有關,配置了 subspec,且依賴比較多。
然後就是 pod update 之後的編譯,由於是原始碼編譯,所以這塊的時間花費也不少,接下去會考慮 framework 的方式。
持續整合
在剛開始,持續整合還不是很完善,業務方升級元件,直接把 podspec 扔到 private repo 裡就完事了。這樣最簡單,但也經常會帶來編譯通不過的問題。而且這種隨意的版本升級也不太能保證質量。於是我們就搭建了一套持續整合系統,大概如此
每個元件升級之前都需要先通過編譯,然後再決定是否升級。這套體系看起來不復雜,但在實施過程中經常會遇到後端的併發問題,導致業務方要麼整合失敗,要麼要等不少時間。而且也沒有一個地方可以呈現當前版本的元件版本資訊。還有就是業務方對於這種命令列的升級方式接受度也不是很高。
基於此,在經過了幾輪討論之後,有了新版的持續整合平臺,升級操作通過網頁端來完成。
大致思路是,業務方如果要升級元件,假設現在的版本是 0.1.7,新增了一些 feature 之後,殼工程測試通過,想整合到主工程裡看看效果,或者其他元件也想引用這個最新的,就可以在後臺手動把版本升到 0.1.8-rc.1,這樣的話,原先依賴 ~> 0.1.7 的元件,不會升到 0.1.8,同時想要測試這個元件的話,只要手動把版本調到 0.1.8-rc.1 就可以了。這個過程不會觸發 CI 的編譯檢查。
當測試通過後,就可以把尾部的 -rc.n 去掉,然後點選「整合」,就會走 CI 編譯檢查,通過的話,會在主工程的 podfile 裡寫上固定的版本號 0.1.8。也就是說,podfile 裡所有的元件版本號都是固定的。
周邊設施
基礎元件及元件的文件 / Demo / 單元測試
無線基礎的職能是為集團提供解決方案,只是在蘑菇街 App 裡能 work 是遠遠不夠的,所以就需要提供入口,知道有哪些可用元件,並且如何使用,就像這樣(目前還未實現)
這就要求元件的負責人需要及時地更新 README / CHANGELOG / API,並且當發生 API 變更時,能夠快速通知到使用方。
公共 UI 元件
元件化之後還有一個問題就是資源的重複性,以前在一個工程裡的時候,資源都可以很方便地拿到,現在獨立出去了,也不知道哪些是公用的,哪些是獨有的,索性都放到自己的元件裡,這樣就會導致包變大。還有一個問題是每個元件可能是不同的產品經理在跟,而他們很可能只關注於自己關心的頁面長什麼樣,而忽略了整體的樣式。公共 UI 元件就是用來解決這些問題的,這些元件甚至可以跨 App 使用。(目前還未實現)
小結
「元件化」是 App 膨脹到一定體積後的解決方案,能一定程度上解決問題,在提高開發效率的過程中,採坑是難免的,希望這篇文章能夠帶來些幫助。
相關文章
- 蘑菇街一鍵搬家的工具
- 蘑菇街TeamTalk服務端分析服務端
- 蘑菇街前端電話一面前端
- 蘑菇街SRE體系建設實踐
- 前端面試(2)之蘑菇街一面前端面試
- Thinkphp仿蘑菇街商城版(b2c)PHP
- Thinkphp仿蘑菇街商城版(c2c)PHP
- iOS實習面經(位元組美團阿里蘑菇街)iOS阿里
- 來自滬江、滴滴、蘑菇街架構師的 DOCKER 實踐分享架構Docker
- iOS的元件化(模組化)之路iOS元件化
- 對話:一個工程師在蘑菇街4年的架構感悟工程師架構
- Android 元件化之路Android元件化
- 蘑菇街釋出2020年財報,佣金收入佔比過半
- 吐槽前端元件化的踩坑之路前端元件化
- 聚美元件化實踐之路元件化
- “蘑菇街裁員”上熱搜:工作上這3件事,你越早明白越好!
- 《黃昏沉眠街》開發者訪談:從插畫師到獨立遊戲製作人的進化之路遊戲
- 蘑菇街2021財年第二季度營收1.13億元 同比下滑43%營收
- 站長網播報:傳蘑菇街美麗說將合併 微信整頓影視智慧財產權
- 《Among Us》養活了海內外幾條街的AppAPP
- Apple Watch學習之路 基礎控制元件學習APP控制元件
- [Android]Gank 元件化例項AppAndroid元件化APP
- [Android元件化]Android app BundleAndroid元件化APP
- Architecture(5)電商APP元件化探索APP元件化
- AndroidApp圖片輪播效果的元件化AndroidAPP元件化
- 07:蘑菇的前身-蘑菇小方塊的實現#python遊戲程式設計#紅傘傘Python遊戲程式設計
- 2018,我們的元件化實施之路 | 掘金年度徵文元件化
- 京東App Swift 混編及元件化落地APPSwift元件化
- flutter的進階之路之常用元件Flutter元件
- 肯德基的遊戲化之路遊戲
- Appium自動化(7) - 控制元件定位工具之Appium 的 InspectorAPP控制元件
- 解決元件化中 ModuleApplication 共存問題元件化APP
- 移動端APP元件化架構實踐APP元件化架構
- OAuth 2.1 的進化之路OAuth
- 一款基於Kotlin+MVP+元件化的麻雀AppKotlinMVP元件化APP
- vue元件之路之輪播圖的實現Vue元件
- uniAPP開發的採坑之路(1)APP
- uniAPP開發的採坑之路(2)APP