蘑菇街 App 的組件化之路

作者:limboy
文章源自:http://limboy.me/ios/2016/03/10/mgj-components.html

在組件化之前,蘑菇街 App 的代碼都是在一個工程里開發(fā)的,在人比較少,業(yè)務(wù)發(fā)展不是很快的時候耍攘,這樣是比較合適的,能一定程度地保證開發(fā)效率。

慢慢地代碼量多了起來藏杖,開發(fā)人員也多了起來,業(yè)務(wù)發(fā)展也快了起來脉顿,這時單一工程開發(fā)模式就會顯露出一些弊端:

  • 耦合比較嚴(yán)重(因?yàn)闆]有明確的約束蝌麸,「組件」間引用的現(xiàn)象會比較多);
  • 容易出現(xiàn)沖突(尤其是使用 Xib艾疟,還有就是 Xcode Project来吩,雖說有腳本可以改善);
  • 業(yè)務(wù)方的開發(fā)效率不夠高(只關(guān)心自己的組件蔽莱,卻要編譯整個項(xiàng)目弟疆,與其他不相干的代碼糅合在一起)。

為了解決這些問題盗冷,就采取了「組件化」策略怠苔。它能帶來這些好處:

  • 加快編譯速度(不用編譯主客那一大坨代碼了);
  • 自由選擇開發(fā)姿勢(MVC / MVVM / FRP)正塌;
  • 方便 QA 有針對性地測試嘀略;
  • 提高業(yè)務(wù)開發(fā)效率。

先來看下乓诽,組件化之后的一個大概架構(gòu):

「組件化」顧名思義就是把一個大的 App 拆成一個個小的組件帜羊,相互之間不直接引用。那如何做呢鸠天?

實(shí)現(xiàn)方式

組件間通信

以 iOS 為例讼育,由于之前就是采用的 URL 跳轉(zhuǎn)模式,理論上頁面之間的跳轉(zhuǎn)只需 open 一個 URL 即可。所以對于一個組件來說奶段,只要定義「支持哪些 URL」即可饥瓷,比如詳情頁,大概可以這么做的:

[MGJRouter registerURLPattern:@"mgj://detail?id=:id" toHandler:^(NSDictionary *routerParameters) {
    NSNumber *id = routerParameters[@"id"];
    // create view controller with id
    // push view controller
}];

首頁只需調(diào)用 [MGJRouter openURL:@"mgj://detail?id=404"] 就可以打開相應(yīng)的詳情頁痹籍。

那問題又來了呢铆,我怎么知道有哪些可用的 URL?為此蹲缠,我們做了一個后臺專門來管理棺克。

然后可以把這些短鏈生成不同平臺所需的文件,iOS 平臺生成 .{h,m} 文件线定,Android 平臺生成 .java 文件娜谊,并注入到項(xiàng)目中。這樣開發(fā)人員只需在項(xiàng)目中打開該文件就知道所有的可用 URL 了斤讥。

目前還有一塊沒有做纱皆,就是參數(shù)這塊,雖然描述了短鏈芭商,但真想要生成完整的 URL派草,還需要知道如何傳參數(shù),這個正在開發(fā)中蓉坎。

還有一種情況會稍微麻煩點(diǎn)澳眷,就是「組件A」要調(diào)用「組件B」的某個方法,比如在商品詳情頁要展示購物車的商品數(shù)量蛉艾,就涉及到向購物車組件拿數(shù)據(jù)钳踊。

類似這種同步調(diào)用,iOS 之前采用了比較簡單的方案勿侯,還是依托于 MGJRouter拓瞪,不過添加了新的方法 - (id)objectForURL:,注冊時也使用新的方法進(jìn)行注冊:


[MGJRouter registerURLPattern:@"mgj://cart/ordercount" toObjectHandler:^id(NSDictionary *routerParamters){
    // do some calculation
    return @42;
}]

使用時 NSNumber *orderCount = [MGJRouter objectForURL:@"mgj://cart/ordercount"]這樣就拿到了購物車?yán)锏纳唐窋?shù)助琐。

稍微復(fù)雜但更具通用性的方法是使用「協(xié)議」 <-> 「類」綁定的方式祭埂,還是以購物車為例,購物車組件可以提供這么個 Protocol:

@protocol MGJCart <NSObject>
+ (NSInteger)orderCount;
@end

