iOS 組件化

理論篇

什么是組件化

組件化開發(fā)就是將一個臃腫的、單一的項(xiàng)目,根據(jù)功能/業(yè)務(wù)/技術(shù)等等進(jìn)行拆分,形成一個個獨(dú)立的功能組件,然后借助 CocoaPods 管理工具將其任意組合,集成一個完整的項(xiàng)目。

你可以將 AFNetworking、SDWebImage 等等三方庫理解為自己項(xiàng)目的一部分,屬于基礎(chǔ)組件部分,而我們要做的就是將項(xiàng)目劃分成多個獨(dú)立功能模塊,再集成為一個完整的項(xiàng)目。這一過程看似多此一舉,但是帶來的優(yōu)勢卻是非常大。

為什么需要組件化

項(xiàng)目初期,功能相對簡單,普通的MVC+模塊文件分割就可以滿足絕大部分的需求。但是隨著功能需求越來越多,業(yè)務(wù)越來越復(fù)雜多樣,現(xiàn)有的架構(gòu)已經(jīng)不太適用了,即使使用了 Git 分支管理,依然經(jīng)常發(fā)生合并沖突等等問題,另外后期的維護(hù)成本也大大增加,業(yè)務(wù)邏輯變得復(fù)雜、模塊之間耦合度很大、查找問題效率變低、項(xiàng)目編譯過程過慢…… 而且伴隨著開發(fā)人員的增多(多個小組之間協(xié)作開發(fā)),這些問題尤為突出,優(yōu)化開發(fā)結(jié)構(gòu)變得非常重要。

中間層

針對上面的問題,第一個想到的優(yōu)化就是新增一個中間層來協(xié)調(diào)各個模塊之間的調(diào)用,所有的模塊都通過這個中間層去實(shí)現(xiàn)調(diào)用和交互,但是這樣雖然一定程度上降低了模塊與模塊的之間的耦合度,但是耦合都轉(zhuǎn)嫁到了中間層上了,并且中間層的改動只能由一個人操作,否則非常容易發(fā)生沖突,本質(zhì)上并沒有發(fā)生多少變化。另外一點(diǎn),查找問題的效率低下、編譯過慢等問題依舊沒有得到有效的解決。

這是傳統(tǒng)的中介者模式,這個中間層會依賴其他組件,其他組件也會依賴中間層完成服務(wù)。

組件化

組件化能夠幫助我們將過大的項(xiàng)目拆解成數(shù)個小組件,開發(fā)者只需要關(guān)注于組件所依賴的其他組件,而無需關(guān)心完整項(xiàng)目的其他部分,每個組件可以自己采取所習(xí)慣的架構(gòu)模式:MVC、MVVM、MVCS等等,就像開發(fā)一款個人獨(dú)立的app那樣自由。

項(xiàng)目組件化之后所帶來的好處是非常多的,我們先總結(jié)一下非組件化所造成的問題:

非組件化:

  • 代碼高耦合度、高依賴
  • 項(xiàng)目復(fù)雜、臃腫、編譯過長(影響調(diào)試)
  • 難以融合/集成其他產(chǎn)品
  • 需要統(tǒng)一架構(gòu)
    ……

組件化:

  • 代碼復(fù)用性提高,可方便的集成到其他項(xiàng)目
  • 項(xiàng)目可配置,方便集成和功能回退(指定版本)
  • 化整為零,將項(xiàng)目細(xì)小化
  • 方便組件的并行開發(fā)
  • 可方便做單元測試
  • 組件自由度高,即插即用
    ……

當(dāng)然組件化也有著它的缺點(diǎn),對已有的項(xiàng)目實(shí)施組件化架構(gòu)比較困難,耗費(fèi)時間長,項(xiàng)目組成員需要一定學(xué)習(xí)成本;組件化并沒有相應(yīng)的標(biāo)準(zhǔn),拆分的粒度要適中,拆分粒度過高,則讓項(xiàng)目變得復(fù)雜,起到了反作用效果,反之,粒度過低,體現(xiàn)不了組件化的優(yōu)勢,在項(xiàng)目業(yè)務(wù)不斷地添加的過程中,進(jìn)行不斷的嘗試調(diào)整,找到適合自己項(xiàng)目的才是最好的。

組件化的分層

項(xiàng)目組件化中,最難把握的就是粒度問題,這需要開發(fā)的自己的經(jīng)驗(yàn)去把控。這里只給出個人認(rèn)為的層次的劃分。

【基礎(chǔ)組件】:宏定義/常量/自定義工具類,如常用的自定義分類
【功能組件】:項(xiàng)目中所用到的功能,如地圖定位/消息推送/分享等
【業(yè)務(wù)組件】:項(xiàng)目中的模塊/業(yè)務(wù),如聊天室/直播間/個人中心等
【中間組件】:負(fù)責(zé)項(xiàng)目中的路由/消息通知/傳參/回調(diào)等
【宿主工程】:項(xiàng)目容器,用來集成組件,調(diào)整各個組件之間的消息傳遞的容器。

