iOS 组件化

理论篇

什么是组件化

组件化开发就是将一个臃肿的、单一的项目,根据功能/业务/技术等等进行拆分,形成一个个独立的功能组件,然后借助 CocoaPods 管理工具将其任意组合,集成一个完整的项目。

你可以将 AFNetworking、SDWebImage 等等三方库理解为自己项目的一部分,属于基础组件部分,而我们要做的就是将项目划分成多个独立功能模块,再集成为一个完整的项目。这一过程看似多此一举,但是带来的优势却是非常大。

为什么需要组件化

项目初期,功能相对简单,普通的MVC+模块文件分割就可以满足绝大部分的需求。但是随着功能需求越来越多,业务越来越复杂多样,现有的架构已经不太适用了,即使使用了 Git 分支管理,依然经常发生合并冲突等等问题,另外后期的维护成本也大大增加,业务逻辑变得复杂、模块之间耦合度很大、查找问题效率变低、项目编译过程过慢…… 而且伴随着开发人员的增多(多个小组之间协作开发),这些问题尤为突出,优化开发结构变得非常重要。

中间层

针对上面的问题,第一个想到的优化就是新增一个中间层来协调各个模块之间的调用,所有的模块都通过这个中间层去实现调用和交互,但是这样虽然一定程度上降低了模块与模块的之间的耦合度,但是耦合都转嫁到了中间层上了,并且中间层的改动只能由一个人操作,否则非常容易发生冲突,本质上并没有发生多少变化。另外一点,查找问题的效率低下、编译过慢等问题依旧没有得到有效的解决。

这是传统的中介者模式,这个中间层会依赖其他组件,其他组件也会依赖中间层完成服务。

组件化

组件化能够帮助我们将过大的项目拆解成数个小组件,开发者只需要关注于组件所依赖的其他组件,而无需关心完整项目的其他部分,每个组件可以自己采取所习惯的架构模式:MVC、MVVM、MVCS等等,就像开发一款个人独立的app那样自由。

项目组件化之后所带来的好处是非常多的,我们先总结一下非组件化所造成的问题:

非组件化:

  • 代码高耦合度、高依赖
  • 项目复杂、臃肿、编译过长(影响调试)
  • 难以融合/集成其他产品
  • 需要统一架构
    ……

组件化:

  • 代码复用性提高,可方便的集成到其他项目
  • 项目可配置,方便集成和功能回退(指定版本)
  • 化整为零,将项目细小化
  • 方便组件的并行开发
  • 可方便做单元测试
  • 组件自由度高,即插即用
    ……

当然组件化也有着它的缺点,对已有的项目实施组件化架构比较困难,耗费时间长,项目组成员需要一定学习成本;组件化并没有相应的标准,拆分的粒度要适中,拆分粒度过高,则让项目变得复杂,起到了反作用效果,反之,粒度过低,体现不了组件化的优势,在项目业务不断地添加的过程中,进行不断的尝试调整,找到适合自己项目的才是最好的。

组件化的分层

项目组件化中,最难把握的就是粒度问题,这需要开发的自己的经验去把控。这里只给出个人认为的层次的划分。

【基础组件】:宏定义/常量/自定义工具类,如常用的自定义分类
【功能组件】:项目中所用到的功能,如地图定位/消息推送/分享等
【业务组件】:项目中的模块/业务,如聊天室/直播间/个人中心等
【中间组件】:负责项目中的路由/消息通知/传参/回调等
【宿主工程】:项目容器,用来集成组件,调整各个组件之间的消息传递的容器。

中间层的几种方案

在组件化过程中,中间层是各个组件的通信的桥梁,中间层在组件化过程中扮演着非常重要的角色。目前关于中间层的设计笔者已知的有以下三种方式:基于URL Scheme的路由、基于Runtimetarget-action、面向接口。

  • 路由

