iOS組件化開發入門 —— 提交本身的私有庫

 

前言:本人也是初次接觸組件化開發,感受現有的資料太繁雜,就簡單整理了一下,在此跟你們分享一些入手的經驗,主要就是描述cocoapods的私有庫封裝和提交。組件化開發是個大的議題,涉及到架構思路、設計模式應用、項目經驗、工具的使用,因此在此只是作一個開始,後面還會作進一步的拓展和深刻,儘可能作到乾貨,歡迎探討和糾正。html

 

目錄:ios

  • 什麼是組件化開發
  • 組件化的核心內容
  • 模塊間通訊的簡單Demo
  • Cocoapods 原理
  • 使用cocoapods製做私有庫的通常流程詳述
  • 小結

  

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
homePage

 

@implementation DetailVC

- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor orangeColor];
}
-(void)showView:(NSString*)str{
    NSLog(@"處理detailVC的邏輯");
}

@end
detail

(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:&para 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
router

  (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
target

 

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
spec字段舉例

  五、註冊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在組件間通訊時的使用等等,還有不少有關設計模式和架構的思路沒有深刻探討。我的認爲,關於組件化,仍是根據業務需求和產品體量來運用,網上也有不少能夠借鑑的成型的解決方案,不要爲了組件化而組件化,這時有必定設計模式的知識積累仍是頗有幫助的。

 

 

相關文章
相關標籤/搜索