可以看到通過協(xié)議可以直接指定返回的數(shù)據(jù)類型兵钮。然后在購物車組件內(nèi)再新建個類實(shí)現(xiàn)這個協(xié)議蛆橡,假設(shè)這個類名為MGJCartImpl,接著就可以把它與協(xié)議關(guān)聯(lián)起來[ModuleManager registerClass:MGJCartImpl forProtocol:@protocol(MGJCart)]掘譬,對于使用方來說泰演,要拿到這個MGJCartImpl,需要調(diào)用[ModuleManager classForProtocol:@protocol(MGJCart)]葱轩。拿到之后再調(diào)用 + (NSInteger)orderCount就可以了睦焕。

那么藐握,這個協(xié)議放在哪里比較合適呢?如果跟組件放在一起垃喊,使用時還是要先引入組件猾普,如果有多個這樣的組件就會比較麻煩了。所以我們把這些公共的協(xié)議統(tǒng)一放到了 PublicProtocolDomain.h下本谜,到時只依賴這一個文件就可以了初家。

Android 也是采用類似的方式。

組件生命周期管理

理想中的組件可以很方便地集成到主客中耕突,并且有跟 AppDelegate一致的回調(diào)方法笤成。這也是 ModuleManager做的事情。

先來看看現(xiàn)在的入口方法:


- (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]主要負(fù)責(zé)一些 SDK 的初始化眷茁。[self trackLaunchTime]是我們打的一個點(diǎn),用來監(jiān)測從 main方法開始到入口方法調(diào)用結(jié)束花了多長時間纵诞。其他的都由 ModuleManager搞定上祈,loadModuleFromPlist:pathForResource:方法會讀取 bundle 里的一個 plist 文件,這個文件的內(nèi)容大概是這樣的:

每個Module都實(shí)現(xiàn)了ModuleProtocol浙芙,其中有一個-(BOOL)applicaiton:didFinishLaunchingWithOptions:方法登刺,如果實(shí)現(xiàn)了的話,就會被調(diào)用嗡呼。

還有一個問題就是纸俭,系統(tǒng)的一些事件會有通知,比如 applicationDidBecomeActive會有對應(yīng)的UIApplicationDidBecomeActiveNotification南窗,組件如果要做響應(yīng)的話揍很,只需監(jiān)聽這個系統(tǒng)通知即可。但也有一些事件是沒有通知的万伤,比如 - application:didRegisterUserNotificationSettings:窒悔,這時組件如果也要做點(diǎn)事情,怎么辦敌买?

一個簡單的解決方法是在AppDelegate的各個方法里简珠,手動調(diào)一遍組件的對應(yīng)的方法,如果有就執(zhí)行虹钮。

- (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];
        }
    }
}

殼工程

既然已經(jīng)拆出去了聋庵,那拆出去的組件總得有個載體,這個載體就是殼工程芙粱,殼工程主要包含一些基礎(chǔ)組件和業(yè)務(wù)SDK祭玉,這也是主工程包含的一些內(nèi)容,所以如果在殼工程可以正常運(yùn)行的話宅倒,到了主工程也沒什么問題攘宙。不過這里存在版本同步問題屯耸,之后會說到。

遇到的問題

組件拆分

由于之前的代碼都是在一個工程下的蹭劈,所以要單獨(dú)拿出來作為一個組件就會遇到不少問題疗绣。首先是組件的劃分,當(dāng)時在定義組件粒度時也花了些時間討論铺韧,究竟是粒度粗點(diǎn)好多矮,還是細(xì)點(diǎn)好。粗點(diǎn)的話比較有利于拆分哈打,細(xì)點(diǎn)的話靈活度比較高塔逃。最終還是選擇粗一點(diǎn)的粒度,先拆出來再說料仗。

假如要把詳情頁遷出來湾盗,就會發(fā)現(xiàn)它依賴了一些其他部分的代碼,那最快的方式就是直接把代碼拷過來立轧,改個名使用格粪。比較簡單暴力。說起來比較簡單氛改,做的時候也是挺有挑戰(zhàn)的帐萎,因?yàn)檎5臉I(yè)務(wù)并不會因?yàn)椤附M件化」而停止,所以開發(fā)同學(xué)們需要同時兼顧正常的業(yè)務(wù)和組件的拆分胜卤。

版本管理

我們的組件包括第三方庫都是通過 Cocoapods 來管理的疆导,其中組件使用了私有庫。之所以選擇 Cocoapods葛躏,一個是因?yàn)樗容^方便澈段,還有就是用戶基數(shù)比較大,且社區(qū)也比較活躍(活躍到了會時不時地觸發(fā) Github 的 rate limit紫新,導(dǎo)致長時間 clone 不下來··· 見此)均蜜,當(dāng)然也有其他的管理方式,比如 submodule / subtree芒率,在開發(fā)人員比較多的情況下囤耳,方便、靈活的方案容易占上風(fēng)偶芍,雖然它也有自己的問題充择。主要有版本同步和更新/編譯慢的問題。