中間層的幾種方案

在組件化過程中,中間層是各個組件的通信的橋梁,中間層在組件化過程中扮演著非常重要的角色。目前關(guān)于中間層的設(shè)計筆者已知的有以下三種方式:基于URL Scheme的路由、基于Runtimetarget-action、面向接口。

  • 路由

iOS 中支持的 URL Scheme 讓我們能夠在應(yīng)用之間、應(yīng)用內(nèi)部傳遞消息。日常開發(fā)過程中經(jīng)常用到的就是調(diào)用系統(tǒng)服務(wù)、喚起三方app等等,這些屬于應(yīng)用之間的消息傳遞,而我們這里借助 URL Scheme 完成應(yīng)用內(nèi)部的消息傳遞。這里的路由 URL 遵循網(wǎng)上通用的資源標(biāo)識符合 URI,如:appscheme://home/scan?param=value,我們通過 URL 來傳遞信息,下層服務(wù)方通過 URL 獲取參數(shù)提供服務(wù),上層消費(fèi)者通過 URL 獲取到服務(wù),完成調(diào)用。

基于 URL Scheme 的三方庫

JLRoutes 是一種基于 URL Scheme 的路由框架,它全局會保存一個Map,key 是路由協(xié)議 url,value 則是對 url 解析后 block 回調(diào),你可以在該回調(diào)中處理具體的業(yè)務(wù)。

實(shí)例:

例如我們的路由協(xié)議定義如下:

scheme://描述/打開方式/保留字段/功能標(biāo)識?參數(shù)1=值1&參數(shù)2=值2
||
myroute://market/1/route/cjpm?stockcode=600212.ss

首先配置路由 url 和 對應(yīng)的回調(diào)處理:

/// 默認(rèn)下都會進(jìn)入這里,這里填寫路由匹配規(guī)則
[JLRoutes.globalRoutes addRoute:@"/market/:operate/route/:code" handler:^BOOL(NSDictionary<NSString *,id> * _Nonnull parameters) {
    NSLog(@"%@", parameters);
    // 接下來的業(yè)務(wù)邏輯
    return YES; // 返回YES,表示處理截止,后面的路由規(guī)則不再啟用
}];

然后在需要路由的地方傳入相應(yīng)的路由 url :

// 某地方獲取到的url
NSURL* url = [NSURL URLWithString:@"myroute://market/1/route/cjpm?stockcode=600212.ss"];
// 處理路由
[JLRoutes routeURL:url];

基于Runtimetarget-action

相比 url scheme 的提前注冊、實(shí)現(xiàn)服務(wù),CTMediator 借助 OC 運(yùn)行時的特性,現(xiàn)實(shí)組件之間服務(wù)的自動發(fā)現(xiàn),無需提前注冊即可實(shí)現(xiàn)組件間的調(diào)用,因此,這種方案的可維護(hù)性、可讀性、擴(kuò)展性相對較高。

官方的 Demo 中,結(jié)構(gòu)是這樣的:

Demo 結(jié)構(gòu)

CTMediator 的使用流程大體是這樣的:

底層組件

  1. 創(chuàng)建 Target_ 開頭的目標(biāo)類,如Target_A(該類是為了讓中間件 CTMedator 通過 NSClassFromString生成類),類中定義 Action_ 開頭的可調(diào)用的方法(為了讓中間件 CTMedator通過 NSSelectorFromString 生成方法器),并且這些方法都有一個字典類型參數(shù)接收調(diào)用者傳遞過來的信息。

  2. 創(chuàng)建 CTMedator 的分類(方便擴(kuò)展、分塊),此分類對應(yīng)著Target_A,分類中定義該組件對外(調(diào)用者)開放的 API 方法,該組件的開發(fā)者需要使用 CTMedator 的核心方法 performTarget:action:params:shouldcacheTarget: 完成方法調(diào)用。

上層組件

導(dǎo)入對應(yīng) CTMedator 的分類,完成方法調(diào)用。

相比傳統(tǒng)的中介者模式,這種 target-action 方案解放了中間件對其他組件的依賴,因?yàn)樗峭ㄟ^ NSClassFromStringNSSelectorFromString 來生成類的實(shí)例和方法器SEL的,然后介入消息的分發(fā)機(jī)制完成消息分發(fā)的,即所謂的主動發(fā)現(xiàn)服務(wù)。傳統(tǒng)的中介者模式中,中間件和其他組件是雙向依賴的:

傳統(tǒng)的中間層,圖來自網(wǎng)絡(luò)

