「蜂鳥配送商家版」是一款針對商家打造的專業配送軟件,有了這款應用,您可使用蜂鳥商家版呼叫全部平臺訂單及電話訂單配送,餐飲、鮮花、蛋糕、生鮮、商超都可配送。超低運費,清晰合理。海量補貼,充值返現。html
以上這段對「蜂鳥商家版」的描述摘自 蜂鳥配送官網,大概能夠理解爲蜂鳥商家版是一個給廣大商家用來發單呼叫配送員的 App。許多同窗可能只據說過「餓了麼」外賣應用,可是對支撐起外賣配送的後勤業務「蜂鳥配送」卻知之甚少,實際上天天海量的外賣訂單都是由蜂鳥配送系統進行處理和配送最終送到消費者手中的。外賣 O2O 是由外賣平臺、商戶、配送系統這三方合做共同完成的,缺一不可。O2O 最核心的價值就是人與服務的鏈接,而這種鏈接最終都是經過配送才得以實現的。ios
自 2016 年末開始我參與蜂鳥商家版的維護工做,除了平常的開發迭代之外,期間還參與推動了項目 Swift 化、項目組件化 / 模塊化、非業務組件開源化等技術改造工做,今天這篇文章就給你們分享一下蜂鳥商家版 iOS 的組件化 / 模塊化實踐過程和本身的心得體會。git
蜂鳥商家版 iOS 端代碼使用 Git 進行管理,代碼託管在內網的 GitLab 上。項目的依賴管理工具是你們比較熟悉的 CocoaPods,除了 RN 模塊爲了和 Android 組公用採用 Submodule 進行管理外,其餘全部的子模塊都採用 Pods 庫的方式引入。github
在「蜂鳥商家版 iOS 組件化 / 模塊化」工做開展以前,項目主要存在以下這些問題:數據庫
在組件化 / 模塊化以前,蜂鳥商家版 App 的全部代碼 / 資源文件等都是在同一個主工程裏的,只有 RN 倉庫或組內公用私有庫等極少部分代碼遊離於主工程以外,因此在開發時,每一次都要編譯整個項目的全部代碼,十分低效。這個問題在獨立開發時還不是十分明顯,畢竟雖然項目大可是代碼只有一我的在提交,因此項目代碼量增長也不是那麼誇張並且對項目發生的變化比較熟悉。可是當多人協做開發時,這個缺陷就暴露了出來,你們在各自開發不一樣的業務時,不只要時刻和他人同步項目變化、讀懂他人代碼,還要每次編譯完整個項目才能對本身所作的一點修改進行調試,效率低下。swift
我開始參與蜂鳥商家版 iOS 端的維護時,以前只有一個前輩在維護,也就是一我的獨立維護一個 App。而後過了沒多久,他離職去了另外一家公司,因此又變成了一我的獨立維護這個 App。這時候由於是獨立開發,因此也不存在什麼太大的問題。但隨着團隊擴大,後面陸續來了幾位同事共同負責這個項目的維護工做,你們都在同一個工程上進行業務開發,常常遇到如代碼衝突、開發效率低下、職責劃分不清、代碼管理混亂等問題。安全
因爲公司處在高速發展的階段,業務增加很快,最直觀的表現就是市場 & 客服部門不斷接到大量一線使用者的使用反饋或訴求,最後就變成了產品展現給咱們開發人員的一份接一份的 PRD。緊湊的業務開發需求和各類靈活的功能迫使咱們想盡一切可以使用的辦法來提升開發效率,提升提測質量。網絡
當我開始參與這個項目的維護時,這個項目就已是一個 Swift 和 OC 混編的項目了,而後還有 RN 和 H5 代碼,能夠說是十分複雜了。雖然這不是我廠惟一一個 Swift 和 OC 的混編項目,但絕對是當時 Swift 化最高的一個項目,約 25% 的代碼爲 Swift。衆所周知,Swift 和 OC 的互相調用遠不如 Java 和 Kotlin 的互相調用那麼順滑(反正你如今知道了),而且到處藏着危機,暗坑無數,因此迫切須要找一個方式,將 Swift 和 OC 代碼進行整理、轉換或者分隔。畢竟,這個文件是 OC 下一個文件就是 Swift 這種頻繁的思惟轉換在業務開發這種本就十分緊張的場景下,會令人十分疲憊,不利於開發工做的順利進行。架構
爲了解決以上這些問題,咱們曾經進行過以下一些探索:ide
能夠發現上述嘗試的結果都不是十分理想,在與 iOS 組內大佬們進行一些溝通,聽取大佬們的意見後,決定對原項目進行「組件化 / 模塊化拆分」工做,它能帶來以下這些好處:
說到組件化 / 模塊化,那麼什麼是組件化 / 模塊化呢?組件化和模塊化的區別又在哪裏呢?
組件,就是咱們對功能的封裝,一個功能就是一個組件,數據庫、網絡、文件操做、社會化分享等等這些功能都是組件。咱們之因此要搞出組件的概念,是爲了可以讓咱們的上層業務模塊可以隨時依賴和調用這些基礎功能。組件基本上能夠分爲基礎功能組件、通用 UI 組件、基礎業務組件等這幾類。因此爲了知足上述要求,組件必須具備較高的獨立性、擴展性以及複用性。
模塊,就是對一系列有內聚性的業務進行整理,將其與其它業務進行切割、拆分,從主工程或原所在位置抽離爲一個相對獨立的部分。僅僅針對業務而言,好比說咱們能夠把訂單業務獨立爲爲一個模塊,能夠把我的中心獨立爲一個模塊,把用戶登陸獨立爲一個模塊等,在 App 中的體現就是一個個獨立的 Git 倉庫。模塊化的一個好處是用到時能夠搭積木,好比能夠多個工程間複用同一個或幾個業務模塊,好比騰訊的 QQ 和 TIM,除了 UI 界面外 TIM 顯然複用了大量現有的原 QQ 工程的業務模塊代碼,固然,咱們這裏暫時並無這個需求。
通過小組會議討論,咱們的想法是將共用組件獨立出來,而後直接按業務對現有主工程進行拆分同時兼顧 Swift 與 OC 分離,大體劃分以下表所示:
組件 | 庫名 | 主要內容 |
---|---|---|
基礎(OC) | LPDBOCFoundationGarbage | 基礎的 OC 組件,各類零散的、混亂的視圖、組件、控件、常量、OC 宏定義等,全放在這裏,供上層調用。和他的庫名同樣,其本質就大概就是個垃圾桶。 |
基礎(Swift) | LPDBPublicModule | 基礎的 Swift 組件,包含一些公用的 Swift 擴展,和模塊間解耦的協議。 |
網絡(OC) | LPDBNetwork | 網絡組件,對 AFNetworking 的淺層封裝,同時包含了和網絡相關的業務功能。 |
... | ... | ... |
模塊 | 庫名 | 主要內容 |
---|---|---|
歷史(OC) | LPDBHistoryModule | 歷史訂單模塊,包含和歷史訂單相關的資源文件、UI、業務邏輯代碼等。 |
登陸(OC) | LPDBLoginModule | 用戶登陸模塊,包含和登陸、註冊頁面相關的資源文件、UI、業務邏輯代碼等。 |
用戶中心(OC) | LPDBUserCenterModule | 用戶中心模塊,包含和用戶我的中心以及狀態相關的資源文件、UI、業務邏輯代碼等。 |
... | ... | ... |
按照上面的思路,理想化的模塊 / 組件依賴關係圖大概是這個樣子的:
由於蜂鳥商家版的團隊開發人員以前均沒有過任何項目的拆分經驗,你們也都是摸着石頭過河,走一步看一步。因此雖然以上的拆分思路整體是對的,先拆組件後拆業務,但因爲各類各樣的緣由,一些問題就在接下來的工做實施過程當中暴露了出來。
咱們小組主要仍是以業務開發爲主,因此組件化 / 模塊化工做都是你們抽空閒時間來完成,並無進行硬性的排期和設置 Deadline。按照以前制定的計劃,咱們進行了如下這些工做:
LPDBOCFoundationGarbage 是咱們項目最早抽出的部分,這個庫將和 LPDBPublicModule 一塊兒,做爲整個工程的最底層,再往下就是。這個庫的定位和它的名字同樣,就是一個垃圾桶,啥都往裏放。其中大體包含如下一些東西:
由於咱們在進行拆分任務的同時,還在同時維持着項目的開發工做,因此咱們暫時沒有精力作細緻的拆分工做,只能先把這些零散的部分先放在一塊兒進行管理。
LPDBPublicModule 是基礎的 Swift 組件,這個庫主要包含:
由於工程內的 Swift 代碼大可能是咱們新寫的,因此相對舊的 OC 代碼而言,整理地更好一些,因此這個倉庫乾淨不少
LPDBNetwork 網絡組件是咱們項目完成 OC 和 Swift 基礎部分後最早抽出的部分,剛開始咱們認爲這部分僅僅是單純的業務網絡請求操做和對 AFNetworking 的淺層封裝,不包含界面 UI 邏輯等。不過當咱們拆解完成後,發現其中還包含了一堆奇怪的東西:
這一部分的話,由於都是比較古老的代碼,因此當初的開發人員都已經再也不繼續維護了,因此在只能是咱們本身進行拆分的狀況下,爲了防止大的變動致使發生問題,因此沒有對這一塊進行更細緻的拆解工做。畢竟再爛代碼也比不能工做的代碼要好。
Swift 的 UI 庫,咱們將工程中的一些 Swift 視圖和控件收集到了這個項目中,主要包含如下這些內容:
由於 Swift 代碼總量還不是很大,因此這個庫的東西目前也不是不少,之後會逐漸豐富起來。
完成了上面的組件庫的獨立工做後,業務模塊的拆解就相對輕鬆一些了,目前咱們主要完成了三個業務模塊的拆分工做。
LPDBHistoryModule 歷史訂單模塊,和歷史訂單頁面相關的信息都在該模塊中,主要包含如下內容:
由於該模塊相對來講比較獨立,因此拆分過程也比較順利,主要依賴了 LPDBPublicModule、LPDBNetwork、LPDBOCFoundationGarbage 組件。
LPDBLoginModule 用戶登陸模塊是一個與用戶登陸、註冊以及用戶登陸信息有關的模塊,主要包含了如下信息:
該模塊相比較歷史訂單模塊複雜了一些,不過仍然比較順利,主要依賴了 LPDBPublicModule、LPDBOCFoundationGarbage、LPDBNetwork 組件。
LPDBUserCenterModule 用戶中心模塊是一個與用戶我的中心以及用戶信息修改有關的模塊,主要包含了如下信息:
該模塊主要依賴了 LPDBOCFoundationGarbage 組件和 LPDBLoginModule 模塊。
剩下的其餘一些模塊仍然處於計劃中的狀態,暫未進行拆分。到這一步的話,庫間依賴關係大體以下圖所示:
能夠看到其中存在一些不太合理的依賴關係,如 LPDBUserCenterModule 依賴 LPDBLoginModule 模塊,也就是所謂的業務模塊橫向依賴問題,接下來,咱們就要處理這一問題。
因爲以前開發過程當中從未有過任何模塊化的考量,因此蜂鳥商家版的代碼很是雜糅,項目依賴關係十分複雜,主要能夠分爲如下三類耦合:
在拆分業務模塊的過程當中,常常發生兩個業務模塊同時引用某一塊業務代碼的問題,這時咱們就須要對這一塊代碼進行理解,首先區分它到底應不該該劃分到業務層來?
在 LPDBUserCenterModule 的抽離過程當中就遇到了這個問題,LPDBUserCenterModule 和 LPDBLoginModule 共同依賴了幾個和用戶信息有關的數據模型,致使須要發生模塊間橫向依賴,因此咱們將共用的數據模型抽出,而後下沉到了 LPDBOCFoundationGarbage 中。
另外一個常常遇到的問題就是跨模塊調用代碼的問題了,不只是模塊與模塊間代碼的互相調用、模塊間頁面的跳轉,還有模塊反向調用主工程代碼等問題,這個問題的解決咱們分了三步:
由於工程的複雜性和之前代碼的不規範,致使咱們在處理切割業務模塊時比較痛苦,因此咱們在剛開始抽出模塊時採用了一種快速但不太安全的方式進行解耦,好比在 LPDBUserCenterModule 模塊中須要調用主工程的 getMiddlePageVC 方法時,咱們用了以下臨時解決方案:
if ([[UIApplication sharedApplication].delegate respondsToSelector:@selector(getMiddlePageVC)]) {
UIViewController *info = [[UIApplication sharedApplication].delegate performSelector:@selector(getMiddlePageVC)];
...
}
複製代碼
而後在主工程的 中實現這個接口:
// .h
@interface AppDelegate : UIResponder <UIApplicationDelegate>
...
// LPDBUserCenterModule
- (UIViewController *)getMiddlePageVC;
...
@end
// .m
@implementation AppDelegate
...
- (UIViewController *)getMiddlePageVC {
...
return xxx;
}
...
@end
複製代碼
這一方案的優勢就是靈活,利用 NSClassFromString、performSelector 等方式,可以快速解決各類耦合問題,瞬間切割出模塊。但缺點也顯而易見,字符串硬編碼,維護成本大,去掉了編譯器檢查,容易翻車。
因此天然而然地,當咱們的某個業務模塊的拆分工做基本定型時,咱們就開始將第一步中的反射調用方式替換爲協議的方式進行調用,好比當 LPDBLoginModule 模塊須要調用主工程的 getCoordinate 方法時,示例以下:
id delegate = [[UIApplication sharedApplication] delegate];
if (![delegate conformsToProtocol:@protocol(AppDelegateProtocol)]) {
return;
}
CLLocationCoordinate2D coordinate = [delegate coordinate];
複製代碼
而後在主工程中實現該方法:
// .h
#import "AppDelegate.h"
@import LPDBLoginModule;
@interface AppDelegate (Protocol) <AppDelegateProtocol>
@end
// .m
@implementation AppDelegate (Protocol)
- (CLLocationCoordinate2D)getCoordinate {
return self.coordinate;
}
@end
複製代碼
可是,樣的改變並不能完全解決所編寫的模塊間互相調用的代碼缺少編譯器檢查的問題,而僅僅是對調用方作了判斷加上了容錯,並不能在編譯期就讓開發人員察覺到問題,必定要進行測試才能夠,因此這種方式也不是十分理想。
那麼爲了完全解決問題,咱們開發和引入了組件通訊和工具 Lotusoot,調用方式有下列幾種可供參考:
let lotus = s(AccountLotus.self)
let accountModule: AccountLotus = LotusootCoordinator.lotusoot(lotus: lotus) as! AccountLotus
accountModule.login(username: "admin", password: "wow") { (error) in
print(error ?? "")
}
複製代碼
let error: NSError? = LotusootRouter.register(route: "newproj://account/login") { (lotusootURL) in
accountModule.showLoginVC(username: "admin", password: "wow")
}
複製代碼
let param: Dictionary = ["username" : "admin",
"password" : "wow"]
// 無回調
LotusootRouter.open(route: "newproj://account/login", params: param)
// 有回調
LotusootRouter.open(route: "newproj://account/login", params: param).completion { (error) in
print(error ?? "open success")
}
// ⚠️不推薦的用法,用 ?pram0=xxx 這樣的形式致使字符串散落在各處,不易管理。
// 但爲了保證 Hybrid 項目中 H5 頁面的正常跳轉,提供了此種調用
LotusootRouter.open(url: "newproj://account/login?username=zhoulingyu").completion { (error) in
print(error ?? "open success")
}
複製代碼
具體能夠參見 iOS 靈活的 模塊化/組件化 工具與規範 Lotusoot 解說 一文,在此很少作贅述。相似的工具還有 BeeHive 和 LPDMvvmRouterKit 等,你們能夠自行進一步探索。
最終結構就變成了如圖所示的樣子:
因爲參與拆分工做的人員比較缺少組件化經驗,因此致使某些庫的拆分不是十分合理,某些應該沉入底層的公用 Model 和常量等沒有在開始時就放到一個合理的位置。業務模塊之間也存在一些不合理的橫向依賴,沒有進行一個合理的業務邊界劃分。這些緣由致使咱們在進行拆分工做時常常須要回過頭來對已經拆出來的模塊和組件從新進行整理和處理,重複勞動量很大。
某些庫好比 LPDBOCFoundationGarbage 比較龐大,而像 LPDBUIKit 這樣的庫中內容卻很是少,這一點的處理上存在問題。若是一個拆分完成的庫仍然比較臃腫的化,說明仍然存在細化拆分的必餘地。
因爲沒有能提早制定好詳細的進度計劃表,加上業務工做的擠壓,致使咱們花在組件化 / 模塊化工做上的時間比較零散。本意是但願你們可以靈活安排工做,合理處置業務開發與技術改造工做之間的關係,但效果不是很理想,表現就是組件化 / 模塊化工做的進行沒有連續性,你們的積極性和工做效率也都不高。
查看和學習一些同類成功的案例資料或者向業內大佬們請教可以對計劃的制定帶來便利,可以使咱們避免不少錯誤的設計,少走一些彎路,下降返工率。
在準備做戰時,我經常發現定好的計劃沒有用處,但計劃的過程仍必不可少。—— 德懷特·艾森豪威爾
制定詳細的總體規劃可以在設計階段就將一些不合理的地方暴露出來,從而拿出解決方案使問題提早獲得解決,或者把不合理的內容刪減替換掉,例如分層不合理、庫間依賴這樣的問題,就會減小不少。拿出細緻的任務拆分計劃和工做量預估,也能更合理地將任務安排到開發人員手中,在提高工做效率的同時也能儘可能避免和業務開發產生衝突。
好的代碼和編碼習慣可以大幅提高項目的可維護性,爲以後的工做帶來便利。咱們以前舊的 OC 代碼比較混亂,基本處於沒法維護的狀態,拆分起來十分痛苦;而新寫的 Swift 代碼明顯質量要高不少(這真的不是咱們自詡...),拆分起來就順利多了。
每個拆分出的模塊及時添加文檔,嫌麻煩的話至少要創建一份通用的 README 模板,每個模塊或組件的創建者把模塊內容、拆分目的、設計思路等基本信息記錄一下,有什麼坑或者注意點也能夠文檔化,使之後的長期項目維護成爲可能。
咱們在組件化 / 模塊化工做期間,產出的一些庫和工具放在了 GitHub 上進行開源,給你們一些借鑑的同時,也但願可以收到你們的意見和建議,提升咱們項目自己的質量:
庫名 | 簡介 | 倉庫地址 |
---|---|---|
EFPodsAnalyzer | 可視化 Pods 庫依賴分析工具 | github.com/EyreFree/EF… |
EFAutoScrollLabel | 一個帶跑馬燈效果的 UILabel | github.com/EyreFree/EF… |
Bamboots | 一個面向協議的 Swift 網絡庫 | github.com/mmoaay/Bamb… |
Lotusoot | 靈活的 Swift 組件解耦和通訊工具 | github.com/Vegetarians… |
bigkeeper | 一個 iOS & Android 模塊化項目效率提高工具 | github.com/BigKeeper/b… |
SideNavigation | 一個支持側滑且可自定義的側邊欄 | github.com/CNKCQ/SideN… |
ViewPagers | 一個支持手勢的 Segmented Control | github.com/CNKCQ/ViewP… |
本文基本描述了蜂鳥商家版 App 到目前爲止的組件化 / 模塊化實踐狀況,但願本文可以給您的移動項目演進提供一些借鑑。在此過程當中咱們產出的一些文章、開源庫和工具,也但願能給你們帶來必定的幫助或者啓發。歡迎你們提出各類反饋和建議或,幫助咱們繼續改進和提升。
2017 年末,也就是差很少我參與蜂鳥商家版的維護工做滿一年的樣子,因爲業務調整的緣由這個 App 已經移交給別的團隊進行維護了,致使項目的 Swift 化和組件化 / 模塊化工做並無所有完成,這一點有些遺憾。不過仍是但願蜂鳥商家版可以愈來愈好,繼續爲廣大商家朋友們服務。
好消息是,接下來我主要參與蜂鳥團隊版 App 的架構工做,這一次咱們根據以前暴露出的問題制定了詳細的工做計劃,有了蜂鳥商家版的踩坑經驗後,我相信這一次咱們必定能順利完成目標。2018,加油,一塊兒拼!
本文編寫過程當中參考瞭如下文章,在此對原做者們表示感謝:
「模塊化平常」系列短文,把本身模塊化過程當中的踩坑歷程分享出來,給有(或者尚未)遇到相似問題的同窗一個參考和幫助:
未完待續...2333
若有任何知識產權、版權問題或理論錯誤,還請指正。
juejin.im/post/5a620c… 轉載請註明原做者及以上信息。