假如基礎(chǔ)組件做了個 API 接口升級匪蟀,這個升級會對原有的接口做改動椎麦,自然就會升一個中位的版本號,比如原先是 1.6.19材彪,那么現(xiàn)在就變成 1.7.0 了观挎。而我們在 Podfile 里都是用 ~

指定的琴儿,這樣就會出現(xiàn)主工程的 pod 版本升上去了,但是殼工程沒有同步到嘁捷,然后群里就會各種反饋編譯不過造成,而且這個編譯不過的長尾有時能拖上兩三天。

然后我們就想了個辦法雄嚣,如果不在殼工程里指定基礎(chǔ)庫的版本晒屎,只在主工程里指定呢,理論上應(yīng)該可行缓升,只要不出現(xiàn)某個基礎(chǔ)庫要同時維護(hù)多個版本的情況鼓鲁。但實(shí)踐中發(fā)現(xiàn),殼工程有時會莫名其妙地升不上去港谊,在 podfile 里指定最新的版本又可以升上去骇吭,所以此路不通。

還有一個問題是pod update時間過長歧寺,經(jīng)常會在Analyzing Dependency上卡 10 多分鐘绵跷,非常影響效率。后來排查下來是跟組件的 Podspec 有關(guān)成福,配置了 subspec,且依賴比較多荆残。

然后就是 pod update 之后的編譯奴艾,由于是源碼編譯,所以這塊的時間花費(fèi)也不少内斯,接下去會考慮 framework 的方式蕴潦。

持續(xù)集成

在剛開始,持續(xù)集成還不是很完善俘闯,業(yè)務(wù)方升級組件潭苞,直接把 podspec 扔到 private repo 里就完事了累贤。這樣最簡單响逢,但也經(jīng)常會帶來編譯通不過的問題。而且這種隨意的版本升級也不太能保證質(zhì)量士聪。于是我們就搭建了一套持續(xù)集成系統(tǒng)遮婶,大概如此:

每個組件升級之前都需要先通過編譯蝗碎,然后再決定是否升級。這套體系看起來不復(fù)雜旗扑,但在實(shí)施過程中經(jīng)常會遇到后端的并發(fā)問題蹦骑,導(dǎo)致業(yè)務(wù)方要么集成失敗,要么要等不少時間臀防。而且也沒有一個地方可以呈現(xiàn)當(dāng)前版本的組件版本信息眠菇。還有就是業(yè)務(wù)方對于這種命令行的升級方式接受度也不是很高边败。

基于此,在經(jīng)過了幾輪討論之后捎废,有了新版的持續(xù)集成平臺笑窜,升級操作通過網(wǎng)頁端來完成。

大致思路是缕坎,業(yè)務(wù)方如果要升級組件怖侦,假設(shè)現(xiàn)在的版本是 0.1.7,添加了一些 feature 之后谜叹,殼工程測試通過匾寝,想集成到主工程里看看效果,或者其他組件也想引用這個最新的荷腊,就可以在后臺手動把版本升到 0.1.8-rc.1艳悔,這樣的話,原先依賴~> 0.1.7的組件女仰,不會升到 0.1.8猜年,同時想要測試這個組件的話,只要手動把版本調(diào)到 0.1.8-rc.1 就可以了疾忍。這個過程不會觸發(fā) CI 的編譯檢查乔外。

當(dāng)測試通過后,就可以把尾部的-rc.n去掉一罩,然后點(diǎn)擊「集成」杨幼,就會走 CI 編譯檢查,通過的話聂渊,會在主工程的 podfile 里寫上固定的版本號 0.1.8差购。也就是說,podfile 里所有的組件版本號都是固定的汉嗽。

周邊設(shè)施

基礎(chǔ)組件及組件的文檔 / Demo / 單元測試

無線基礎(chǔ)的職能是為集團(tuán)提供解決方案欲逃,只是在蘑菇街 App 里能 work 是遠(yuǎn)遠(yuǎn)不夠的,所以就需要提供入口饼暑,知道有哪些可用組件稳析,并且如何使用,就像這樣(目前還未實(shí)現(xiàn))

這就要求組件的負(fù)責(zé)人需要及時地更新 README / CHANGELOG / API撵孤,并且當(dāng)發(fā)生 API 變更時迈着,能夠快速通知到使用方。

公共 UI 組件