target-action 方式則是單向依賴,這樣做的一個好處就是降低了一定的耦合,在我們移除某個組件時,中間件無需進(jìn)行改動。

使用 CTMedator 之后,圖來自網(wǎng)絡(luò)

那么,由于沒有引入具體的類,而是通過字符生成對應(yīng)的類和方法,那么關(guān)于 CTMedator 的分類要清楚的知道 Target_ 類以及其中的內(nèi)容。

CTMedator 的分類可以劃分為一個組件,必要時,集成到項(xiàng)目中進(jìn)行調(diào)用。

  • 面向接口 Protocol

在路由和 target-action 方案中,都存在硬編碼問題、參數(shù)不明確問題:URL 、Target_Action_ 的硬編碼,參數(shù)都是通過字典的形式傳遞,類型不明確。

面向接口 的方式能夠很好的解決這兩個問題。面向接口的方案通常由兩部分組成,一個是用來管理接口協(xié)議的類(ModuleManager),一個是具體的接口協(xié)議(ComponentProtocol)。

ModuleManager 負(fù)責(zé)消息的調(diào)用和轉(zhuǎn)發(fā),它內(nèi)部需要存儲一張映射表,完成 Protocol -> Class 的工作。ComponentProtocol 文件定義了業(yè)務(wù)組件可以提供的功能服務(wù),可以將所有服務(wù)都定義到其中,也可以按組件劃分。這樣所有調(diào)用方只需要依賴中間件,不需要依賴其他的組件,而中間件通過接口協(xié)議綁定可以用于服務(wù)的類,即每個組件有一個用于實(shí)現(xiàn)對外提供的接口協(xié)議的類。在編譯時,將對應(yīng)的類注冊到ModuleManager 中,Protocol 的名稱即為查找的 key。

注冊綁定:

[ModuleManager registerClass:User forProtocol:@protocol(UserProtocol)];

調(diào)用時通過接口協(xié)議從 ModuleManager 中映射出注冊的 Class,將獲取到的 Class 實(shí)例化,并調(diào)用協(xié)議方法完成服務(wù)調(diào)用。

Class cls = [[ModuleManager sharedInstance] classForProtocol:@protocol(UserProtocol)];
NSObject <UserProtocol> *user = [[cls alloc] init];
NSString *userName = [user getUserName];

接口協(xié)議的方式雖然可以很好的解決參數(shù)類型的不確定性,硬編碼問題(實(shí)現(xiàn)部分可以任意替換),但是它不是前面兩種的替代品,因?yàn)樗麄兌加凶约旱膫?cè)重點(diǎn),如 路由URL 可以在應(yīng)用之間實(shí)現(xiàn)消息傳遞,面向接口可以用來為某類添加功能或者對類進(jìn)行功能約束等。

一些注入框架是支持面向接口的注入的,可使用這些庫取代 ModuleManager 類。

小結(jié)

三種方式都分為底層服務(wù)方和上層使用方,服務(wù)方都對外提供 了服務(wù)媒介,CTMediator 中是 Target_A 文件,面向接口就是 Protocol,路由 URL Scheme 則是回調(diào) block。

在三種方式中,個人覺得最不推薦的是 CTMediator 方案,感覺很是臃腫,雖然可以通過多個分類去定義組件,但是實(shí)際上對底層組件的調(diào)用邏輯都耦合在了中間件中,這意味著中間件需要頻繁的進(jìn)行更新,另外存在太多的硬編碼地方,target、action以及參數(shù)名都是硬編碼在中間件中的,這樣的方式并不靈活。但是 CTMediator 中通過運(yùn)行時解耦了中間件對底層組件的依賴,以及去 model 化的想法還是非常好的。

面向接口 Protocol 的方案貫穿了底層組件、中間件以及上層組件,一方面解耦了中間件對底層組件的耦合,底層組件變得透明,可以根據(jù)接口協(xié)議任意替換,另一方面接口協(xié)議還確定了參數(shù)類型。但是該方案面向的是應(yīng)用內(nèi)部的功能通信,外部調(diào)用應(yīng)用時,還是需要路由或者硬編碼的形式完成。

路由定義了一套用于信息傳遞的標(biāo)準(zhǔn),通過路由,服務(wù)方可以注冊并實(shí)現(xiàn)符合某種特定條件的服務(wù),使用方則通過中間件傳遞 一條URL 來調(diào)用該服務(wù)。服務(wù)方和使用方彼此透明,可以任意替換。和接口協(xié)議比起來,路由的可以處理本地內(nèi)部和遠(yuǎn)程外部的兩種類型的調(diào)用,缺點(diǎn)是 url 需要硬編碼,而且參數(shù)類型都是字符。路由 URL 和接口協(xié)議都需要提前注冊才能使用,路由需要 block,接口協(xié)議需要 class。

