前言:本人也是初次接觸組件化開發,感受現有的資料太繁雜,就簡單整理了一下,在此跟你們分享一些入手的經驗,主要就是描述cocoapods的私有庫封裝和提交。組件化開發是個大的議題,涉及到架構思路、設計模式應用、項目經驗、工具的使用,因此在此只是作一個開始,後面還會作進一步的拓展和深刻,儘可能作到乾貨,歡迎探討和糾正。html
目錄:ios
1、 什麼是組件化開發git
一、 概述github
將項目按照功能、業務等進行拆分,在開發過程當中針對不一樣的場景,將「組件」進行「組裝」。json
二、 爲何使用組件化設計模式
(1)界面、邏輯和功能拆分,實現解耦xcode
(2)經過pod管理,很方便的實現安裝和卸載緩存
(3)對於較大項目而言,增強了項目的擴展性、組件複用性和設計統一性網絡
(4) 能夠方便按照組件進行測試架構
(5) 多人開發時能夠更清晰的管理開發任務
(6) 解決項目臃腫,編譯時間過長
三、 場景及經常使用的實現方式
(1) 曾經的經常使用方式:
a. 目錄結構管理:這是最原始的方式,僅僅經過目錄結構實現代碼層次的清晰化。但本質上並無解決代碼之間的依賴混亂的狀況,模塊化劃分也很是不清晰。
b. 子工程:經過子工程能夠實現代碼依賴管理和模塊化,可是須要引入複雜的設置,不利於管理。
c. 靜態庫:將依賴代碼打包成爲靜態庫.a或者Framework,不過因爲不能看到源碼,調試很是不方便。
(2)如今較多的使用:cocoapods,使用它來管理私有庫,從而實現了代碼模塊化管理
2、 組件化的核心內容
一、模塊拆分
(1)一個普遍受用的模塊化方式:
a. 基礎模塊:項目中最基礎的代碼抽取,不牽扯具體功能的封裝,好比一些必要的分類,常量定義,宏命令等,這個模塊是要被其餘模塊所依賴的。
b. 功能模塊:功能封裝的抽取,好比網絡請求、正則匹配、定位功能等。
c. 業務模塊:面向業務的總體模塊,好比訂單、我的中心、首頁等。可能會依賴基礎和功能模塊。
(2)圖示
二、模塊間通訊
(1)組件化最大的特色之一是「模塊化」,解耦是核心話題。模塊間儘可能不要直接引用造成依賴關係(可是業務模塊和基礎模塊之間的引用不可避免)。
(2)採用router 的方式進行模塊間的通訊。router能夠理解爲一個爲了解耦而實現的中間鍵。
(3)這種通訊方式經常採用一種設計模式:命令模式。 使用target + action 的方式響應命令。下面會附上一個簡單demo。
三、CocoaPods遠程私有庫:將咱們的組件作成私有庫,經過cocoaPods進行管理。
四、宿主工程:就是咱們整體的工程項目,各個模塊的「組裝地」,控制模塊間的通訊,配置通常的環境變量等等任務。
五、組件化的準備工做:基礎與工具
(1) 瞭解遠程代碼倉庫的管理工具,本文不會描述如何建立遠程倉庫。
(2) 瞭解cocoapods使用。
(3) 瞭解封裝、解耦的概念和做用。
(4) 熟悉Git命令行(習慣使用SourceTree的同窗儘可能仍是熟悉一下命令行)。
3、模塊間通訊的簡單Demo --- 命令模式:router+target+action
一、demo的文件結構,兩個模塊,一個路由,一個target
二、代碼
(1)兩個業務模塊
@implementation HomePageVC - (void)viewDidLoad { [super viewDidLoad]; } -(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{ /* 跳轉到詳情頁面並實現跳轉後的相應邏輯 使用url傳遞「命令」:http://detail/showLog:?vc=HomePageVC target 是 detail (添加了前綴Target) action 是 showLog(添加了前綴action) */ Router * router = [[Router alloc]init]; [router openURL:@"http://detail/showLog?currentVC=HomePageVC"]; } @end @implementation DetailVC - (void)viewDidLoad { [super viewDidLoad]; self.view.backgroundColor = [UIColor orangeColor]; } -(void)showView:(NSString*)str{ NSLog(@"處理detailVC的邏輯"); } @end
@implementation DetailVC - (void)viewDidLoad { [super viewDidLoad]; self.view.backgroundColor = [UIColor orangeColor]; } -(void)showView:(NSString*)str{ NSLog(@"處理detailVC的邏輯"); } @end
(2)router
@interface Router : NSObject /* 經過傳遞urlStr 將target、action、參數傳進來 */ - (id)openURL:(NSString*)urlStr; @end @implementation Router - (id)openURL:(NSString*)urlStr{ NSURL *url = [NSURL URLWithString:urlStr]; NSMutableDictionary *params = [[NSMutableDictionary alloc]init]; //查詢http攜帶的參數 NSString *parameterString = [url query]; //切割字符串,轉換爲鍵值對 NSArray * parameterArr = [parameterString componentsSeparatedByString:@"&"]; for (NSString *param in parameterArr) { NSArray *elts = [param componentsSeparatedByString:@"="]; if (elts.count<2) continue; id firstElt = [elts firstObject]; id lastElt = [elts lastObject]; if (firstElt && lastElt) { [params setObject:lastElt forKey:firstElt]; } } NSString *actionName = [url.path stringByReplacingOccurrencesOfString:@"/" withString:@""]; if ([actionName hasPrefix:@"native"]) { return @(NO); } //將 target 、action 和 參數傳遞過去 id result = [self performTarget:url.host action:actionName param:params]; return result; } - (id)performTarget:(NSString *)targetName action:(NSString *)actionName param:(NSDictionary *)para { //OC反射機制,獲取class 和 方法名(方法名要注意是否有冒號) NSString *targetClassString = [NSString stringWithFormat:@"Target_%@",targetName]; NSString *actionMethondString = [NSString stringWithFormat:@"action_%@:",actionName]; Class targetClass = NSClassFromString(targetClassString); //肯定target 和 action NSObject *target = [[targetClass alloc] init]; SEL action = NSSelectorFromString(actionMethondString); // 排除不響應的狀況 if ([target respondsToSelector:action]) { return [self safePerformAction:action target:target param:para]; } else { SEL action = NSSelectorFromString(@"notFound:"); if ([target respondsToSelector:action]) { return [self safePerformAction:action target:target param:para]; } else { return nil; } } return nil; } /** 調用指定對象的指定方法完成命令的響應 @param action 方法 @param target 目標對象 @param para 參數 @return 返回值 */ - (id)safePerformAction:(SEL)action target:(NSObject *)target param:(NSDictionary *)para { //方法簽名 NSMethodSignature *methodSig = [target methodSignatureForSelector:action]; if (methodSig == nil) { return nil; } // 獲取這個方法返回值 const char *retType = [methodSig methodReturnType]; NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig]; [invocation setArgument:¶ atIndex:2]; [invocation setTarget:target]; [invocation setSelector:action]; [invocation invoke]; // id是能夠返回任意對象,因此單獨處理基本變量: NSInteger Bool ... if (strcmp(retType, @encode(NSInteger)) == 0|| strcmp(retType, @encode(BOOL)) == 0|| strcmp(retType, @encode(CGFloat)) == 0) { NSInteger result = 0; [invocation getReturnValue:&result]; return @(result); } else if (strcmp(retType, @encode(void)) == 0){ return nil; } else{ id result; [invocation getReturnValue:&result]; return result; } } @end
(3) target
@interface Target_detail : NSObject -(void)action_showLog:(NSDictionary*)parameter; -(void)notFound:(NSDictionary*)parameter; @end @implementation Target_detail -(void)action_showLog:(NSDictionary*)parameter{ DetailVC * detailVC = [[DetailVC alloc]init]; [detailVC showView:parameter[@"currentVC"]]; } -(void)notFound:(NSDictionary*)parameter{ NSLog(@"Target_detail 中%@方法未找到",parameter); } @end
4、Cocoapods 原理
一、經過幾個名詞瞭解原理:
(1)框架描述文件 —— spec , 最重要的文件
json文件,包含框架名稱、版本、框架的源碼地址等信息,每個框架都要有對應的spec,咱們建立本身的私有庫也要建立spec文件,好比:
(2)遠程索引庫: 遠端存放spec 文件的庫,cocoapods在github上的索引庫以下:
(3)本地索引庫: pod setup 以後,將遠程索引庫拷貝到本地。能夠在本地查看:
(4)本地索引庫的檢索文件:
a.本地索引庫會生成一個「檢索時用來索引的文件」
b.命令 pod search <NAME> 檢索的是這個文件
c.文件位置如圖:
(5)本地緩存:用pod安裝一個庫以後,會在cache文件夾裏生成對應的緩存,以下:
二、cocoapods原理綜述:總結上面的幾個名詞的關係和做用
(1)圖示
(2)若是遠端有更新,本地索引庫須要和遠程索引庫保持一致,這個過程須要手動更新,不在上面的圖示之中。
三、git 和pod的一些經常使用命令
(1)git命令:
git init :初始化git倉庫 git add . :將當前文件夾下的全部文件提交的git緩衝區 git status :顯示狀態 git commit -m ‘註釋’ :提交代碼到本地倉庫 git log :查看記錄 git push :推送遠端倉庫 git push <主機名> <分支名> :推送遠端分支 git push <主機名> ‘版本號’ :按照版本號推送到遠程 git remote :查看遠程主機名 git remote add <NAME> <URL> :關聯遠程倉庫 git tag -a ‘版本’ -m ‘描述’ :打標籤 git push --tags :提交標籤到遠程倉庫 git repo add master :查看倉庫地址,添加本身的索引庫 git branch :查看分支 git clean -f :刪除沒有被add的文件
(2)pod命令:
pod update :不會參照podfile.lock,直接檢測新版本,並更新pod庫。 pod install :會參照podfile.lock (裏面記錄了各個pod庫的版本),進行下載(不更新)。若是podfile.lock裏面沒有,則參照podfile。 pod spec create <名字> :手動建立spec文件 pod trunk register <郵箱> '名字' --description='描述' :註冊trunk,用於上傳spec到遠程索引庫 pod lib lint :本地驗證pod庫可否經過驗證 pod repo :得到pod本地/遠程索引庫 pod spec lint :本地/遠程驗證pod庫可否經過驗證
四、其餘
(1)cocoapods打包的私有庫,默認兩層文件夾(全部文件放到一個目錄裏),下面會介紹私有庫文件夾結構分層的方式。
(2)cocoapods打包私有庫時,能夠添加對其餘第三方框架的依賴,具體方式看下文的spec文件字段舉例。
(3) 使用git管理項目的時候,是否須要將pod文件上傳? 這個根據具體狀況來判斷,gitignore文件中能夠設置是否忽略cocoapods,可是不管是否忽略,都建議podfile和podfile.lock這兩個文件要上傳到遠端,來保證多人開發中pod的正確使用。
5、 使用cocoapods製做私有庫的通常流程詳述
<1> 在github上(cocoapods遠端)建立本身的框架,也就是將庫發佈到cocoapods上
一、 建立框架項目,完成框架的代碼。
二、在github上建立代碼倉庫,導入框架項目到遠程倉庫。
三、須要給代碼倉庫打上tag,而且將tag提交到遠端(不然後面提交spec會報錯)
四、在框架目錄下建立spec文件(框架描述文件)
(1)手動建立spec:pod spec create <名字>
(2)字段詳細描述:https://guides.cocoapods.org/syntax/podspec.html
(3)舉例:
Pod::Spec.new do |s| # ――― 基本信息 ――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # # homepage倉庫主頁(注意,是主頁地址,不是完整的倉庫地址,不然有可能報錯) # license 開源協議,通常是'MIT',還有 'BSD' and 'Apache License, Version 2.0'等 # platform 框架支持的最低平臺版本 s.name = "TestSpec" s.version = "0.0.1" s.summary = "簡短描述" s.description = "詳細描述(要比summary長)" s.homepage = "http://EXAMPLE/TestSpec" s.license = "MIT" # s.license = { :type => "MIT", :file => "FILE_LICENSE" } s.author = { "Zhang Qi" => "zhangqi@163.com" } # s.author = "Zhang Qi" # s.platform = :ios #s.platform = :ios, "9.0" # ――― 資源路徑 ――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # # 框架的資源路徑:路徑能夠指向遠端代碼庫,也能夠指向本地項目,例如: # 1.指向遠端代碼庫: { :git => "git@git.oschina.net:xxx/xxx.git", :tag => "1.0.0" } # 2.指向本地項目: { :path => 'TestSpec', } # 3.版本號和上面的version對應 s.source = { :git => "http://EXAMPLE/TestSpec.git", :tag => "#{s.version}" } # ――― 被引入的文件 ――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # # 框架被其餘工程引入時,會導入對應目錄下的.h和.m文件 # 1.描述在s.source路徑下,文件下的對應文件夾 # 2.能夠起到「過濾」的做用,* 爲通配符,好比:Classes/{ZQ,UI}*.{h.m} ,這個是表示ZQ和UI開頭的h和m文件 s.source_files = "Classes", "Classes/**/*.{h,m}" # s.resource = "icon.png" # s.resources = "Resources/*.{png,jpg,xib,storyboard,xcassets}" # ――― 依賴的庫和框架 ――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # # s.framework = "SomeFramework" # s.frameworks = "SomeFramework", "AnotherFramework" # s.library = "iconv" # s.libraries = "iconv", "xml2" # ――― 其餘設置 && 目錄結構 ――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # # 1.dependency 表示依賴的第三方庫,會在導入本庫的同時,導入依賴庫 # 2.subspec 經過設置子庫實現文件夾的層級結構,同時能夠單獨引入子庫 # s.requires_arc = true # s.dependency "JSONKit", "~> 1.4" #s.subspec 'Category' do |c| #c.source_files = 'MyLib/Classes/Category/**/*' #end end
五、註冊trunk 帳號(若是是第一次)
(1)命令行:pod trunk register <郵箱> '名字'
(2)進入郵箱進行驗證:複製並打開裏面提供的連接
六、 提交spec文件到github上(cocoapods):
(1)確保本地倉庫已經commit
(2)pod trunk push xxxx.podsepc
七、搜索剛剛提交的框架
(1)搜索不到:search_index 文件裏面沒有新框架的信息
(2)能夠將search_index刪除,而後從新pod search 或者pod setup
<2> 建立本地私有庫,有時咱們的私有庫並不想讓使用者從遠端加載
一、建立本地工程項目,完成項目的代碼
二、建立本地框架,完成框架的代碼
三、在框架目錄下建立spec文件(框架描述文件)
(1)手動建立spec:pod spec create <名字>
(2)修改spec ,由於是本地私有庫,因此,字段和上面的遠程庫有所不一樣,主要是s.source 裏面的git路徑能夠刪掉了
四、在本地的工程項目中,引用本地的私有庫
(1)pod init 本地的工程項目
(2)更改podfile文件,添加本地的私有庫,好比: pod ‘TestLib’
(3)配置podfile 中引用的私有庫路徑:pod ‘TestLib’ ,:path => ‘../TestLib’ ,這裏須要根據TestLib的真實位置,填寫path路徑。這個路徑是spec文件相對於podfile文件的路徑,其中 ../ 表示向上一級。
(4)(可選)若是須要提交本地私有庫,能夠按照遠端建立私有庫的方式,將本地的私有庫上傳。可是,本地的工程項目,仍是使用的本地引用的方式,導入的私有庫。
(5) 回到本地工程項目的目錄,pod install
(6)安裝完畢後,在本地工程項目中,觀察pods文件夾:
(7)若是本地私有庫裏面發生了更新,直接在本地工程項目裏面pod install便可安裝。
<3> 使用其餘遠程倉庫建立本身的私有庫(好比碼雲)
一、建立本地工程項目的代碼。
二、在碼雲上建立項目倉庫(總體工程項目的遠端倉庫)。
三、在碼雲上建立spec倉庫(私有遠程索引庫),這個倉庫不是用來存放代碼的,是放置spec的,相似cocoapods在github上的遠程索引庫(上文有介紹)。
四、處理本地索引庫:須要添加私有的索引庫
(1) pod repo 能夠查看當前的索引庫
(2)建立私有本地倉庫:pod repo add <NAME> <遠程倉庫url>
五、 建立私有庫框架的代碼,這裏有兩種方式:
(1)正常建立框架的文件,而且爲了測試方便,添加一個測試用的工程項目。下面兩張圖,就是整個私有庫的內容,包含一個Lib,一個TestProject,這個私有庫經過git管理源碼:
上圖爲Lib文件夾
上圖爲測試項目結構
(2)使用cocoapods提供的模板: 進入Lib文件夾,使用命令 pod lib create <NAME>,直接建立一個帶有一、框架源碼 二、測試工程 三、spec文件的模板。結構以下圖:
當使用pod lib create <NAME> 時,會有提示選項出現,以下圖:
建立好的模板,用xcode打開(example裏的工程),以下圖:
六、 在碼雲上建立框架倉庫(私有框架的源代碼倉庫),而且導入上一步建立好的Lib
(1) 在Lib目錄git init
(2) git add 和git commit
(3) git remote add <主機名><URL> 添加倉庫源
(4) git push –u <主機名><分支名> 提交到遠程
七、修改私有庫的spec文件(文件位置在上面第5步中已經標註),以下圖:
八、給私有庫打tag,而且提交到遠端。這一步是必須的,並且tag和上面的spec要一致,否則下面會報錯。
九、提交前的準備:檢測spec文件,須要兩步
(1) pod lib lint 檢測本地spec文件是否有問題,此時不檢測source字段是否正確。
(2) pod spec lint 檢測遠端spec是否有問題。後面咱們會將spec提交到本地,而後會推到遠端,這是很重要的同步遠端的機制。
十、提交spec文件
(1) pod repo push <新建的本地索引庫> <spec文件> , 新建的索引庫在上面第四步中已經建立了。
(2) 若是出現問題,須要到索引庫裏面git clean –f 清除一下,或者刪除並重建一下本地索引庫。
十一、使用pod search 檢索私有庫,若是搜不到,效仿上面介紹的,刪掉search_index文件,從新檢索。
十二、在項目中引入遠程私有庫
(1) pod repo 查看索引庫而且記錄下新建的私有庫的url。這個url是遠程索引庫的地址(spec的倉庫地址),並非私有庫源碼的地址。
(2)因爲默認狀況,podfile裏面指向的遠程索引庫是在github上的,因此,咱們須要添加新的「源」。在podfile頂部,增長添加上一步記錄的url : source '遠程索引庫的url' ,若是須要同時引用github上面的其餘庫,還要再添加github上的索引庫地址,方法同第一步(這個地址默認是不用添加的)。
(3) 在podfile中間,加入要引入的私有庫,不用再使用path => ‘本地路徑’了。
(4) pod install 加載私有庫,觀察此時項目中的pods文件夾結構:
6、小結
本文比較詳細的描述了用cocoapods封裝私有庫的方式,組件化相關的知識還有不少沒有涉及到,包括資源包的封裝,Framework,block在組件間通訊時的使用等等,還有不少有關設計模式和架構的思路沒有深刻探討。我的認爲,關於組件化,仍是根據業務需求和產品體量來運用,網上也有不少能夠借鑑的成型的解決方案,不要爲了組件化而組件化,這時有必定設計模式的知識積累仍是頗有幫助的。