組件化之后還有一個問題就是資源的重復(fù)性邪码,以前在一個工程里的時候裕菠,資源都可以很方便地拿到,現(xiàn)在獨(dú)立出去了闭专,也不知道哪些是公用的奴潘,哪些是獨(dú)有的旧烧,索性都放到自己的組件里,這樣就會導(dǎo)致包變大画髓。還有一個問題是每個組件可能是不同的產(chǎn)品經(jīng)理在跟掘剪,而他們很可能只關(guān)注于自己關(guān)心的頁面長什么樣,而忽略了整體的樣式奈虾。公共 UI 組件就是用來解決這些問題的夺谁,這些組件甚至可以跨 App 使用。(目前還未實(shí)現(xiàn))

小結(jié)

「組件化」是 App 膨脹到一定體積后的解決方案肉微,能一定程度上解決問題匾鸥,在提高開發(fā)效率的過程中,采坑是難免的碉纳,希望這篇文章能夠帶來些幫助勿负。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市劳曹,隨后出現(xiàn)的幾起案子奴愉,更是在濱河造成了極大的恐慌,老刑警劉巖铁孵,帶你破解...
    沈念sama閱讀 217,277評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件锭硼,死亡現(xiàn)場離奇詭異,居然都是意外死亡蜕劝,警方通過查閱死者的電腦和手機(jī)账忘,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評論 3 393
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來熙宇,“玉大人,你說我怎么就攤上這事溉浙√讨梗” “怎么了?”我有些...
    開封第一講書人閱讀 163,624評論 0 353
  • 文/不壞的土叔 我叫張陵戳稽,是天一觀的道長馆蠕。 經(jīng)常有香客問我,道長惊奇,這世上最難降的妖魔是什么互躬? 我笑而不...
    開封第一講書人閱讀 58,356評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮颂郎,結(jié)果婚禮上吼渡,老公的妹妹穿的比我還像新娘。我一直安慰自己乓序,他們只是感情好寺酪,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,402評論 6 392
  • 文/花漫 我一把揭開白布坎背。 她就那樣靜靜地躺著,像睡著了一般寄雀。 火紅的嫁衣襯著肌膚如雪得滤。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,292評論 1 301
  • 那天盒犹,我揣著相機(jī)與錄音懂更,去河邊找鬼。 笑死急膀,一個胖子當(dāng)著我的面吹牛沮协,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播脖阵,決...
    沈念sama閱讀 40,135評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼皂股,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了命黔?” 一聲冷哼從身側(cè)響起呜呐,我...
    開封第一講書人閱讀 38,992評論 0 275
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎悍募,沒想到半個月后蘑辑,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,429評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡坠宴,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,636評論 3 334
  • 正文 我和宋清朗相戀三年洋魂,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片喜鼓。...
    茶點(diǎn)故事閱讀 39,785評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡副砍,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出庄岖,到底是詐尸還是另有隱情豁翎,我是刑警寧澤,帶...
    沈念sama閱讀 35,492評論 5 345
  • 正文 年R本政府宣布隅忿,位于F島的核電站心剥,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏背桐。R本人自食惡果不足惜优烧,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,092評論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望链峭。 院中可真熱鬧畦娄,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至再膳,卻和暖如春挺勿,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背喂柒。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評論 1 269
  • 我被黑心中介騙來泰國打工不瓶, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人灾杰。 一個月前我還...
    沈念sama閱讀 47,891評論 2 370
  • 正文 我出身青樓蚊丐,卻偏偏與公主長得像,于是被迫代替她去往敵國和親艳吠。 傳聞我的和親對象是個殘疾皇子麦备,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,713評論 2 354

推薦閱讀更多精彩內(nèi)容

  • 在組件化之前,蘑菇街 App 的代碼都是在一個工程里開發(fā)的昭娩,在人比較少凛篙,業(yè)務(wù)發(fā)展不是很快的時候,這樣是比較合適的栏渺,...
    yuditxj閱讀 546評論 0 1
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,102評論 25 707
  • 不怕跌倒呛梆,所以飛翔 組件化開發(fā) 參考資源 Android組件化方案 為什么要組件化開發(fā) 解決問題 實(shí)際業(yè)務(wù)變化非常...
    筆墨Android閱讀 2,982評論 0 0
  • 該文章屬于劉小壯原創(chuàng),轉(zhuǎn)載請注明:劉小壯[http://www.reibang.com/u/2de707c93d...
    劉小壯閱讀 93,264評論 266 518
  • 你的笑容如此美麗 二十年有多長 花開花落之間 一代人的血汗拼搏 終于綻放出奪目的花朵 二十年有多短 時光穿梭之間 ...
    飛翔雨季閱讀 514評論 0 2