路由和接口協(xié)議并不沖突,可以使用路由 + 協(xié)議的方式來實(shí)現(xiàn)中間件,路由實(shí)現(xiàn)外部的調(diào)用,應(yīng)用的降級處理等,組件之間通過接口協(xié)議來定義功能服務(wù),這樣組件內(nèi)部可以在迭代中方便的替換實(shí)現(xiàn)類。

核心工具 CocoaPods

組件化架構(gòu),需要一個宿主工程,負(fù)責(zé)集成所有的組件。每個組件都是一個單獨(dú)的工程,通過 Git 私有倉庫來管理。這樣拆分工程項(xiàng)目,開發(fā)人員只需要關(guān)注與組件相關(guān)的部分,而不用考慮其他組件,新人上手更容易。

組件化集成

所有的組件都上傳到 Git 倉庫并支持 Cocoapods 集成。主工程通過配置 Podfile 文件,然后一鍵 pod update 即可。使用 Cocoapods 來管理組件主要因?yàn)槠浔旧砉δ軓?qiáng)大,方便的集成整個項(xiàng)目,解放對依賴庫的管理。使用組件化的集成方式,可以很好的避免傳統(tǒng)項(xiàng)目中的代碼沖突問題。

核心命令:

# 安裝命令
sudo gem install cocoapods
# 配置
pod setup
# 通過Podfile安裝三方庫
pod install
# 通過Podfile更新安裝三方庫
pod update

Git 是一個分布式版本控制系統(tǒng),能夠快速高效地處理從小型到大型項(xiàng)目的所有內(nèi)容。Git 官方文獻(xiàn)資料。

當(dāng)然,如果不想記住這些命令,你可以借助市場上的熱門開發(fā)工具,這里推薦 Git 官方桌面端、Sourcetree

Xcode 本身就支持項(xiàng)目的 Git 倉庫管理,在 Source control 中就可以創(chuàng)建管理你的項(xiàng)目。

image.png

在你創(chuàng)建項(xiàng)目時,Xcode 就提示你是否創(chuàng)建 Git 倉庫:

默認(rèn)創(chuàng)建Git 倉庫

這里需要注意的就是 podspec 索引文件的編寫。

Pod::Spec.new do |s|
  s.name             = '組件工程名'
  s.version          = '0.0.1'
  s.summary          = '簡介'
  s.homepage         = '遠(yuǎn)程倉庫地址'
  s.license          = { :type => 'MIT', :file => 'LICENSE' }
  s.author           = { '作者' => '作者' }
  s.source           = { :git => '遠(yuǎn)程倉庫地址', :tag => s.version }
  s.ios.deployment_target = '8.0'
  s.source_files     = 'Classes/**/*.{swift,h,m,c}'
  s.resources        = 'Assets/*'
  s.dependency 'AFNetworking', '~> 2.3'
  s.dependency 'Reachability','~> 3.2
end

source_files:是你要共享的文件
resources:是一些資源文件,比如圖片資源
dependency:是該組件所需要依賴的其他組件、三方組件等。

關(guān)于創(chuàng)建子模塊/子文件夾

    //簡單:
    subspec 'Twitter' do |sp|
        sp.source_files = 'Classes/Twitter' //指定子模塊路徑
    end
    subspec 'Pinboard' do |sp|
        sp.source_files = 'Classes/Pinboard'
    end
    
    //復(fù)雜:
    Pod::Spec.new do |s|
        s.name = 'RestKit'

        s.subspec 'Core' do |cs|
            cs.dependency 'RestKit/ObjectMapping'
            cs.dependency 'RestKit/Network'
            cs.dependency 'RestKit/CoreData'
        end
        s.subspec 'ObjectMapping' do |os|
        end
    end

更多內(nèi)容參考 基礎(chǔ)-podSpec使用。

典型的產(chǎn)品

  • 滴滴

滴滴的組件化是將項(xiàng)目拆分為業(yè)務(wù)部分和技術(shù)部分,業(yè)務(wù)部分包括專車、拼車、巴士等組件,使用一個 pods 管理,技術(shù)部分則分為登陸分享、網(wǎng)絡(luò)、緩存等基礎(chǔ)組件,分別使用不同的 pods 管理。

組件間的通信通過 ONERouter 中間件進(jìn)行通信,中間件擔(dān)負(fù)協(xié)調(diào)和調(diào)用各個組件的責(zé)任。組件間通信通過 OpenURL 方法來進(jìn)行對應(yīng)的調(diào)用。ONERouter 內(nèi)部保存一份 Class - URL 的映射表,通過 URL 找到 Class 并發(fā)起調(diào)用, Class 的注冊放在 +load 中進(jìn)行。

  • 淘寶

