APP組件化之路

本文爲『移動前線』羣在3月10日的分享總結整理而成,轉載請註明來自『移動開發前線』公衆號。java

嘉賓介紹git

蘑菇街李忠(花名銀時,網名 limboy),客戶端開發經驗,目前主要負責移動端基礎架構設計及核心技術難點攻克(以 iOS 爲主),爲集團全部 App 提供移動端解決方案。 熱衷於嘗試新技術,並在團隊中推廣,致力於以優秀的代碼、新的理念拓寬工程師的思路和眼界,以提高團隊總體做戰能力爲己任。github

在組件化以前,蘑菇街 App 的代碼都是在一個工程裏開發的,在人比較少,業務發展不是很快的時候,這樣是比較合適的,能必定程度地保證開發效率。後端

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

  • 耦合比較嚴重(由於沒有明確的約束,「組件」間引用的現象會比較多)併發

  • 容易出現衝突(尤爲是使用 Xib,還有就是 Xcode Project,雖然說有腳本能夠改善:https://github.com/truebit/xUnique )app

  • 業務方的開發效率不夠高(只關心本身的組件,卻要編譯整個項目,與其餘不相干的代碼糅合在一塊兒)框架

爲了解決這些問題,就採起了「組件化」策略。它能帶來這些好處:組件化

  • 加快編譯速度(不用編譯主客那一大坨代碼了)單元測試

  • 自由選擇開發姿式(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 作的事情。

先來看看如今的入口方法:


其中 [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 不下來··· 見此:https://github.com/CocoaPods/CocoaPods/issues/4989#issuecomment-193772935 ),固然也有其餘的管理方式,好比 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 膨脹到必定體積後的解決方案,能必定程度上解決問題,在提升開發效率的過程當中,採坑是不免的,但願這篇文章可以帶來些幫助。

QA環節

Q: 對協議部分也蠻好奇的,Android端有采用這種方法的可能性麼?

A: Android咱們有一個commanager, 思路相似,具體實現的話,須要再翻一下...


Q:不一樣組件間怎麼監督和核查UI和代碼資源的公用性,以免浪費?

A: 組件間的監督和核查機制,咱們沒有作,目前正在作的是’包大小壓縮’,其中會涉及到圖片的去重和壓縮,是統一作的。


Q: 每一個組件所依賴的基礎庫是在各自的podfile中,仍是在主工程podfile裏面寫?

A:  在殼工程的podspec和主工程的podfile裏都有,有一個小技巧是能夠把殼工程的podspec做爲dev pod來開發,這樣就能夠避免有一份podspec, 又有一份podfile.


Q:你的組件是如何響應URL的?

A: 組件會經過MGJRouter註冊URL, 而後設置callback, 在callback裏會經過NavigationController push一個VC出來。

Q: 那這個NavigationController是否是要經過參數傳進去?

A: 不用,咱們有一個UISkeletonModule模塊,經過它能夠拿到全局的NavigationController.


Q: 蘑菇街目前的業務下,組件量有多少個?

A: iOS有70多個(包括基礎的和業務的),Android更多。


Q: 那麼多組件,開發的時候,是否是須要先熟悉70多個呢?

A: 這是個問題,組件分爲基礎組件和業務組件,業務組件之間不須要互相熟悉,不過基礎組件確實須要。因此咱們正在作一個’組件展現平臺’,在上面能夠看到咱們提供哪些基礎組件,都是怎麼用的。


Q: 殼工程會將基礎庫都拷貝一份麼,開發時須要將全部組件代碼check下來,這樣是否是致使代碼文件很大?

A: 殼工程會經過pod去拉組件,pod在某個版本以後加入了local cache功能,一樣的lib, 就不用再到網上去下了,因此其實仍是蠻快的,代碼文件不會大到哪裏去吧。


Q:全部的組件都是放在一個工程裏麼,能把單個組件都弄成一個子工程來管理麼,私有pod,而後在主工程裏面pod install對應的組件?

A: 組件都是有單獨的工程的,而後會放到私有的pod源中進行管理。


Q: 你的組件在註冊URL的時候已經實例化過了是麼,仍是說你的組件在block張實例化? 若是是後者,那麼你註冊URL的時機是何時,是在AppDelegatede的didLaunch中調用start方法麼?

A: 在註冊URL時實例化的,一個模塊若是想在didFinishLaunch時作點事情,只要繼承ModuleProtocol協議,而後實現該方法就好了,它會在AppDelegate的相應階段被調用。


Q: 爲何還須要一個殼工程,直接放在主工程下面不行麼?

A: 殼工程用於「乾淨」的業務開發,直接放到主工程,而後push到本身的倉庫麼?那這樣的話,主工程的意義就跟殼工程是同樣的吧,若是直接就是在主工程下面開發,那各個組件都在這上面開發,就回到了最開始的狀態了。


Q: 能夠說起一下Model這塊怎麼處理的麼,Model在各組件間會有傳遞麼?

A: Model通常不會在組件間傳遞,在組件內部卻是能夠隨便傳,Model這塊咱們是本身寫一個MGJEntity, 大體的功能跟JSONModel相似


Q: 組件間數據較多時怎樣組件方式傳遞?

A:  目前尚未遇到這樣的狀況,若是比較多的話,可能會考慮同步調用,也就是走協議,這樣類型就是已知的。組件間通常不傳Model, 若是數據格式比較複雜,會考慮走協議,若是比較簡單就直接open url了。


Q: 這些組件都是開源的麼,作項目的開發者能夠看到具體的實現麼,實現文件是打包成靜態庫麼?

A: 對內是開放源碼的,項目開發者能夠看到,以後的打算是在持續集成那邊經過編譯後自動framework化。


Q: 一個新的需求有什麼標準判斷是新加一個組件來作仍是在某個關聯的組件裏作? 有沒有這樣的狀況,某個組件在開發到必定程度會拆分出若干個組件來?

A: 其實沒有太嚴格的標準,對於業務方來講,若是現有的基礎組件不能知足,就會向咱們提需求,而後咱們儘可能知足他們,若是這個需求比較偏,那就會建議他們從新實現一個。


Q: 請問通常組件化的發佈週期是?須要業務手動發佈吧?

A: 這個因組件而異,對於基礎組件來講,沒什麼問題就不會去動它,對於業務組件來講,他們的發佈週期是跟着班車走的,升級行爲是在持續集成後臺的組件管理中進行的


Q: 大家當初作組件化的過程當中,沒有考慮過將每一個模塊作成一個單獨的Framework框架,而後在主框架上組合一塊兒麼,或者能夠說一下這二者的區別麼?

A: 有的,目前已經有幾個是這麼作了,framework化就是調試時會比較麻煩,斷點進不去,好處就是能提高編譯速度。


移動前線社羣開始收集羣成員的博客、公衆號啦!若是你有博客,歡迎踊躍提交:

https://github.com/pockry/mobile-frontier

相關文章
相關標籤/搜索