美文网首页开发杂谈iOSios专题
iOS组件化实践(三):实施

iOS组件化实践(三):实施

作者: CodeWeaver | 来源:发表于2017-01-27 15:16 被阅读2893次

    前言

    上一篇中我们对组件化的准备工作做了介绍,这篇文章我们以SXNews为例进行组件化,Demo地址在这里,壳工程获取脚本在这里,希望本文能给你带来帮助。

    一、修改配置

    根据上一篇文章所述,你应该已经有了ModularizationDemo文件夹,此时该文件夹中只有configOldProject两个子文件夹。这时我们应该针对项目情况,修改config内容。

    这里是SXNewsPodfile

    SXNews的Podfile

    根据这个文件,我们可以得知该项目主要使用了RAC等框架开发,因此为了方便起见,我们需要修改一下我们的config:

    • 通过终端进入config文件夹内,后面的路径是你的文件夹路径

    cd ~/ModularizationDemo/config

    • 修改config/templates/Podfile,在source 'https://github.com/CocoaPods/Specs.git'后添加我们的私有pod源source 'git@github.com:ModularizationDemo/PodSpec.git'
    添加私有源
    • 复制config.sh文件为config_category.sh,复制templates文件夹为templates_category

    cp config.sh config_category.sh
    cp -r templates/ templates_category/

    • 修改config_category.sh文件,这里我使用的是vi,用:进入命令模式,输入以下代码,将所有的templates改为templates_category

    %s#templates#templates_category#g

    • 进入templates文件夹,修改里面的Podfile这个文件,由于该项目的所有模块都适用了RAC、AFN、MJExtension、SDWebImage,因此我们添加给该模版添加这些方便后面处理(其中为了后面网络请求代码方便,我使用了自己的HLNetworking,详细的使用方法可以在该框架的主页查阅):
    pod 'ReactiveCocoa','2.5'  
    pod 'HLNetworking', '~> 2.0.0.beta'  
    pod 'MJExtension', '~> 2.0'  
    pod 'SDWebImage','~> 3.7'
    
    • 进入templates_category文件夹,同样修改里面的Podfile这个文件,category类工程后面的作用主要是做中间件,因此我们添加给该模版添加中间件框架Lothar,详细的使用方法可以在该框架的主页查阅):

    pod 'Lothar', '~> 1.0.5'

    至此,配置文件已经修改完成,这里有我写好的配置文件,可以根据需求更改里面的参数。

    二、创建组件模块

    SXNews这个项目结构很简单,分为SearchDetailPhotoSetWeatherNewsReplyMain这7个业务组件和ToolsOther这两个公共文件夹。根据前两篇文章的内容,我们在这一节先将7个业务组件拆分出来。

    先根据划分好的业务组件建立组件仓库:

    • 首先在git上创建需要组件化的组件仓库,例如Search,该仓库为空仓库即可,并记录仓库的https地址、ssh地址和项目主页地址,这里我们以github的例子为例:

    HTTPS: https://github.com/ModularizationDemo/Search.git
    SSH: git@github.com:ModularizationDemo/Search.git
    HomePage: https://github.com/ModularizationDemo/Search

    • 然后在终端中进入文件夹config,然后执行config.sh文件脚本(组件使用config.sh脚本,组件的action使用config_category.sh脚本)
    cd ~/ModularizationDemo/config  
    ./config.sh
    
    • 此时终端会显示提示信息,根据提示信息输入作者项目名组织名仓库HTTPS URL仓库SSH URL主页 URL,这些信息我们在上一步中就已经获得了,逐个填入即可:
    建立本地组件仓库
    • 完成后我们会发现ModularizationDemo目录下已经多了一个Search目录,其目录大概如下:
    完成后的文件夹目录
    • 接着我们打开项目中的xcodeproj文件,将原项目中Search相关的部分拖入新项目的Search文件夹内,记得选上Copy items if needed
    • 完成后的Search项目目录应该如下:
    • 接着我们尝试编译一下,发现出现了一些警告,从警告中得知,这里主要是缺少部分公共代码(UILabel+Wonderful.hNSString+Base64.h)以及RAC相关的依赖:
    • 编辑模版为我们生成好的Podfile,根据错误提示添加缺少的框架,然后pod install完成cocoapods配置
    • 打开Search.xcworkspace,尝试编译,发现还是缺少RAC,查看原项目发现,原项目中使用了pch引入公共库的头文件,因此依次在需要引入RAC的类中逐个添加#import <ReactiveCocoa/ReactiveCocoa.h>即可
    • 再次编译,发现缺少UILabel+Wonderful.hNSString+Base64.hSXDetailPage.h,对于这些项目内的依赖我们暂且不管,重复以上步骤拆分其他组件
    • 到这里所有的组件都应该拆分好了,但是这样的组件由于相互依赖,是无法独立运行的,接下来我们就通过Lothar这个中间件去除组件依赖
    拆分好之后的项目目录

    三、去除组件依赖

    1. 提取公共代码

    本系列文章的前两篇也说过,在项目开发中,难免会产生形如Common或者Tools这样的公共代码,在组件化中,应当将这些代码细分为各种二方库;在本例中,由于这一块代码量很少,因此直接将这部分所有代码生成一个私有pod,作为基础库供于其他组件使用。

    • 首先依旧使用config生成项目模版,并将Tools相关代码放入项目中,步骤与拆分组件类似
    • 编辑Podfile文件,只引入HLNetworking这个库,并pod install
    • 这里原项目使用的是自己编写的对AFN的简单封装,我这里直接将其改为依赖于HLNetworking

    SXAdManager修改前
    SXAdManager修改后
    • 编辑Tools.podspec文件,增加依赖s.dependency "HLNetworking"
    编辑Tools.podspec文件
    • 提交并上传代码,打tag,验证pod是否可用,最后上传至私有pod
    git add .  
    git commit -m "Tools 1 init"  
    git push  
    git tag 1  
    pod lib lint // 如果这一步通过了就上传tag
    git push --tags  
    pod repo push myspec --allow-warnings // myspec是上一篇文章中准备好的私有pod仓库的名字,--allow-warnings是忽略警告,pod提供了很多参数,具体请查阅cocoapods.org
    

    如果一切正常,完成的结果在终端显示如下:

    完成结果

    这样我们的公共代码就提取好了。

    2. 解除组件横向依赖

    接下来我们以Search模块为例,介绍其解除于其他模块耦合的方法,限于篇幅,Lothar相关的方法就不写说明了,具体请查阅Lothar的项目介绍及注释

    第一步 创建组件的服务接口

    • 首先将SXSearchViewModel中对AFN的依赖改为HLNetworking的调用,然后根据缺失的依赖添加基础库的import

    • 如果一切顺利,我们会发现SXSearchPage.m中引入了Detail模块的SXDetailPage.h(顶部有#import "SXDetailPage.h"),我们先暂时将其注释掉

    • 然后在旧工程中删掉Search文件夹,编译,发现很多地方提示没有SXSearchPage,查看错误代码,发现Search相关依赖主要是需要生成SXSearchPage控制器

    • 根据该需求,我们创建Search模块的Lothar扩展,提供此服务:

      • 在git中创建Search-Category项目
      • 终端进入config文件夹,输入./config_category.sh配置项目模版,项目名为Search-Category
      • 终端进入Detail-Category文件夹,输入pod install完成Lothar的安装,完成后打开Search-Category.xcworkspace
      • Search-Category文件夹中创建Lothar的category
      • 由于Search需求的无非是跳转页面并将keyword传值过去,因此我们在Lothar+Search中写一个方法并实现它:
    - (nullable UIViewController *)Search_aViewControllerWithKeyword:(nullable NSString *)keyword;
    
    - (nullable UIViewController *)Search_aViewControllerWithKeyword:(nullable NSString *)keyword {
        NSMutableDictionary *dict = [NSMutableDictionary dictionary];
        if (keyword) {
            dict[kSearchKeyword] = keyword;
        }
        return [self performTarget:@"Search" action:@"aViewController" params:[dict copy] shouldCacheTarget:YES];
        }
    }
    
    • 其中target字符串是提供服务的组件名,action的字符串是Search中的Target的方法名去掉:@"keyword"则是参数解包用的key,这里的两个字符串即前两篇说的硬编码

    • 编译一下,没什么问题,这个服务的接口就OK了

    • 接着在旧工程中的Podfile写入pod "Search-Category", :path => "../Search-Category",执行pod install,提示我们target所支持的development版本不对,我们暂时先将Podfile的platform :ios, '7.0'改为platform :ios, '8.0',再次执行pod install

    • 完成后在AppDelegateSXDetailPage中修改错误警告:

    
    #import <Search-Category/Lothar+Search.h>
    
    UIViewController *viewController = [[Lothar shared] Search_aViewControllerWithKeyword:sender.titleLabel.text];
    [self.navigationController pushViewController:viewController animated:YES];
    
    
    • 此时旧工程就应该能正常编译了,但是SXSearchPage相关的代码会没有效果,接下来我们实现Search模块的服务

    第二步 实现组件服务

    接着我们在Search模块中支持这个服务:

    • 打开Search的workspace,创建Target_Search类,创建并实现接口方法:
    - (UIViewController *)Action_aViewController:(NSDictionary *)params;
    
    - (UIViewController *)Action_aViewController:(NSDictionary *)params {
       NSString *keyword = params[@"keyword"];
       UIStoryboard *sb = [UIStoryboard storyboardWithName:@"Main" bundle:nil];
       SXSearchPage *sp = [sb instantiateViewControllerWithIdentifier:@"SXSearchPage"];
       sp.keyword = keyword;
       return sp;
    }
    
    • 我们发现该控制器是从旧工程一个公共的Main.storyboard中创建出来的,为了明确控制器归属,我们将这个storyboard拆分:
      • 在Search中创建一个叫做SXSearchPage的storyboard
      • 找到Main这个storyboard,将SXSearchPage剪切,复制到Search中SXSearchPage.storyboard
      • SXSearchPage.storyboardSXSearchPage控制器设置为is Inital View Controller
      • 修改Target_Search的实现为
    - (UIViewController *)Action_aViewController:(NSDictionary *)params {
        NSString *keyword = params[@"keyword"];
        UIStoryboard *sb = [UIStoryboard storyboardWithName:@"SXSearchPage" bundle:nil];
        SXSearchPage *sp = sb.instantiateInitialViewController;
        sp.keyword = keyword;
        return sp;
    }
    
    • 然后尝试编译,发现SXSearchPage.m中与SXDetailPage相关代码编译不通过,接着回到旧工程查看相关代码,该部分属于Detail模块的范围内,因此我们创建一个Detail-Category,创建方式跟Search-Category相同,
    • Detail-Category中创建以下方法提供服务接口:
    // Lothar+Detail.h
    - (nullable UIViewController *)Detail_aViewControllerWithDocid:(nonnull NSString *)docid;
    
    // Lothar+Detail.m
    - (UIViewController *)Detail_aViewControllerWithDocid:(NSString *)docid {
        NSMutableDictionary *dict = [NSMutableDictionary dictionary];
        if (docid) {
            dict[@"docid"] = docid;
        }
        return [self performTarget:@"Detail" action:@"aViewController" params:[dict copy] shouldCacheTarget:YES];
    }
    
    • Search的Podfile中加入pod 'Detail-Category', :path => '../Detail-Category',以使用Detail的服务,将出错代码修改为
    UIViewController *viewController = [[Lothar shared] Detail_aViewControllerWithDocid:[self.searchListArray[indexPath.row] docid]];
    [self.navigationController pushViewController:viewController animated:YES];
    
    • 此时Search模块应该编译通过了,但此时Detail-Category的服务接口并未实现,且Detail-Category尚未从旧工程中拆分出来,接下来我们先暂时在主工程中实现该服务

    第三步 旧工程中实现组件服务

    • 关闭所有的xcode窗口,找到Detail文件夹,创建Target子文件夹
    • 创建Target_Detail类,实现Detail-Category提供的接口
    // Target_Detail.h
    - (UIViewController *)Action_aViewController:(NSDictionary *)params;
    
    // Target_Detail.m
    - (UIViewController *)Action_aViewController:(NSDictionary *)params {
        SXNewsEntity *model = [[SXNewsEntity alloc]init];
        model.docid = params[@"docid"];
        
        UIStoryboard *sb = [UIStoryboard storyboardWithName:@"News" bundle:nil];
        SXDetailPage *devc = (SXDetailPage *)[sb instantiateViewControllerWithIdentifier:@"SXDetailPage"];
        devc.newsModel = model;
        return devc;
    }
    
    • 补全因从Main.storyboardNews.storyboard拆分出来而失效的push操作
    • 在旧工程的Podfile中加入以下仓库,用于统一编译,添加完后执行pod install
    pod 'Tools', '1'
    
    pod 'Search-Category', :path => '../Search-Category'
    pod 'Search', :path => '../Search'
    pod 'Detail-Category', :path => '../Detail-Category'
    
    • 最后将Search模块中使用的图片从主工程中放入SearchAssets.xcassets里,Search模块直接编译运行,检查UI
    • 此时Search模块的拆分就全部完成了,其他模块同理,按照Search模块逐步拆分即可,最后旧工程应该只剩下全局配置代码、AppDetegatemain

    3.提交并上传仓库

    提示:如果出现pod search找不到私有仓库的情况,可以先使用rm ~/Library/Caches/CocoaPods/search_index.json命令清除pods的索引再搜索。

    当所有组件都拆分完毕并调试无误之后,就可以给组件打tag并提交版本了,这里我们还是以Search为例,具体方法如下:

    • 首先先编辑Search.podspecs.version为版本号,s.dependency为依赖的库,这里主要改这两个,有的组件会依赖一些动态库或者静态库,文件模版里有示例,如果还是无法通过校验,请参照cocoapods.org
    • 添加好依赖之后,终端进入Search,输入pod lib lint --allow-warnings --sources=myspec,master进行校验
    • 如果校验通过,输入git add .添加所有文件,项目模版配置时已包含.gitignore因此不会添加Pods相关文件
    • 输入git commit -m "提交信息",完成提交
    • 输入git tag 1给当前的commit打上tag
    • 输入git pushgit push --tags,push代码和tag
    • 输入pod repo push myspec --allow-warnings --sources=myspec,master,将podspec文件上传至私有podspec仓库
    • 最后在主工程里将Podfile中的pod 'Search', :path => '../Search'改为pod 'Search', '~> 1,执行pod install完成所有操作

    看完这篇文章,你应该已经基本完成项目的组件化,下一篇将阐述如何优化组件化后的代码以及相应的一些规范。

    参考文章

    念纪-模块化与解耦

    limboy-蘑菇街组件化之路

    limboy-蘑菇街组件化之路-续

    Casa Taloyum-iOS应用架构谈-组件化方案

    Skyline75489-浅析iOS应用组件化设计

    philon-iOS组件化思路-大神博客研读和思考

    philon-iOS组件化实践方案-LDBusMediator练就

    bang-iOS组件化方案探索

    携程移动端架构演进与优化之路

    iOS-组件化架构漫谈

    相关文章

      网友评论

      • AllenSk:有没有其他人也遇到64位不支持的问题的
      • AllenSk:您好,参照文章我这边遇到了一个问题
        在使用Lother的时候出现了
        symbols not found for for architecture x86_64的错误,
        现在是一筹莫展啊,不知道大神你还在不在,
        这个是Lothar本身的问题吗
      • 熊本丸:大神,若像这样拆分,组件有一个私有pod,组件对应的category也会产生一个pod,调用方想调用某组件暴露出来的方法时就必须引用category文件,这样是否是依赖??
      • zhao1zhihui:这个看着一脸迷茫啊
      • LevySu:这样解耦后,会有很多的文件,很多的私有库。请问有什么好解决办法吗
      • Vine_Finer:你这个网络库的修改有什么用啊?
      • ba28044025ed:感谢博主的分享,期待下一篇,有个问题一直不明白。刚刚开始创建的私有源仓库有什么用处呢?
        CodeWeaver:@逃学书生 最好别用pch,每个类里面根据需要去引用别的头文件,这样可以将防止隐式依赖
        ba28044025ed:@wangshiyu13 楼主 我还有个问题 独立一个组件模块出来后 这个组件模块中需要引用第三方控件比如 Masonry、FMDB等等 是不是组件模块中的子视图或者控制器都得一个一个去包含第三方的头文件才可以呢 有没有像pch文件一样直接包含整个组件模块需要引用的头文件呢?
        CodeWeaver:私有源仓库是专门用于记录私有库的podspec信息的,毕竟私有库的代码你也不希望直接在cocoapods上就能搜到吧
      • 3cefffdcacc3:讲的很详细 感谢大神分享
      • 邹佳欣Curtain:老哥稳……
        邹佳欣Curtain: @wangshiyu13 😄😄😄
        CodeWeaver:@邹佳欣Curtain :grin:
      • 风不会停歇:向大佬低头Orz
      • Goplayer王布斯:终于盼到了,感谢博主分享,期待下一篇
        CodeWeaver:@简书的王布斯 还没写完呢!

      本文标题:iOS组件化实践(三):实施

      本文链接:https://www.haomeiwen.com/subject/ztprbttx.html