App 的組件化之路

在組件化之前,app 代碼都是計劃在一個工程開發的,在人比較少,業務發展不是很快的時候,這樣是比較合適的,能一定程度地保證開發效率。

慢慢地代碼量多了起來,開發人員也多了起來,業務發展也快了起來,這時單一工程開發模式就會顯露出一些弊端

耦合比較嚴重(因為沒有明確的約束,「組件」間引用的現象會比較多)

容易出現沖突(尤其是使用 Xib,還有就是 Xcode Project,雖說有腳本可以改善)

業務方的開發效率不夠高(只關心自己的組件,卻要編譯整個項目,與其他不相干的代碼糅合在一起)

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

加快編譯速度(不用編譯主客那一大坨代碼了)

自由選擇開發姿勢(MVC / MVVM / FRP)

方便 QA 有針對性地測試

提高業務開發效率

先來看下,組件化之后的一個大概架構


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

實現方式


組件間通信

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

首頁只需調用?[MGJRouter openURL:@"mgj://detail?id=404"]?就可以打開相應的詳情頁。

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

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

目前還有一塊沒有做,就是參數這塊,雖然描述了短鏈,但真想要生成完整的 URL,還需要知道如何傳參數,這個正在開發中。

還有一種情況會稍微麻煩點,就是「組件A」要調用「組件B」的某個方法,比如在商品詳情頁要展示購物車的商品數量,就涉及到向購物車組件拿數據。

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

使用時?NSNumber *orderCount = [MGJRouter objectForURL:@"mgj://cart/ordercount"]?這樣就拿到了購物車里的商品數。

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

可以看到通過協議可以直接指定返回的數據類型。然后在購物車組件內再新建個類實現這個協議,假設這個類名為MGJCartImpl,接著就可以把它與協議關聯起來?[ModuleManager registerClass:MGJCartImpl forProtocol:@protocol(MGJCart)],對于使用方來說,要拿到這個?MGJCartImpl,需要調用?[ModuleManager classForProtocol:@protocol(MGJCart)]。拿到之后再調用?+ (NSInteger)orderCount就可以了。

那么,這個協議放在哪里比較合適呢?如果跟組件放在一起,使用時還是要先引入組件,如果有多個這樣的組件就會比較麻煩了。所以我們把這些公共的協議統一放到了?PublicProtocolDomain.h?下,到時只依賴這一個文件就可以了。

Android 也是采用類似的方式。

組件生命周期管理

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

先來看看現在的入口方法

其中?[MGJApp startApp]?主要負責一些 SDK 的初始化。[self trackLaunchTime]?是我們打的一個點,用來監測從?main?方法開始到入口方法調用結束花了多長時間。其他的都由?ModuleManager?搞定,loadModuleFromPlist:pathForResource:?方法會讀取 bundle 里的一個 plist 文件,這個文件的內容大概是這樣的

每個?Module?都實現了?ModuleProtocol,其中有一個?- (BOOL)applicaiton:didFinishLaunchingWithOptions:?方法,如果實現了的話,就會被調用。

還有一個問題就是,系統的一些事件會有通知,比如?applicationDidBecomeActive?會有對應的?UIApplicationDidBecomeActiveNotification,組件如果要做響應的話,只需監聽這個系統通知即可。但也有一些事件是沒有通知的,比如?- application:didRegisterUserNotificationSettings:,這時組件如果也要做點事情,怎么辦?

一個簡單的解決方法是在?AppDelegate?的各個方法里,手動調一遍組件的對應的方法,如果有就執行。

殼工程

既然已經拆出去了,那拆出去的組件總得有個載體,這個載體就是殼工程,殼工程主要包含一些基礎組件和業務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 膨脹到一定體積后的解決方案,能一定程度上解決問題,在提高開發效率的過程中,采坑是難免的,希望這篇文章能夠帶來些幫助。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,363評論 6 532
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,497評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,305評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,962評論 1 311
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,727評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,193評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,257評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,411評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,945評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,777評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,978評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,519評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,216評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,642評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,878評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,657評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,960評論 2 373