淘寶架構(gòu)的核心思想是一切皆組件,將工程中所有代碼都抽象為組件。在 CocoaPods 中可以通過 podfile 很好的配置各個組件,包括組件的增加和刪除,以及控制某個組件的版本。

淘寶架構(gòu)的主要分為四層,從上到下依次是:業(yè)務(wù)組件 -> 核心層 /容器-> 中間件/功能封裝 -> 底層庫。容器是整個架構(gòu)的核心,負(fù)責(zé)組件間的調(diào)度和消息派發(fā)。

總線設(shè)計:URL 路由 + 服務(wù) + 消息。統(tǒng)一所有組件的通信標(biāo)準(zhǔn),各個業(yè)務(wù)間通過總線進(jìn)行通信。

URL 路由

路由 URL 統(tǒng)一對三端的行為進(jìn)行了統(tǒng)一,一套 URL 就可以調(diào)起 iOS、Android、前端三個平臺的對應(yīng)組件。

URL 路由請求可以被解析就直接調(diào)起相應(yīng)的組件,如果不能被解析(沒有對應(yīng)的組件)就跳轉(zhuǎn) H5 頁面,這稱為降級處理。

服務(wù)

服務(wù)提供一些公共服務(wù),是面向接口的,通過接口協(xié)議 Protocol 進(jìn)行調(diào)用。

消息

URL 路由通常都是一對一進(jìn)行通信,那么針對一對多的消息派發(fā)和調(diào)度就可以通過消息完成,這類似于 iOS 的通知機(jī)制。例如應(yīng)用的前后臺切換、Socket的推送消息等,都可以通過消息機(jī)制分發(fā)到接收消息的組件。

小結(jié)

我們可以看到,滴滴和淘寶的組件化上有很多的相似之處,組件化的核心工具 CocoaPods,URL 路由進(jìn)行頁面的路由跳轉(zhuǎn),其他的如接口協(xié)議、消息通知等,應(yīng)該都有類似的解決方法。除了管理組件的核心 CocoaPods 工具,URL 路由、接口協(xié)議服務(wù)、消息通知等都是我們在組件化過程中使用到的利器。

總結(jié)

組件化開發(fā)就是將項(xiàng)目進(jìn)行拆分成一個個獨(dú)立的功能組件,然后將其組合成一個完整的項(xiàng)目。那么,如何拆分?組件如何通信?如何組合?都是我們要考慮的問題。關(guān)于分層和拆分的粒度都沒有標(biāo)準(zhǔn)化的,需要開發(fā)者根據(jù)以往已經(jīng)合理的進(jìn)行規(guī)范。組件間的通信有多種方式,這里比較推崇淘寶的架構(gòu),路由 + 服務(wù) + 消息的形式實(shí)現(xiàn)多種方式的通信。組件化的核心工具就是 CocoaPods ,我們要做的就是將組件項(xiàng)目上傳到 Git 或者碼云,編寫項(xiàng)目的 podSpec 文件讓組件支持 CocoaPods 集成 即可。CocoaPods 的功能十分強(qiáng)大,即使非組件化項(xiàng)目,我們同樣使用它來管理依賴庫,安裝、卸載、升級、降級等,只需要一個命令即可完成,作為開發(fā)者,這個工具是必定要掌握的。

參考


實(shí)踐篇

上一章節(jié)中,我們簡單介紹了以下組件化的概念、使用到的工具等,這一章節(jié)中我們來演示一個組件如何制作。

組件的創(chuàng)建

首先我們來為項(xiàng)目創(chuàng)建一個關(guān)于網(wǎng)絡(luò)請求的功能組件 LLNetworking。

  • 拉取模版

我們將創(chuàng)建在桌面上的一個名為 Demo 文件夾中。通過終端進(jìn)入到該文件夾下,然后輸入命令:

pod lib create LLNetworking

這個命令會為了拉取 Pod 的 基礎(chǔ)模板。拉取之后,還會通過詢問的形式為你配置一些東西:

// 作用的平臺
What platform do you want to use?? [ iOS / macOS ]
 > iOS

// 語言環(huán)境
What language do you want to use?? [ Swift / ObjC ]
 > ObjC

// 是否需要一個 demo 用來測試組件
Would you like to include a demo application with your library? [ Yes / No ]
 > Yes

Which testing frameworks will you use? [ Specta / Kiwi / None ]
 > None

Would you like to do view based testing? [ Yes / No ]
 > Yes

// 組件中,文件的前綴
What is your class prefix?
 > LL

確認(rèn)之后,系統(tǒng)會為你自動配置組件項(xiàng)目,創(chuàng)建好的項(xiàng)目如下:

LLNetworking
  • Example 工程

