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在组件间通讯时的使用等等,还有不少有关设计模式和架构的思路没有深刻探讨。我的认为,关于组件化,仍是根据业务需求和产品体量来运用,网上也有不少能够借鉴的成型的解决方案,不要为了组件化而组件化,这时有必定设计模式的知识积累仍是颇有帮助的。

 

 

相关文章
相关标签/搜索