①什麼是組件化?html
組件化就是將一個項目拆分紅若干個組件,分而治之。好比一個汽車的生產,也是將輪子、燈、座椅等等做爲單獨的組件,由各自的工廠去生產維護,生產輪子的就專門作輪子,生產座椅的就專門生產座椅,等各個組件都作好後再拿到組裝廠統一調度組裝使用。ios
在實際的開發中也是同樣,好比咱們常常用到的微信,有朋友圈、漂流瓶、聊天模塊、支付模塊等等衆多功能,微信開發者也是按照組件來劃分各自的開發任務的,好比A團隊負責漂流瓶、B團隊負責朋友圈等。而後在總項目中分別調用組件來使用。git
固然,組件並不必定是這麼大的業務模塊,也多是一個小UI,好比bander、按鈕等等,這樣在項目中多處地方用到的話就能夠直接調用組件使用了。github
組件化開發的好處有如下幾點:微信
一、高複用性:組件建立後就能夠被須要的地方調用,好比Bander,由於項目中可能會有多個地方用到bander,因此將bander抽成一個組件後,須要用的地方直接就能夠拿來用,而不用在重寫一個,提升了代碼的複用性;網絡
二、低耦合性:低耦合就是指各部分依賴程度低,比較獨立。由於組件化開發是各自維護本身的組件,因此相對來講比較獨立;微信開發
因此,組件化開發很適合多人開發的項目,組件間單獨維護單獨測試,簡單方便。架構
②組件化、模塊化、插件化app
這三個概念很相近,咱們一一來看:框架
首先,模塊化,模塊化是指將一個項目按照業務邏輯劃分紅若干個模塊,好比購物類app能夠劃分爲商品展現模塊、購物車模塊、訂單模塊、客服模塊等等,將一個項目分紅幾層,這樣能夠保證每一個模塊的職能單一;
模塊化雖然進行了分層開發,可是有個問題就是代碼複用性不高,好比A團隊開發商品模塊時寫了一個bander,而在B團隊開發的訂單模塊中也會用到,可是B卻沒辦法用,這個時候就出現了組件化。
組件化是在模塊化的基礎上的進一步演變,它劃分的更細了,每一個組件都是獨立的,能夠按照須要選擇須要的組件組合起來就成爲了整個項目。
而插件化,本質上也是模塊化的一種,它也是按照業務邏輯進行劃分,但不是劃分紅一個個模塊,而是劃分紅插件(這些插件能夠獨立編譯打包成爲一個獨立子app),
而上線的時候是將各個插件組合到一塊兒造成一個大的app。同時由於插件化的加載是動態的,因此能夠實現熱修復。
//熱修復原理 首先,生成新版本的apk與舊版本的apk的差別補丁包文件; 其次,使用熱修復框架的Api在Application中去嘗試加載指定路徑的補丁差別包; 最後,只須要將補丁差別包寶貝到對應路徑,代開有bug的App,在Applcation建立的時候就會將補丁包文件加載到內存中而且替換對應的方法。
雖然組件化和插件化拆分的部分均可以單獨編譯運行,可是兩種仍是有較大差別的:
//劃分單位 組件化的單位是組件(module)。 插件化的單位是apk(一個完整的應用)。 //實現內容 組件化實現的是解耦與加快編譯, 隔離不須要關注的部分。 插件化實現的也是解耦與加快編譯,同時實現熱插拔也就是熱更新。 //靈活性 組件化的靈活性在於按加載時機切換,分離出獨立的業務組件,好比微信的朋友圈 插件化的靈活性在因而加載apk, 徹底能夠動態加載,動態更新,比組件化更靈活。 組件化能作的只是, 朋友圈已經有了,我想單獨調試,維護,和別人不耦合。可是和整個項目仍是有關聯的。 插件化能夠說朋友圈就是一個app, 我須要整合了,把它整合進微信這個大的app裏面 其實從框架名稱就能夠看出: 組 和 插。 組原本就是一個系統,你把微信分爲朋友圈,聊天, 通信錄按意義上劃爲獨立模塊,但並非真正意義上的獨立模塊。 插原本就是不一樣的apk, 你把微信的朋友圈,聊天,通信錄單獨作一個徹底獨立的app, 須要微信的時候插在一塊兒,就是一個大型的app了。
插件化更關注動態加載、熱更新、熱修復等‘插拔’技術,目前熱門的插件化方案有:阿里的atlas,360公司的RePlugin,滴滴的VirtualAPK等等;
因此,組件化、模塊化、插件化都是將一個項目劃分紅若干個部分,分而治之,只不過各自劃分的依據和劃分的單位不一樣。
咱們在瞭解清楚組件化的意義後,那麼接下來進行組件化操做,第一個問題就是組件怎麼拆分?
組件劃分不細緻會形成不少冗餘代碼,或者劃分的太細緻則會加倍增長工做量。換句話說,組件的劃分決定了整個工程的質量。
我會從如下幾個方面來劃分組件:
UI
公共組件基礎通用組件的劃分
咱們這麼來理解基礎通用組件, 變化不大,並且基本上每一個項目都用到,項目中都要圍繞這些組件來實現業務功能的組件。例如咱們在Pods
中的
AFNetworking
、
SDWebImage
、
FMDB
,以及經常使用到的
Category
等。這些組件或許須要根據業務進行一些二次封裝,可是每一個項目中對它們的改動都不大。
URL
跳轉路由 ALRouterCategory
的設計AOP代替繼承基礎業務組件
咱們能夠將相似用戶行爲統計
、異常上報
、推送服務
、消息通道
、支付
、通用宏定義頭文件
這種根據業務爲基礎的服務SDK
做爲基礎業務組件,最好每一個基礎業務組件都各分其責,避免某些組件沒用到某些功能而形成代碼冗餘。
UI公共組件
UI
也有公共部分,建議在進行開發以前能夠和設計師取一下經,或許他們已經作好了公共組件~這樣劃分根據他們的來就行了。
UI
組件種類繁多,你們記得根據 公共的原則來抽離就行..
獨立業務組件
根據業務的獨立性來劃分,例如我將我司的電商APP
能夠劃分爲首頁組件
、購物車組件
、登陸註冊組件
、訂單組件
、用戶中心組件
、商品組件
。
獨立組件必定要保證獨立性,避免首頁
含有商品
組件等這種狀況,每一個組件都經過Route
來交互,必要時提供對應的接口。
總結
以上內容總結爲下圖,只要劃分清晰了才能提升代碼效率,組件化纔有意義。
講完概念和劃分後,咱們接下來看一下怎麼去作一個組件呢?好比咱們如今想建立一個工具類CDUtils組件:
①本地倉庫的建立
1.完成功能開發工做,實現組件功能,也就是把代碼寫好,實現對應的功能
注意,組件化的順序應該是:先實現好組件功能在製做組件化。因此應該先開發好組件的功能(就跟正常開發項目同樣,能夠添加依賴庫實現想要的功能),完成功能後開始製做組件。而不是先組件化而後在實現功能,這個就顛倒了。
2.而後打開終端 切換到改項目路徑下 輸入
pod lib create XXX (XXX:表明想要封裝的組件名稱, 這個根據本身的需求而定)
3.而後會出來一些對組件工程的設置:
What language do you want to use?? [ Swift / ObjC ] ObjC(開發語言設置,根據本身而定,這裏爲ObjC) Would you like to include a demo application with your library? [ Yes / No ] Yes(是否須要建立一個demo用來測試你的組件,這裏選擇Yes,是爲了以後對寫好的組件進行測試) Which testing frameworks will you use? [ Specta / Kiwi / None ] None(測試框架) Would you like to do view based testing? [ Yes / No ] No(是否要作基礎的視圖測試) What is your class prefix? XX (文件前綴)
4.當建立完成以後,工程會自動打開,這時咱們發現項目的結構發生了變化:
在pods工程Development Pods文件夾下有一個replaceMe.m文件,咱們要將咱們寫的東西(colorTool.h和colorTool.m替換到這裏),注意這裏是要文件的真實替換而不是在目錄中的順序變化,替換完成是下面這個樣子:(添加功能的代碼必定放在Classes)
這個時候,咱們就已經建立好了一個組件colorTool存放在本地庫,咱們就能夠在本地使用了,好比說在剛纔那個項目中,添加pod管理
pod init
而後就會出現一個podfile,咱們在裏面添加組件及地址
platform :ios, '9.0' target 'cdutils' do pod 'ColorTool', :path =>'ColorTool' end
而後執行pod install 咱們就可使用了
固然,在本地其餘項目也可使用,
好比咱們新建一個項目,而後pod init 只不過在podfile文件中path須要寫全地址 而後pod install 發現也可使用
platform :ios, '9.0' target 'weew' do pod 'ColorTool', :path =>'/Users/uerName/Desktop/cdutils/ColorTool' end
這裏的全地址就是podspec所在的路徑
但咱們在實際開發中代碼不能只存放在本地,須要存儲在遠程,讓團隊均可以用,因此咱們還須要將本地倉庫的組件推送到遠程倉庫。
②遠程倉庫
既然是遠程倉庫,那咱們須要選擇一個遠程代碼託管平臺,經常使用的有碼雲和github兩種,由於碼雲訪問速度快和私有庫免費,因此咱們這裏選擇了碼雲,二者在使用上都是相同的,無非就是遠程地址不一樣而已。
5.在碼雲上建立項目,獲取項目地址
6.配置spec文件,這個文件很重要,它描述該庫某一個版本的信息,好比庫的名字、版本號、描述、依賴庫等等,每個組件都有本身的spec文件。
因此,咱們須要修改spec文件,好比說修改裏面的source內容等信息,舉個例子
Pod::Spec.new do |spec| //庫名 spec.name = 'ColorTool' //版本號 spec.version = '0.1.0' // 受權協議 spec.license = { :type => 'MIT', :file => 'LICENSE' } //庫的首頁 spec.homepage = ''https://gitee.com/github-xxxxx' //做者 spec.authors = { 'xxx' => 'xxx@126.com' } //庫的概要 spec.summary = 'A short description of ColorTool.' // 庫的源路徑和版本號 這個是最重要的 必定要寫本身的組件遠程地址 spec.source = { :git => 'https://gitee.com/github-xxxxx/colorTool.git', :tag => 'v3.1.0' } //源文件,這個庫僅包含Reachability.h和Reachability.m文件 spec.source_files = 'ColorTool/Classes/**/*' //使用到的系統框架 spec.framework = 'SystemConfiguration' // 是否支持ARC spec.requires_arc = true end
當對內容修改完成以後,保存。
7.拿到地址後,切換到組件根目錄(也就是.podspec文件目錄)下 將代碼提交到遠程倉庫:
注意,在提交代碼是必定要保證spec中的source是遠程地址 不然pod install的時候source不對致使不能正確執行
cd /Users/userName/Desktop/cdutils/ColorTool //記得後面必定要有 . git add . git commit -m "初始化" //添加遠程倉庫(根據本身的項目地址來操做) git remote add origin https://gitee.com/xxx/HFMyTest.git //推送到遠程 git push -u origin master -f //打tag 注意:這裏的tag號必須和.podSpec文件的版本號一致 git tag 0.1.0 //將tag推送到遠程 git push --tags
這樣咱們就將組件功能代碼添加到了遠程,接下來咱們在將spec文件推送到遠程。
8.若是尚未建立spec遠程倉庫,能夠建立一個,也是在碼雲建立,建立過程和上面寫的同樣,只不過這個不是存放具體代碼,而是存放各個組件的spec文件。
9.若是沒有將遠程spec倉庫添加到本地,能夠經過下面指令添加到本地:
pod repo add 自定義一個Specs名稱 公司Specs地址
在這個地方能夠看到咱們剛纔建立的本地spec倉庫
10.將spec推送到遠程,別人要想pod 'colorTool'時是找不到spec文件的,也就沒有辦法根據source去拉代碼,因此須要將spec推送到遠程。
pod repo push MySpecs(同9) 組件名字.podspec
pod repo push MySpecs ColorTool.podspec --use-libraries --allow-warnings
若是有警告,要忽略的話 pod repo push MySpecs 組件名字.podspec --use-libraries --allow-warnings 包含私有庫 (這個暫時尚未用過) pod repo push MySpecs 組件名字.podspec –sources=oschina-qx2
11.這樣,咱們就將spec推送到了遠程,可使用pod search ColorTool來查詢是否已經提交到cocoapods;
12.每當咱們要迭代版本的時候,除了修改業務功能代碼變更,就是要修改.podspec這個文件,只用修改版本號,重複六、七、10便可。注意的一點是咱們是把組件提交到了碼雲上,因此從碼雲上clone代碼修改迭代的話可能不太好弄,由於只有組件缺乏環境,因此咱們能夠吧咱們寫組件的這個xcworkspace文件也存到遠程,這樣就能夠在這裏面方便的修改組件了。
若是咱們迭代組件版本更新到遠程後,發現組件仍是舊版本,能夠作以下操做
//1.更新本地倉庫 pod repo update MySpecs(本地倉庫名) //2.刪除項目中的刪除podfile.lock+xcworkspace+Pods文件 從新執行pod install pod install
這樣,當其餘人在遠程想用咱們的組件的時候,就能夠了:
①先將咱們的遠程spec倉庫添加到本地:pod repo add 自定義一個Specs名稱 公司Specs地址(這個是須要驗證帳戶密碼的)
②添加pod管理,在podfile文件中添加組件:
pod 'ColorTool', :git => 'https://gitee.com/github-13584768/colorTool.git'
其實這裏不用指定git的具體地址,可是不指定的話老是顯示找不到colortoo的說明文件,重置spec庫也沒用,這裏就指定了 更多關於podfile文件用法
添加完組件咱們就可使用了。
本地Specs倉庫位置:在終端輸入:pod repo,便可顯示出當前全部的倉庫地址及名稱,找到對應的Specs,複製路徑並前往文件夾。其中存放着咱們組件的版本號文件和文件下的.podspec文件.
組件中有pod其餘框架的狀況:
有的時候咱們的組件會用到其餘第三方框架或者咱們本身寫的其餘組件,好比咱們如今有個彈框工具組件,須要依賴SDWebimage,因此在組件開發的時候咱們在podfile中經過pod 'SDWebimage' 引入這個框架進行開發調用。但當別人用咱們的組件就會出現的時候,不知道組件依賴SDWebimage,因此會出現找不到SDWebimage的錯誤,那麼這個時候咱們應該在組件中的podSpea文件中s.dependency說明一下咱們這個組件依賴了哪些框架,這樣系統會自動配置好咱們組件依賴的框架環境,保證咱們的組件正常使用。(這個屬性默認是註釋的,咱們須要去掉#並填入咱們本身須要依賴的框架)
寫好以後保存而後進行步驟12中的操做。若是依賴多個庫,能夠並列寫:
s.dependency 'AFNetworking' s.dependency 'SDWebImage'
組件中有圖片等資源的狀況:
好比上面這個組件,咱們在使用的時候發現圖片加載不出來,這是由於咱們少作了三步:
1.是否將圖片放到了Assets文件夾中(這裏面放的是文件,好比.png等文件)
2.是否配置podspec文件的資源屬性:
//這個屬性默認是註釋掉的 意思就是'alertToolLib/Assets下的全部文件都放到alertToolLib.bundle中 這個alertToolLib/Assets/*路徑是根據本身實際狀況須要的 看本身圖片路徑是什麼樣的 s.resource_bundles = { 'alertToolLib' => ['alertToolLib/Assets/*'] }
3.加載圖片資源的路徑是否正確:
imageName
或者
[mainbundle pathForResource]
讀取,可是在用
pod
進行管理的時候,
pod
中的資源文件也會變成
bundle
加入到
mainBundle
中,可是因爲資源文件的
bundle
並非
mainBundle
,因此這種方法是行不通的,關鍵是要取到資源相關聯的
bundle
其關係是這樣的
因此咱們要到對應的bundle中去加載圖片
組件中有加載xib的狀況,這個緣由和加載圖片同樣,也是由於路徑問題,解決方案和圖片同樣(xib和圖片同樣都屬於圖片資源,因此都要存放到Assets文件夾中)
當咱們寫好若干個組件以後,就出現了一個問題,好比商品詳情頁的組件想跳轉到購物車組件,那不一樣的組件間是如何通訊的呢?
在沒有組件化的時候,咱們的作法是在詳情頁中引入購物車的頭文件進行調用,可是這樣的話會是代碼耦合性很是高,各個部分相互引用,當咱們須要把某個東西拿出來用的時候,發現要刪減不少東西,結構以下所示:(箭頭表示引用)
上面這個結構太錯綜複雜了,對一個模塊的修改每每影響多的地方,後期維護成本大,因此咱們須要用其餘方式來訪問其餘模塊,讓模塊間相互獨立。好比如今經常使用的兩種方案,經過路由或url來訪問其餘組件(模塊),咱們一一來看。
①路由CTMediator(runtime+addtargetAction)
鑑於上面模塊間的關係太複雜,咱們須要想一個辦法就是不但願導入其餘模塊的頭文件但仍然可使用該模塊,咱們想到了一個方法就是創建一箇中間件,這個中間件導入了咱們全部要用到的模塊的頭文件。咱們想用其餘模塊的其餘功能直接調用這個中間件的一個方法就行。
咱們直接引入中間件 調用器方法就行:
#import "middleware.h" @interface ViewController () @end @implementation ViewController- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event{ [middleware alertToolShowLoadingProgressIndication]; }
上面的模塊間關係就變成了↓↓
這個時候咱們就能夠不引用其餘模塊的頭文件來調用其餘模塊功能了,這樣能夠有效解決高耦合的問題的問題了,可是這樣作還有一個問題,
那就是模塊與模塊間雖然不耦合了,可是模塊間與中間件相互引用耦合了,並且中間件功能太多太複雜了,很差維護,還能夠繼續優化。
這個時候咱們想到了runtime中有兩個方法:
//根據類名字符串獲取同名的類 Class cla = NSClassFromString(@"AlertTool"); //根據方法名字符串獲取同名方法 SEL selector = NSSelectorFromString(@"showLoadingProgressIndication");
咱們在獲取到類和方法後能夠根據performSelector方法來實現調用
Class cla = NSClassFromString(@"AlertTool"); SEL selector = NSSelectorFromString(@"showLoadingProgressIndication"); [cla performSelector:selector withObject:nil]; //以上三行的實際效果就至關於[AlertTool showLoadingProgressIndication];
因此咱們能夠將中間件與runtime結合,根據指定類名和方法名讓中間件實現相應功能:
這樣模塊間的關係就變成了這樣了:
這樣的話,只讓模塊對中間件依賴,中間件徹底不依賴任何模塊,咱們所說的解耦合其實也就是這種效果。每一個模塊的負責人都不用再擔憂另外一個模塊如何,只須要和中間層進行溝通便可。(CTMediator就是這個中間件)
這個方案很好的解決了耦合問題,可是還存在一個問題,那就是咱們是直接告訴中間件哪一個類名哪一個方法名的,可是在實際多人開發中,咱們是不知道其餘人寫的組件類名和方法名是什麼的?
因此這就須要組件的開發者提早將方法名暴露出來,也就是每一個組件建立一個target類(由組件開發者維護),其內部定義了組件對外暴露的action(方法)。和組件通訊時,其實質是調用一個特定的target-action
的方法。target
類的類名必須以Target_
開頭,好比Target_A
,action
的方法名必須以Action_
開頭,好比Action_nativeFetchDetailViewController
。注意,暴露出來的這個target類並非這個組件的具體實現,它只是爲了方便調用者使用,target類的實現文件中會引入組件的頭文件,實現聲明文件中的功能,從而達到調用組件的目的。
@interface Target_A : NSObject - (UIViewController *)Action_nativeFetchDetailViewController:(NSDictionary *)params; @end
//Target_A.m #import "Target_A.h" #import "DemoModuleADetailViewController.h" @implementation Target_A //這裏須要注意的一點是 由於咱們是經過runtime來調方法的 參數也是經過params傳遞進來的字典 因此在Action_方法中的參數就是字典,字典中能夠包含咱們須要的值
- (UIViewController *)Action_nativeFetchDetailViewController:(NSDictionary *)params
{
// 由於action是從屬於ModuleA的,因此action直接可使用ModuleA裏的全部聲明
DemoModuleADetailViewController *viewController = [[DemoModuleADetailViewController alloc] init];
viewController.valueLabel.text = params[@"key"];
return viewController;
}
@end
另外,咱們經過查看CTMediator的源碼發現還有如下兩個注意點:
// 源碼中拼接方法名的時候都加上了:因此這就致使咱們在實現Action_方法的時候都要寫上參數dic,固然寫上的話咱們在調用的時候傳nil就行 反正也不會用到 NSString *actionString = [NSString stringWithFormat:@"Action_%@:", actionName]; //NSSelectorFromString是動態加載實例方法 因此這就要求Action_方法都得是實例方法,不能是類方法 SEL action = NSSelectorFromString(actionString);
這樣的話每一個部分都是單獨的了,中間件涉及不到其餘引用,能夠拿出來放到其餘項目中用,組件也涉及不到其餘引用,能夠拿出來放到其餘項目中用,開發者只須要根據暴露出來的target-action
去中間件中調用就行。
UIViewController *viewController = [[CTMediator sharedInstance] performTarget:@"A" action:@"nativeFetchDetailViewController" params:@{@"key":@"value"} shouldCacheTarget:NO];
這樣,咱們就能夠經過中間件進行調用了,注意的是經過中間件調用不須要寫暴露出來的類名和方法名的前綴,也就是Target_和
Action_。
②其餘方案:
URL(蘑菇街),這種方式沒有用過,想要了解的能夠看一下下面幾篇文章
在組件化的實際開發中,咱們能夠經過上面的流程去開發,好比咱們仍然拿alertTool這個組件來講,
1.這個附件依賴了CTMediator和SDWebimage兩個庫,因此咱們要在spec文件中進行配置;
2.組件代碼中要用到一些圖片資源,咱們放到Accests文件中,組件中使用這些資源的時候出了在spec文件打開資源屬性外,還要注意調用的位置是在本身組件內的bundle中;
3.組件的實現代碼,咱們也能夠分爲123三個部分,1是代碼的具體功能實現,2是將代碼的類名和方法名都暴露出來給調用者使用,這裏面須要注意的是Action_方法都是實例方法且都得有參數(參數咱們通常都選擇NSDictionary),3就是寫一個CTMediator的分類,這個分類中是對CTMediator調用過程的一個封裝,這樣能夠更加方便調用者使用。2和3都是須要組件開發者來建立維護的。
調用者的使用:簡單方便
#import "ViewController.h" #import <alertToolLib/CTMediator+alertTool.h> @interface ViewController () @end @implementation ViewController - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{ [[CTMediator sharedInstance]alertUserMes:@"儂好"]; }
這只是舉了一個小例子,若是想要再稍微複雜點的例子,能夠參考下面的這個demo
經過組件化後,整個項目的結構就變成了這樣⬇️⬇️↓↓
關於插件化、模塊化、組件化三者通訊方式的區別(僅供瞭解)
通訊方式
模塊化的通訊方式,無非是相互引入;我抽取了common, 其餘模塊使用天然要引入這個module
組件化的通訊方式,按理說能夠劃分爲多種,主流的是隱式和路由。隱式的存在使解耦與靈活大大下降,所以路由是主流
插件化的通訊方式,不一樣插件自己就是不一樣的進程了。所以通訊方式偏向於Binder機制相似的進程間通訊