iOS 中支持的 URL Scheme 让我们能够在应用之间、应用内部传递消息。日常开发过程中经常用到的就是调用系统服务、唤起三方app等等,这些属于应用之间的消息传递,而我们这里借助 URL Scheme 完成应用内部的消息传递。这里的路由 URL 遵循网上通用的资源标识符合 URI,如:appscheme://home/scan?param=value,我们通过 URL 来传递信息,下层服务方通过 URL 获取参数提供服务,上层消费者通过 URL 获取到服务,完成调用。

基于 URL Scheme 的三方库

JLRoutes 是一种基于 URL Scheme 的路由框架,它全局会保存一个Map,key 是路由协议 url,value 则是对 url 解析后 block 回调,你可以在该回调中处理具体的业务。

实例:

例如我们的路由协议定义如下:

scheme://描述/打开方式/保留字段/功能标识?参数1=值1&参数2=值2
||
myroute://market/1/route/cjpm?stockcode=600212.ss

首先配置路由 url 和 对应的回调处理:

/// 默认下都会进入这里,这里填写路由匹配规则
[JLRoutes.globalRoutes addRoute:@"/market/:operate/route/:code" handler:^BOOL(NSDictionary<NSString *,id> * _Nonnull parameters) {
    NSLog(@"%@", parameters);
    // 接下来的业务逻辑
    return YES; // 返回YES,表示处理截止,后面的路由规则不再启用
}];

然后在需要路由的地方传入相应的路由 url :

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

基于Runtimetarget-action

相比 url scheme 的提前注册、实现服务,CTMediator 借助 OC 运行时的特性,现实组件之间服务的自动发现,无需提前注册即可实现组件间的调用,因此,这种方案的可维护性、可读性、扩展性相对较高。

官方的 Demo 中,结构是这样的:

Demo 结构

CTMediator 的使用流程大体是这样的:

底层组件

  1. 创建 Target_ 开头的目标类,如Target_A(该类是为了让中间件 CTMedator 通过 NSClassFromString生成类),类中定义 Action_ 开头的可调用的方法(为了让中间件 CTMedator通过 NSSelectorFromString 生成方法器),并且这些方法都有一个字典类型参数接收调用者传递过来的信息。

  2. 创建 CTMedator 的分类(方便扩展、分块),此分类对应着Target_A,分类中定义该组件对外(调用者)开放的 API 方法,该组件的开发者需要使用 CTMedator 的核心方法 performTarget:action:params:shouldcacheTarget: 完成方法调用。

上层组件

导入对应 CTMedator 的分类,完成方法调用。

相比传统的中介者模式,这种 target-action 方案解放了中间件对其他组件的依赖,因为它是通过 NSClassFromStringNSSelectorFromString 来生成类的实例和方法器SEL的,然后介入消息的分发机制完成消息分发的,即所谓的主动发现服务。传统的中介者模式中,中间件和其他组件是双向依赖的:

传统的中间层,图来自网络

target-action 方式则是单向依赖,这样做的一个好处就是降低了一定的耦合,在我们移除某个组件时,中间件无需进行改动。

使用 CTMedator 之后,图来自网络

那么,由于没有引入具体的类,而是通过字符生成对应的类和方法,那么关于 CTMedator 的分类要清楚的知道 Target_ 类以及其中的内容。

CTMedator 的分类可以划分为一个组件,必要时,集成到项目中进行调用。

  • 面向接口 Protocol

在路由和 target-action 方案中,都存在硬编码问题、参数不明确问题:URLTarget_Action_ 的硬编码,参数都是通过字典的形式传递,类型不明确。

面向接口 的方式能够很好的解决这两个问题。面向接口的方案通常由两部分组成,一个是用来管理接口协议的类(ModuleManager),一个是具体的接口协议(ComponentProtocol)。

ModuleManager 负责消息的调用和转发,它内部需要存储一张映射表,完成 Protocol -> Class 的工作。ComponentProtocol 文件定义了业务组件可以提供的功能服务,可以将所有服务都定义到其中,也可以按组件划分。这样所有调用方只需要依赖中间件,不需要依赖其他的组件,而中间件通过接口协议绑定可以用于服务的类,即每个组件有一个用于实现对外提供的接口协议的类。在编译时,将对应的类注册到ModuleManager 中,Protocol 的名称即为查找的 key。

