組件化做爲目前移動應用架構的主流方式之一,近年來一直是業界積極探索和實踐的方向。html
起初的這個項目,App只有一條產品線,代碼邏輯相對比較清晰,後期隨着公司業務的迅速發展,如今App裏面承載了大概五六條產品線,每一個產品線的流程有部分是同樣的,也有部分是不同的,這就須要作各類各樣的判斷及定製化需求。大概作了一年多後,出現了不一樣產品線提過來的需求,開發人員都須要在主工程中開發,可是開發人員開發的是不一樣的產品線,也得將整個工程跑起來,代碼管理、並行開發效率、分支管理、上線時間明顯有所限制。大概就在去年末,咱們的領導提出了這個問題,但願做成組件化,將代碼重構拆分紅模塊,在主工程中組裝拆分的模塊,造成一個完整的App。 前端
注:傳統的 App 架構設計更多強調的是分層,基於設計模式六大原則之一的單一職責原則,將系統劃分爲基礎層,網絡層,UI層等等,以便於維護和擴展。但隨着業務的發展,系統變得愈來愈複雜,只作分層就不夠了。App 內各子系統之間耦合嚴重, 邊界愈來愈模糊,常常發生你中有我我中有你的狀況(如圖一)。ios
這對代碼質量,功能擴展,以及開發效率都會形成很大的影響。此時,通常會將各個子系統劃分爲相對獨立的模塊,經過中介者模式收斂交互代碼,把模塊間交互部分進行集中封裝, 全部模塊間調用均經過中介者來作(如圖二)。這時架構邏輯會清晰不少,但由於中介者仍然須要反向依賴業務模塊,這並無從根本上解除循壞依賴等問題。時不時發生一個模塊進行改動,多個模塊受影響編譯不過的狀況。進一步的,經過技術手段,消除中介者對業務模塊依賴,即造成了業務模塊化架構設計(圖三)。git
經過業務模塊化架構,通常能夠達到明確模塊職責及邊界,提高代碼質量,減小複雜依賴,優化編譯速度,提高開發效率等效果。不少文章都有相關分析,在此再也不累述。github
組件化開發的缺點:編程
組件化開發的優勢:swift
業務模塊化設計經過對各業務模塊的解耦改造,避免循環雙向依賴,達到提高開發效率和質量的目的。但業務需求的依賴是沒法消除的,因此模塊化方案首先要解決的是如何在無代碼依賴的狀況下實現跨模塊通訊的問題。iOS 由於其強大的運行時特性,不管是基於 NSInvocation 仍是基於 peformSelector 方法, 均可以很很容易作到這一點。但不能爲了解耦而解耦,提高質量與效率纔是咱們的目的。直接基於 hardcode 字符串 + 反射的代碼明顯會極大損害開發質量與效率,與目標背道而馳。因此,模塊化解耦需求的更準確的描述應該是「如何在保證開發質量和效率的前提下作到無代碼依賴的跨模塊通訊」。 目前業界常見的模塊間通信方案大體以下幾種:vim
基於路由 URL 的 UI 頁面統跳管理。 基於反射的遠程接口調用封裝。 基於面向協議思想的服務註冊方案。 基於通知的廣播方案。 根據具體業務和需求的不一樣,大部分公司會採用以上一種或者某幾種的組合。後端
統跳路由是頁面解耦的最多見方式,大量應用於前端頁面。經過把一個 URL 與一個頁面綁定,須要時經過 URL 能夠方便的打開相應頁面。設計模式
//經過路由URL跳轉到商品列表頁面
//kRouteGoodsList = @"//goods/goods_list"
UIViewController *vc = [Router handleURL:kRouteGoodsList];
if(vc) {
[self.navigationController pushViewController:vc animated:YES];
}
複製代碼
固然有些場景會比這個複雜,好比有些頁面須要更多參數。 基本類型的參數,URL 協議自然支持:
//kRouteGoodsDetails = @「//goods/goods_detail?goods_id=%d」
NSString *urlStr = [NSString stringWithFormat:@"kRouteGoodsDetails", 123];
UIViewController *vc = [Router handleURL:urlStr];
if(vc) {
[self.navigationController pushViewController:vc animated:YES];
}
複製代碼
複雜類型的參數,能夠提供一個額外的字典參數 complexParams, 將複雜參數放到字典中便可:
+ (nullable id)handleURL:(nonnull NSString *)urlStr
complexParams:(nullable NSDictionary*)complexParams
completion:(nullable RouteCompletion)completion;
複製代碼
上面方法裏的 completion 參數,是一個回調 block, 處理打開某個頁面須要有回調功能的場景。好比打開會員選擇頁面,搜索會員,搜到以後點擊肯定,回傳會員數據:
//kRouteMemberSearch = @「//member/member_search」
UIViewController *vc = [Router handleURL:urlStr complexParams:nil completion:^(id _Nullable result) {
//code to handle the result
...
}];
if(vc) {
[self.navigationController pushViewController:vc animated:YES];
}
複製代碼
考慮到實現的靈活性,提供路由服務的頁面,會將 URL 與一個 block 相綁定。block 中放入所需的初始化代碼。能夠在合適的地方將初始化 block 與路由 URL 綁定,好比在 +load 方法裏:
+ (void)load {
[Router bindURL:kRouteGoodsList
toHandler:^id _Nullable(NSDictionary * _Nullable parameters) {
return [[GoodsListViewController alloc] init];
}];
複製代碼
更多路由 URL 相關例子,能夠參考 Bifrost 項目中的 Demo.
URL 自己是一種跨多端的通用協議。使用路由URL統跳方案的優點是動態性及多端統一 (H5, iOS,Android,Weex/RN); 缺點是能處理的交互場景偏簡單。因此通常更適用於簡單 UI 頁面跳轉。一些複雜操做和數據傳輸,雖然也能夠經過此方式實現,但都不是很效率。
目前天貓和蘑菇街都有使用路由 URL 做爲本身的頁面統跳方案,達到解耦的目的。
當沒法 import 某個類的頭文件但仍需調用其方法時,最常想到的就是基於反射來實現了。例:
Class manager = NSClassFromString(@"YZGoodsManager");
NSArray *list = [manager performSelector:@selector(getGoodsList)];
//code to handle the list
複製代碼
但這種方式存在大量的 hardcode 字符串。沒法觸發代碼自動補全,容易出現拼寫錯誤,並且這類錯誤只能在運行時觸發相關方法後才能發現。不管是開發效率仍是開發質量都有較大的影響。
如何進行優化呢?這實際上是各端遠程調用都須要解決的問題。移動端最多見的遠程調用就是向後端接口髮網絡請求。針對這類問題,咱們很容易想到建立一個網絡層,將這類「危險代碼」封裝到裏面。上層業務調用時網絡層接口時,不須要 hardcode 字符串,也不須要理解內部麻煩的邏輯。
相似的,我能夠將模塊間通信也封裝到一個「網絡層」中(或者叫消息轉發層)。這樣危險代碼只存在某幾個文件裏,能夠特別地進行 code review 和聯調測試。後期還能夠經過單元測試來保障質量。模塊化方案中,咱們能夠稱這類「轉發層」爲 Mediator (固然你也能夠起個別的名字)。同時由於 performSelector 方法附帶參數數量有限,也沒有返回值,因此更適合使用 NSInvocation 來實現。
//Mediator提供基於NSInvocation的遠程接口調用方法的統一封裝
- (id)performTarget:(NSString *)targetName
action:(NSString *)actionName
params:(NSDictionary *)params;
//Goods模塊全部對外提供的方法封裝在一個Category中
@interface Mediator(Goods)
- (NSArray*)goods_getGoodsList;
- (NSInteger)goods_getGoodsCount;
...
@end
@impletation Mediator(Goods)
- (NSArray*)goods_getGoodsList {
return [self performTarget:@「GoodsModule」 action:@"getGoodsList" params:nil];
}
- (NSInteger)goods_getGoodsCount {
return [self performTarget:@「GoodsModule」 action:@"getGoodsCount" params:nil];
}
...
@end
複製代碼
而後各個業務模塊依賴Mediator, 就能夠直接調用這些方法了。
//業務方依賴Mediator模塊,能夠直接調用相關方法
NSArray *list = [[Mediator sharedInstance] goods_getGoodsList];
複製代碼
這種方案的優點是調用簡單方便,代碼自動補全和編譯時檢查都仍然有效。 劣勢是 category 存在重名覆蓋的風險,須要經過開發規範以及一些檢查機制來規避。同時 Mediator 只是收斂了 hardcode, 並未消除 hardcode, 仍然對開發效率有必定影響。
業界的 CTMediator 開源庫,以及美團都是採用相似方案。
有沒有辦法絕對的避免 hardcode 呢?若是接觸事後端的服務化改造,會發現和移動端的業務模塊化很類似。Dubbo 就是服務化的經典框架之一。它是經過服務註冊的方式來實現遠程接口調用的。即每一個模塊提供本身對外服務的協議聲明,而後將此聲明註冊到中間層。調用方能從中間層看到存在哪些服務接口,而後直接調用便可。例:
//Goods模塊提供的全部對外服務都放在GoodsModuleService中
@protocol GoodsModuleService
- (NSArray*)getGoodsList;
- (NSInteger)getGoodsCount;
...
@end
//Goods模塊提供實現GoodsModuleService的對象,
//並在+load方法中註冊
@interface GoodsModule : NSObject<GoodsModuleService>
@end
@implementation GoodsModule
+ (void)load {
//註冊服務
[ServiceManager registerService:@protocol(service_protocol)
withModule:self.class]
}
//提供具體實現
- (NSArray*)getGoodsList {...}
- (NSInteger)getGoodsCount {...}
@end
//將GoodsModuleService放在某個公共模塊中,對全部業務模塊可見
//業務模塊能夠直接調用相關接口
...
id<GoodsModuleService> module = [ServiceManager objByService:@protocol(GoodsModuleService)];
NSArray *list = [module getGoodsList];
...
複製代碼
這種方式的優點也包括調用簡單方便。代碼自動補全和編譯時檢查都有效。實現起來也簡單,協議的全部實現仍然在模塊內部,因此不須要寫反射代碼了。同時對外暴露的只有協議,符合團隊協做的「面向協議編程」的思想。劣勢是若是服務提供方和使用方依賴的是公共模塊中的同一份協議(protocol), 當協議內容改變時,會存在全部服務依賴模塊編譯失敗的風險。同時須要一個註冊過程,將 Protocol 協議與具體實現綁定起來。
業界裏,蘑菇街的 ServiceManager 和阿里的 BeeHive 都是採用的這個方案。
基於通知的模塊間通信方案,實現思路很是簡單, 直接基於系統的 NSNotificationCenter 便可。 優點是實現簡單,很是適合處理一對多的通信場景。 劣勢是僅適用於簡單通信場景。複雜數據傳輸,同步調用等方式都不太方便。 模塊化通信方案中,更多的是把通知方案做爲以上幾種方案的補充。
組件的存在方式是以每一個pod庫的形式存在的。那麼咱們組合組件的方法就是經過利用CocoaPods的方式添加安裝各個組件,咱們就須要製做CocoaPods遠程私有庫,將其發不到公司的gitlab或GitHub,使工程可以Pod下載下來。
echo "# test" >> README.md
git init
git add README.md
git commit -m "first commit"
git remote add origin https://github.com/c/test.git
git push -u origin master
複製代碼
pod lib create ProjectName
複製代碼
echo "# test" >> README.md
git init
git add README.md
git commit -m "first commit"
git remote add origin https://github.com/c/test.git
git push -u origin master
複製代碼
三、Edit podspec file
vim CoreLib.podspec
Pod::Spec.new do |s|
s.name = '組件工程名'
s.version = '0.0.1'
s.summary = 'summary'
s.description = <<-DESC
description
DESC
s.homepage = '遠程倉庫地址'
s.license = { :type => 'MIT', :file => 'LICENSE' }
s.author = { '做者' => '做者' }
s.source = { :git => '遠程倉庫地址', :tag => s.version.to_s }
s.ios.deployment_target = '8.0'
s.source_files = 'Classes/**/*.{swift,h,m,c}'
s.resources = 'Assets/*'
s.dependency 'AFNetworking', '~> 2.3'
end
複製代碼
//create local tag
git tag '0.0.1'
或
git tag 0.0.1
//local tag push to remote
git push --tags
或
git push origin 0.0.1
//delete local tag
git tag -d 0.0.1
//delete remote tag
git tag origin :0.0.1
複製代碼
pod lib lint --allow-warnings --no-clean
複製代碼
pod repo add CoreLib git@git.test/CoreLib.git
pod repo push CoreLib CoreLib.podspec --allow-warnings
複製代碼
關於組件該如何拆分,這個沒有一個完整的標準,由於每一個公司的業務場景不同,對應衍生出來的各個業務模塊也就不同,因此業務組件間的拆分,這個根據本身公司的業務模塊來進行合理的劃分便可。這裏咱們來講下整個工程的組件大體的劃分方向。
至於組件的拆分顆粒度,這個着實很差去判定,因人而異,不一樣的需求功能複雜度拆分出來的組件大小也不盡相同
以上咱們只是講解了簡單的理論知識,若是你們要實戰的話仍是要多查閱一些資料,不過目前咱們的app使用的是組件化開發的方式,目前各個模塊解耦,能夠快速開發新的app。優勢仍是有不少的,但願你們也能夠敢於嘗試。不過組件化模塊化拆分也要考慮到本身的項目的,最終找到適合本身項目的方案纔是你要作的事情,但願本片文檔能夠爲你提供一些思路。