項(xiàng)目文件目錄中存在一個名為 Example 的工程,這個工程是你選擇 Would you like to include a demo application with your library? 中選擇 Yes 時為你添加的,這個還是很有用的,在你開發(fā)過程中可以通過它來集成測試組件功能的正確性、完整性。 我們先打開這個 Example 來看下:

項(xiàng)目結(jié)構(gòu)

這個 Example 已經(jīng)為你的組件創(chuàng)建了索引文件 podspec,并且集成了該組件。我們來看下 ExamplePodfile 的內(nèi)容:

use_frameworks!

platform :ios, '8.0'

target 'LLNetworking_Example' do
  pod 'LLNetworking', :path => '../'

  target 'LLNetworking_Tests' do
    inherit! :search_paths

    pod 'FBSnapshotTestCase'
  end
end

其中為你集成了一個測試用例 pod 'FBSnapshotTestCase',目前可以忽略。

我們可以看到: pod 'LLNetworking', :path => '../' 這一句,path 路徑指向了本地路徑,對應(yīng) LLNetworking 主目錄下:

組件目錄

這個文件夾下,一個存放你的各種類文件,一個存放圖片資源等。

  • podspec 文件

在你回答之前問題之后,pod 為你自動創(chuàng)建了該文件,并執(zhí)行了 pod install 命令,該命令會找到組件的索引文件(也在本地) LLNetworking.podspec

#
# Be sure to run `pod lib lint LLNetworking.podspec' to ensure this is a
# valid spec before submitting.
#
# Any lines starting with a # are optional, but their use is encouraged
# To learn more about a Podspec see https://guides.cocoapods.org/syntax/podspec.html
#

Pod::Spec.new do |s|
  s.name             = 'LLNetworking'
  s.version          = '0.1.0'
  s.summary          = 'A short description of LLNetworking.'

# This description is used to generate tags and improve search results.
#   * Think: What does it do? Why did you write it? What is the focus?
#   * Try to keep it short, snappy and to the point.
#   * Write the description between the DESC delimiters below.
#   * Finally, don't worry about the indent, CocoaPods strips it!

  s.description      = <<-DESC
TODO: Add long description of the pod here.
                       DESC

  s.homepage         = 'https://github.com/LOLITA0164/LLNetworking'
  # s.screenshots     = 'www.example.com/screenshots_1', 'www.example.com/screenshots_2'
  s.license          = { :type => 'MIT', :file => 'LICENSE' }
  s.author           = { 'LOLITA0164' => '476512340@qq.com' }
  s.source           = { :git => 'https://github.com/LOLITA0164/LLNetworking.git', :tag => s.version.to_s }
  # s.social_media_url = 'https://twitter.com/<TWITTER_USERNAME>'

  s.ios.deployment_target = '8.0'

  s.source_files = 'LLNetworking/Classes/**/*'
  
  # s.resource_bundles = {
  #   'LLNetworking' => ['LLNetworking/Assets/*.png']
  # }

  # s.public_header_files = 'Pod/Classes/**/*.h'
  # s.frameworks = 'UIKit', 'MapKit'
  # s.dependency 'AFNetworking', '~> 2.3'
end

該文件為你的組件自動配置了一些基本的信息,因?yàn)槲抑笆褂眠^ trunk 登陸過,所以這里有的的賬號信息。當(dāng)然這些信息是需要你根據(jù)情況修改的,更多的配置你可以搜索相關(guān)文檔。

注意:這里的 Git 地址目前是找不到的,后期需要自己關(guān)聯(lián)。

設(shè)置共享文件

podspec 文件中 s.source_files = 'LLNetworking/Classes/**/*' 指代共享的資源路徑,我們需要將共享的文件放到這里來。

共享文件夾

我們打開組件的目錄查看,可以看到這里已經(jīng)有了名為 ReplaceMe 的文件了,這暗示你用共享文件替換它。

podspec 文件中還有一個被注釋掉的:

# s.resource_bundles = {
  #   'LLNetworking' => ['LLNetworking/Assets/*.png']
  # }

這個目錄中存放一些圖片等資源,當(dāng)你需要的時候可以開啟來。

我們來創(chuàng)建一個 LLNetworking 類:

@interface LLNetworking : NSObject
-(NSString*)getSomething;
@end

@implementation LLNetworking
-(NSString *)getSomething{
    return @"test method.";
}
@end

將其移動到組件的共享目錄下并刪除掉空文件ReplaceMe

設(shè)置共享文件

這樣,我們就設(shè)置好了共享的內(nèi)容,即組件開發(fā)好了。接下來,我們使用 Example 工程來使用這個組件的內(nèi)容。

終端進(jìn)入 Example 工程目錄下,執(zhí)行 pod install 命令來安裝組件。

安裝新的組件