注册绑定:

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

调用时通过接口协议从 ModuleManager 中映射出注册的 Class,将获取到的 Class 实例化,并调用协议方法完成服务调用。

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

接口协议的方式虽然可以很好的解决参数类型的不确定性,硬编码问题(实现部分可以任意替换),但是它不是前面两种的替代品,因为他们都有自己的侧重点,如 路由URL 可以在应用之间实现消息传递,面向接口可以用来为某类添加功能或者对类进行功能约束等。

一些注入框架是支持面向接口的注入的,可使用这些库取代 ModuleManager 类。

小结

三种方式都分为底层服务方和上层使用方,服务方都对外提供 了服务媒介,CTMediator 中是 Target_A 文件,面向接口就是 Protocol,路由 URL Scheme 则是回调 block

在三种方式中,个人觉得最不推荐的是 CTMediator 方案,感觉很是臃肿,虽然可以通过多个分类去定义组件,但是实际上对底层组件的调用逻辑都耦合在了中间件中,这意味着中间件需要频繁的进行更新,另外存在太多的硬编码地方,targetaction以及参数名都是硬编码在中间件中的,这样的方式并不灵活。但是 CTMediator 中通过运行时解耦了中间件对底层组件的依赖,以及去 model 化的想法还是非常好的。

面向接口 Protocol 的方案贯穿了底层组件、中间件以及上层组件,一方面解耦了中间件对底层组件的耦合,底层组件变得透明,可以根据接口协议任意替换,另一方面接口协议还确定了参数类型。但是该方案面向的是应用内部的功能通信,外部调用应用时,还是需要路由或者硬编码的形式完成。

路由定义了一套用于信息传递的标准,通过路由,服务方可以注册并实现符合某种特定条件的服务,使用方则通过中间件传递 一条URL 来调用该服务。服务方和使用方彼此透明,可以任意替换。和接口协议比起来,路由的可以处理本地内部和远程外部的两种类型的调用,缺点是 url 需要硬编码,而且参数类型都是字符。路由 URL 和接口协议都需要提前注册才能使用,路由需要 block,接口协议需要 class

路由和接口协议并不冲突,可以使用路由 + 协议的方式来实现中间件,路由实现外部的调用,应用的降级处理等,组件之间通过接口协议来定义功能服务,这样组件内部可以在迭代中方便的替换实现类。

核心工具 CocoaPods

组件化架构,需要一个宿主工程,负责集成所有的组件。每个组件都是一个单独的工程,通过 Git 私有仓库来管理。这样拆分工程项目,开发人员只需要关注与组件相关的部分,而不用考虑其他组件,新人上手更容易。

组件化集成

所有的组件都上传到 Git 仓库并支持 Cocoapods 集成。主工程通过配置 Podfile 文件,然后一键 pod update 即可。使用 Cocoapods 来管理组件主要因为其本身功能强大,方便的集成整个项目,解放对依赖库的管理。使用组件化的集成方式,可以很好的避免传统项目中的代码冲突问题。

核心命令:

# 安装命令
sudo gem install cocoapods
# 配置
pod setup
# 通过Podfile安装三方库
pod install
# 通过Podfile更新安装三方库
pod update

Git 是一个分布式版本控制系统,能够快速高效地处理从小型到大型项目的所有内容。Git 官方文献资料

当然,如果不想记住这些命令,你可以借助市场上的热门开发工具,这里推荐 Git 官方桌面端Sourcetree

Xcode 本身就支持项目的 Git 仓库管理,在 Source control 中就可以创建管理你的项目。

image.png

在你创建项目时,Xcode 就提示你是否创建 Git 仓库:

默认创建Git 仓库

这里需要注意的就是 podspec 索引文件的编写。