我們發(fā)現(xiàn),Example 項(xiàng)目中 Pods/Development Pods/LLNetworking 下,多出來最新添加的文件。

使用組件

我們安裝好組件之后來使用一下組件的功能,就像使用三方庫那樣:

-(void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    LLNetworking * networking = LLNetworking.new;
    NSLog(@"%@",networking.getSomething);
}

控制臺輸出:

2019-11-08 17:14:47.455341+0800 LLNetworking_Example[7038:1682304] test method.

這表示功能正常。

在組件開發(fā)過程中,使用 pod 'LLNetworking', :path => '../' 將路徑指向本地是很有必要的,方便測試你的組件配置是否正確,功能是否完善,相比推到遠(yuǎn)程、發(fā)布再集成,這方便太多了。

三方依賴庫

有時候,我們的組件還依賴其他的組件,又或者是三方庫。我們通過 s.dependency 字段去設(shè)置,多個庫可以分開寫多次。

Podfiles 模版里最后一條已經(jīng)為我們添加好了,所依賴的是 AFNetworking ,正好是我們網(wǎng)絡(luò)請求組件所依賴的,我們把它開啟,重新 pod install

Analyzing dependencies
Fetching podspec for `LLNetworking` from `../`
Downloading dependencies
Installing AFNetworking (2.7.0)
……

我們發(fā)現(xiàn),Example 自動拉取了組件 LLNetworking 所依賴的其他組件。CocoaPods 工具的另外一個優(yōu)點(diǎn)就是,多個組件依賴同一個組件時,它會自動幫你檢測安裝,而不會重復(fù)導(dǎo)入。

本地組件和遠(yuǎn)程組件

我們發(fā)現(xiàn) Example 工程的 Pods中,本地開發(fā)的組件和遠(yuǎn)程發(fā)布的組件被分別放在了不同的目錄下。

有了 AFNetworking 之后,你就可以修改你的網(wǎng)絡(luò)請求組件了:

#import <AFNetworking/AFNetworking.h>
@interface LLNetworking : NSObject
@property(strong,nonatomic)NSURLSessionDataTask *task;
- (NSURLSessionDataTask *)POSTWithURLString:(NSString *)URLString parameters:(id)parameters success:(void (^)(id responseObject))success failure:(void (^)(id error))failure;
@end

@implementation LLNetworking
- (NSURLSessionDataTask *)POSTWithURLString:(NSString *)URLString parameters:(id)parameters success:(void (^)(id responseObject))success failure:(void (^)(id error))failure{
    AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
    manager.responseSerializer = [AFHTTPResponseSerializer serializer];
    manager.requestSerializer.timeoutInterval = 20;
   _task =  [manager POST:URLString parameters:parameters success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {
       if (success) {
           success(@{@"status":@"success"});
       }
    } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
        if (failure) {
            failure(@{@"status":@"failure"});
        }
    }];
    return _task;
}
@end

修改好之后,還不能直接在 Example 中使用,需要卸載組件再重新安裝。注釋掉 pod 'LLNetworking', :path => '../' 之后執(zhí)行 pod install 即可完成卸載。

至此,你完成了組件的創(chuàng)建、文件共享、本地化測試使用和更新。但是,我們的組件畢竟是要服務(wù)于宿主工程的,如果僅僅只能是通過本地集成,那意義不大,我們要將其關(guān)聯(lián)到遠(yuǎn)程服務(wù)器,推送到本地搭建的 GitLab,又或者是 GitHub、碼云、Coding 等平臺。

關(guān)聯(lián)遠(yuǎn)程倉庫

在模版 podspec 文件中,已經(jīng)幫我們指定了一個 GitHub 的倉庫地址,

s.homepage         = 'https://github.com/LOLITA0164/LLNetworking'

你可以使用它或者進(jìn)行修改它。我們這里選擇使用它,先去 GitHub 創(chuàng)建對應(yīng)的倉庫。

創(chuàng)建演示demo

在最初創(chuàng)建組件時,系統(tǒng)已經(jīng)幫我們創(chuàng)建好了本地 Git 倉庫,進(jìn)入到項(xiàng)目中,顯示出隱藏文件夾就可以看到(command+shift+. 顯隱):

本地git倉庫

如果沒有,你可以使用命令 git init 創(chuàng)建一個?,F(xiàn)在,我們要將之前的修改進(jìn)行提交(本地提交)。

git commit -am "第一次提交"  

然后我們要把本地的 Git 倉庫和剛剛創(chuàng)建的遠(yuǎn)程倉庫進(jìn)行關(guān)聯(lián)。如何關(guān)聯(lián)呢?你在網(wǎng)站上創(chuàng)建項(xiàng)目后有了這樣的提示:

關(guān)聯(lián)遠(yuǎn)程倉庫

這里有三種:創(chuàng)建一個新的倉庫,推送一個已存在的倉庫以及從其他倉庫導(dǎo)入。我們這里使用第二種即可。

添加遠(yuǎn)程倉庫:

git remote add origin https://github.com/LOLITA0164/LLNetworking.git

將本地內(nèi)容推送到遠(yuǎn)程倉庫:

git push -u origin master

可能會出現(xiàn)讓你登陸驗(yàn)證,輸入你的用戶名和密碼即可。出現(xiàn)以下信息即表示推送成功。

remote: Resolving deltas: 100% (49/49), done.
To https://github.com/LOLITA0164/LLNetworking.git
 * [new branch]      master -> master
Branch 'master' set up to track remote branch 'master' from 'origin'.

回到 GitHub 刷新一下即可看到你的提交記錄。

上述是通過終端命令進(jìn)行 git 操作,如果你并不熟悉 git 命令,你大可以使用便捷的可視化工具(上一章節(jié)有所提及),僅需簡單的點(diǎn)擊操作即可完成項(xiàng)目的管理。

tag 并發(fā)布到 Cocoapods

打標(biāo)簽

至此,我們已經(jīng)成功的將本地倉庫關(guān)聯(lián)并推送到遠(yuǎn)程倉庫,現(xiàn)在我們要發(fā)布一個可用的組件。

首先我們要給當(dāng)前項(xiàng)目打一個 tag 版本號,在 podspec 中:

s.version          = '0.1.0'

指定的版本號是 0.1.0,那么我們就同樣打個 0.1.0tag:

$ git tag 0.1.0  // 打 tag
$ git push --tags  // 推送到遠(yuǎn)程

tag 默認(rèn)在當(dāng)前分支上,因?yàn)檫@里只有 master,所以不用切換分支,如果后期有其他分支,注意別弄錯了。

刷新頁面,項(xiàng)目的 release 選項(xiàng)中會出現(xiàn)剛剛打的版本。

打tag

你也可以直接在頁面的 release 下添加新的 tag,點(diǎn)擊 release 可以看到編輯頁面:

在線打包

發(fā)布到 CocoaPods

由于我們創(chuàng)建的項(xiàng)目以及標(biāo)簽的版本號都是沿用了 podspec 文件中的信息,因此可以直接驗(yàn)證 podspec 文件信息是否可以通過驗(yàn)證,如果需要調(diào)整,調(diào)整之后最好同樣先驗(yàn)證:

pod spec lint

podspec 文件的版本號一定要和 tag 保持一致。

如果通過驗(yàn)證,那么你會看到類似下面的提示,綠色的 passed validation

驗(yàn)證通過

現(xiàn)在可以將 podspec 文件提交到 CocoaPods 上了:

首先要通過 trunk 注冊生成一條會話:

// pod trunk register 郵箱 用戶名 描述
pod trunk register 476512340@qq.com LOLITA0164 --description=組件化demo 

然后去郵箱進(jìn)行驗(yàn)證,驗(yàn)證成功會出現(xiàn)下面頁面:

驗(yàn)證通過

現(xiàn)在,就可以將 podspec 提交給 CocoaPods 了。這個文件將是別人搜索你的組件的索引。

pod trunk push LLNetworking.podspec --allow-warnings

上傳完成之后,接可以通過 pod search LLNetworking 搜索到自己的組件了,如果搜索不到,刪除本地的搜索文件,命令 :

rm ~/Library/Caches/CocoaPods/search_index.json

重新 search 產(chǎn)生新的搜索文件。

發(fā)布新版本則需要打新的 tag,重新編輯 podspec 文件,然后再次提交給 CocoaPods。

集成到宿主工程

我們已經(jīng)完成了網(wǎng)絡(luò)組件的創(chuàng)建和發(fā)布,也支持了 CocoaPods 的集成?,F(xiàn)在我們需要將該組件集成到宿主工程中去,這部分沒什么好提的,因?yàn)槭褂梅绞胶图扇綆焓且粯拥?,可以說三方庫只不過是他人編寫的功能組件而已,我們的組件同樣可以提供給小組成員使用,相比于純粹的三方庫,我們的許多組件都關(guān)聯(lián)了業(yè)務(wù)部分,或者基于私人的其他組件,因此適用范圍較小。

小結(jié)

本章節(jié)先介紹了如何通過 pod 的模版工程創(chuàng)建組件,組件的配置,集成本地組件,然后介紹了遠(yuǎn)程倉庫的關(guān)聯(lián),支持 CocoaPods 的集成等內(nèi)容,學(xué)會了這些,你就可以將自己得意的功能庫提供給他人使用。在組件化的過程中,Git 是我們必須要掌握的,即使你不會使用命令,但是一定要熟悉相關(guān)的軟件。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容