Pod::Spec.new do |s|
  s.name             = '组件工程名'
  s.version          = '0.0.1'
  s.summary          = '简介'
  s.homepage         = '远程仓库地址'
  s.license          = { :type => 'MIT', :file => 'LICENSE' }
  s.author           = { '作者' => '作者' }
  s.source           = { :git => '远程仓库地址', :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:是该组件所需要依赖的其他组件、三方组件等。

关于创建子模块/子文件夹

    //简单:
    subspec 'Twitter' do |sp|
        sp.source_files = 'Classes/Twitter' //指定子模块路径
    end
    subspec 'Pinboard' do |sp|
        sp.source_files = 'Classes/Pinboard'
    end
    
    //复杂:
    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

更多内容参考 基础-podSpec使用

典型的产品

  • 滴滴

滴滴的组件化是将项目拆分为业务部分和技术部分,业务部分包括专车、拼车、巴士等组件,使用一个 pods 管理,技术部分则分为登陆分享、网络、缓存等基础组件,分别使用不同的 pods 管理。

组件间的通信通过 ONERouter 中间件进行通信,中间件担负协调和调用各个组件的责任。组件间通信通过 OpenURL 方法来进行对应的调用。ONERouter 内部保存一份 Class - URL 的映射表,通过 URL 找到 Class 并发起调用, Class 的注册放在 +load 中进行。

  • 淘宝

淘宝架构的核心思想是一切皆组件,将工程中所有代码都抽象为组件。在 CocoaPods 中可以通过 podfile 很好的配置各个组件,包括组件的增加和删除,以及控制某个组件的版本。

淘宝架构的主要分为四层,从上到下依次是:业务组件 -> 核心层 /容器-> 中间件/功能封装 -> 底层库。容器是整个架构的核心,负责组件间的调度和消息派发。

总线设计:URL 路由 + 服务 + 消息。统一所有组件的通信标准,各个业务间通过总线进行通信。

URL 路由

路由 URL 统一对三端的行为进行了统一,一套 URL 就可以调起 iOS、Android、前端三个平台的对应组件。

URL 路由请求可以被解析就直接调起相应的组件,如果不能被解析(没有对应的组件)就跳转 H5 页面,这称为降级处理。

服务

服务提供一些公共服务,是面向接口的,通过接口协议 Protocol 进行调用。

消息

URL 路由通常都是一对一进行通信,那么针对一对多的消息派发和调度就可以通过消息完成,这类似于 iOS 的通知机制。例如应用的前后台切换、Socket的推送消息等,都可以通过消息机制分发到接收消息的组件。

小结

我们可以看到,滴滴和淘宝的组件化上有很多的相似之处,组件化的核心工具 CocoaPods,URL 路由进行页面的路由跳转,其他的如接口协议、消息通知等,应该都有类似的解决方法。除了管理组件的核心 CocoaPods 工具,URL 路由、接口协议服务、消息通知等都是我们在组件化过程中使用到的利器。

总结

组件化开发就是将项目进行拆分成一个个独立的功能组件,然后将其组合成一个完整的项目。那么,如何拆分?组件如何通信?如何组合?都是我们要考虑的问题。关于分层和拆分的粒度都没有标准化的,需要开发者根据以往已经合理的进行规范。组件间的通信有多种方式,这里比较推崇淘宝的架构,路由 + 服务 + 消息的形式实现多种方式的通信。组件化的核心工具就是 CocoaPods ,我们要做的就是将组件项目上传到 Git 或者码云,编写项目的 podSpec 文件让组件支持 CocoaPods 集成 即可。CocoaPods 的功能十分强大,即使非组件化项目,我们同样使用它来管理依赖库,安装、卸载、升级、降级等,只需要一个命令即可完成,作为开发者,这个工具是必定要掌握的。

参考


实践篇

上一章节中,我们简单介绍了以下组件化的概念、使用到的工具等,这一章节中我们来演示一个组件如何制作。

组件的创建

首先我们来为项目创建一个关于网络请求的功能组件 LLNetworking

  • 拉取模版

我们将创建在桌面上的一个名为 Demo 文件夹中。通过终端进入到该文件夹下,然后输入命令:

pod lib create LLNetworking

这个命令会为了拉取 Pod 的 基础模板。拉取之后,还会通过询问的形式为你配置一些东西:

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

// 语言环境
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

确认之后,系统会为你自动配置组件项目,创建好的项目如下:

LLNetworking
  • Example 工程

项目文件目录中存在一个名为 Example 的工程,这个工程是你选择 Would you like to include a demo application with your library? 中选择 Yes 时为你添加的,这个还是很有用的,在你开发过程中可以通过它来集成测试组件功能的正确性、完整性。 我们先打开这个 Example 来看下:

项目结构

这个 Example 已经为你的组件创建了索引文件 podspec,并且集成了该组件。我们来看下 ExamplePodfile 的内容:

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 路径指向了本地路径,对应 LLNetworking 主目录下:

组件目录

这个文件夹下,一个存放你的各种类文件,一个存放图片资源等。

  • podspec 文件

在你回答之前问题之后,pod 为你自动创建了该文件,并执行了 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

该文件为你的组件自动配置了一些基本的信息,因为我之前使用过 trunk 登陆过,所以这里有的的账号信息。当然这些信息是需要你根据情况修改的,更多的配置你可以搜索相关文档。

注意:这里的 Git 地址目前是找不到的,后期需要自己关联。

设置共享文件

podspec 文件中 s.source_files = 'LLNetworking/Classes/**/*' 指代共享的资源路径,我们需要将共享的文件放到这里来。

共享文件夹

我们打开组件的目录查看,可以看到这里已经有了名为 ReplaceMe 的文件了,这暗示你用共享文件替换它。

podspec 文件中还有一个被注释掉的:

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

这个目录中存放一些图片等资源,当你需要的时候可以开启来。

我们来创建一个 LLNetworking 类:

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

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

将其移动到组件的共享目录下并删除掉空文件ReplaceMe

设置共享文件

这样,我们就设置好了共享的内容,即组件开发好了。接下来,我们使用 Example 工程来使用这个组件的内容。

终端进入 Example 工程目录下,执行 pod install 命令来安装组件。

安装新的组件

我们发现,Example 项目中 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.

这表示功能正常。

在组件开发过程中,使用 pod 'LLNetworking', :path => '../' 将路径指向本地是很有必要的,方便测试你的组件配置是否正确,功能是否完善,相比推到远程、发布再集成,这方便太多了。

三方依赖库

有时候,我们的组件还依赖其他的组件,又或者是三方库。我们通过 s.dependency 字段去设置,多个库可以分开写多次。

Podfiles 模版里最后一条已经为我们添加好了,所依赖的是 AFNetworking ,正好是我们网络请求组件所依赖的,我们把它开启,重新 pod install

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

我们发现,Example 自动拉取了组件 LLNetworking 所依赖的其他组件。CocoaPods 工具的另外一个优点就是,多个组件依赖同一个组件时,它会自动帮你检测安装,而不会重复导入。

本地组件和远程组件

我们发现 Example 工程的 Pods中,本地开发的组件和远程发布的组件被分别放在了不同的目录下。

有了 AFNetworking 之后,你就可以修改你的网络请求组件了:

#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 => '../' 之后执行 pod install 即可完成卸载。

至此,你完成了组件的创建、文件共享、本地化测试使用和更新。但是,我们的组件毕竟是要服务于宿主工程的,如果仅仅只能是通过本地集成,那意义不大,我们要将其关联到远程服务器,推送到本地搭建的 GitLab,又或者是 GitHub码云Coding 等平台。

关联远程仓库

在模版 podspec 文件中,已经帮我们指定了一个 GitHub 的仓库地址,

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

你可以使用它或者进行修改它。我们这里选择使用它,先去 GitHub 创建对应的仓库。

创建演示demo

在最初创建组件时,系统已经帮我们创建好了本地 Git 仓库,进入到项目中,显示出隐藏文件夹就可以看到(command+shift+. 显隐):

本地git仓库

如果没有,你可以使用命令 git init 创建一个。现在,我们要将之前的修改进行提交(本地提交)。

git commit -am "第一次提交"  

然后我们要把本地的 Git 仓库和刚刚创建的远程仓库进行关联。如何关联呢?你在网站上创建项目后有了这样的提示:

关联远程仓库

这里有三种:创建一个新的仓库,推送一个已存在的仓库以及从其他仓库导入。我们这里使用第二种即可。

添加远程仓库:

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

将本地内容推送到远程仓库:

git push -u origin master

可能会出现让你登陆验证,输入你的用户名和密码即可。出现以下信息即表示推送成功。

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 刷新一下即可看到你的提交记录。

上述是通过终端命令进行 git 操作,如果你并不熟悉 git 命令,你大可以使用便捷的可视化工具(上一章节有所提及),仅需简单的点击操作即可完成项目的管理。

tag 并发布到 Cocoapods

打标签

至此,我们已经成功的将本地仓库关联并推送到远程仓库,现在我们要发布一个可用的组件。

首先我们要给当前项目打一个 tag 版本号,在 podspec 中:

s.version          = '0.1.0'

指定的版本号是 0.1.0,那么我们就同样打个 0.1.0tag:

$ git tag 0.1.0  // 打 tag
$ git push --tags  // 推送到远程

tag 默认在当前分支上,因为这里只有 master,所以不用切换分支,如果后期有其他分支,注意别弄错了。

刷新页面,项目的 release 选项中会出现刚刚打的版本。

打tag

你也可以直接在页面的 release 下添加新的 tag,点击 release 可以看到编辑页面:

在线打包

发布到 CocoaPods

由于我们创建的项目以及标签的版本号都是沿用了 podspec 文件中的信息,因此可以直接验证 podspec 文件信息是否可以通过验证,如果需要调整,调整之后最好同样先验证:

pod spec lint

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

如果通过验证,那么你会看到类似下面的提示,绿色的 passed validation

验证通过

现在可以将 podspec 文件提交到 CocoaPods 上了:

首先要通过 trunk 注册生成一条会话:

// pod trunk register 邮箱 用户名 描述
pod trunk register 476512340@qq.com LOLITA0164 --description=组件化demo 

然后去邮箱进行验证,验证成功会出现下面页面:

验证通过

现在,就可以将 podspec 提交给 CocoaPods 了。这个文件将是别人搜索你的组件的索引。

pod trunk push LLNetworking.podspec --allow-warnings

上传完成之后,接可以通过 pod search LLNetworking 搜索到自己的组件了,如果搜索不到,删除本地的搜索文件,命令 :

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

重新 search 产生新的搜索文件。

发布新版本则需要打新的 tag,重新编辑 podspec 文件,然后再次提交给 CocoaPods

集成到宿主工程

我们已经完成了网络组件的创建和发布,也支持了 CocoaPods 的集成。现在我们需要将该组件集成到宿主工程中去,这部分没什么好提的,因为使用方式和集成三方库是一样的,可以说三方库只不过是他人编写的功能组件而已,我们的组件同样可以提供给小组成员使用,相比于纯粹的三方库,我们的许多组件都关联了业务部分,或者基于私人的其他组件,因此适用范围较小。

小结

本章节先介绍了如何通过 pod 的模版工程创建组件,组件的配置,集成本地组件,然后介绍了远程仓库的关联,支持 CocoaPods 的集成等内容,学会了这些,你就可以将自己得意的功能库提供给他人使用。在组件化的过程中,Git 是我们必须要掌握的,即使你不会使用命令,但是一定要熟悉相关的软件。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,029评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,395评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,570评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,535评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,650评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,850评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,006评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,747评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,207评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,536评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,683评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,342评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,964评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,772评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,004评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,401评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,566评论 2 349