iOS组件化方案-总结第二篇

作者: sun6boys | 来源:发表于2016-12-31 01:35 被阅读5310次

    概述

    这是iOS组件化方案-总结的第二篇,在本文中我实现了Target-Action方案的Demo,并与第一篇介绍的protocol方案做出对比

    如果没有看过我第一篇protocol组件化方案的同学,可以先去下载我那篇文章中提供的Demo,方便理解我本文的详述以及了解我Demo中实现的业务场景,传送门iOS组件化方案-总结的第一篇

    Target-Action方案

    国际惯例先上Demo(下载主工程就好了哈,如果不能理解可以把所有业务模块都下载下来,Casa也提供了官方Demo,我第一篇文章中提供了传送门)

    Target-Action方案主工程

    Target-Action方案商品详情业务Category组件地址

    Target-Action方案商品详情业务模块地址

    Target-Action方案确认订单Category组件地址

    Target-Action方案确认订单业务模块地址

    Target-Action方案CTMediator地址直接用Casa开源的CTMediator

    实施

    如何把模块做成私有pods我这里就不介绍了,想知道的可以看我第一篇组件化介绍文章。我这里只拿确认订单模块举例

    确认订单模块是个单独的project,为了避免其他模块调用确认订单模块需引入整个模块,这里又做了一个确认订单业务Category的私有组件如下图

    icon

    TAConfirmOrderBusinessCategory即是确认订单模块对外提供服务的入口,我们的业务场景是商品详情模块立即购买进入确认订单模块,确认订单模块提交订单后返回商品详情模块,同时得到通知下单成功,所以上图中入参提供了ConfirmComplete的Block,下图是TAConfirmOrderBusinessCategory.m中的实现

    
    #import "CTMediator+TAConfirmOrder.h"
    
    @implementation CTMediator (TAConfirmOrder)
    
    - (UIViewController *)confirmOrderViewControllerWithGoodsId:(NSString *)goodsId goodsName:(NSString *)goodsName ConfirmComplete:(dispatch_block_t)confirmComplete
    {
        NSMutableDictionary *params = [[NSMutableDictionary alloc] init];
        params[@"goodsId"] = goodsId;
        params[@"goodsName"] = goodsName;
        params[@"completeBlock"] = confirmComplete;
        return [self performTarget:@"TAConfirmOrder" action:@"ConfirmOrderViewController" params:params shouldCacheTarget:NO];
    }
    
    @end
    

    OK,TAConfirmOrderBusinessCategory实现完了,我们来看下TAConfirmOrder模块,模块中定义一个Target_TAConfirmOrder具体实现如下图

    @interface Target_TAConfirmOrder : NSObject
    
    - (UIViewController *)Action_ConfirmOrderViewController:(NSDictionary *)params;
    
    @end
    
    @implementation Target_TAConfirmOrder
    
    - (UIViewController *)Action_ConfirmOrderViewController:(NSDictionary *)params
    {
        TAConfirmOrderViewController *confirmOrderVC = [[TAConfirmOrderViewController alloc] init];
        confirmOrderVC.goodsId = params[@"goodsId"];
        confirmOrderVC.goodsName = params[@"goodsName"];
        confirmOrderVC.confirmComplete = params[@"completeBlock"];
        return confirmOrderVC;
    }
    
    @end
    

    既然TAConfirmOrderBusinessCategoryTAConfirmOrder是2个project,那category是如何调用到Target_TAConfirmOrder的呢?其实很简单,我想看这篇文章的人大部分都知道,无非就是NSClassFromString ,performSelector这些方法,不知道的可以阅读源码

    到这里我都没有贴过架构图或者讲过原理,只是贴了一部分代码和讲述如何实现,为什么?其实组件化原理很简单,简单到比当初学习UITableView容易多了,我的Demo即原理,如果还是看不明白可以自行google一轮或者在评论区提问.

    Target_Action VS Protocol方案

    1.是否需要注册?

    • Target_Action方案不需要注册
    • Protocol方案需要在启动的时候向CRProtocolManager注册

    Target_Action很好的利用了runtime特性,减少注册这一步,不过对于打算用纯Swift开发的同学就有点尴尬。

    在上篇提供的Protocol方案Demo中,在向CRProtocolManager注册服务的是实例对象而非Class,这样确实会造成内存常驻,但是无伤大雅,熟悉runtime的同学应该都知道第一次调用某个类或对象的方法,会构建出类对象,所以无论你用Class注册还是实例对象注册类对象都在,抛开类对象对于一个不挂任何property的实例对象所占用的内存是很小的。当然你可能会问既然都差不多你为什么注册实例而不是注册Class,注册的ServiceProvider实例对象在有些情况下可以记录一些状态,当然这只是极少数情况下出现的,你如果真要把ServiceProvider当单例对象用,我还是强烈建议注册Class

    不过我不认为ServiceProvider需要向中间件注册有逻辑上的问题,区别只是可省可不省

    2.依赖关系

    Target_Action方案中商品详情模块依赖TAConfirmOrderBusinessCategory组件来获取确认订单模块的服务

    Protocol方案中商品详情模块需要依赖CRConfirmOrderServiceProtocol通过CRProtocolManager组件获取提供服务的实例对象,同时确认订单模块也依赖CRConfirmOrderServiceProtocol来注册服务

    乍一看Protocol方案依赖关系好像对业务产生了侵入因为调用方和实现方都同时依赖了CRConfirmOrderServiceProtocol,其实CRConfirmOrderServiceProtocol应当属于确认订单模块的一部分,把他独立出来只是为了避免调用方需要直接引用实现方,这个依赖在架构图中体现的应该是虚线而不是实线。试想如果Target_Action方案不用runtime,那BusinessCategory也需要直接依赖Target。利用runtime中NSProtocolFromString也可以解决对CRConfirmOrderServiceProtocol的依赖,只是造成一定量的硬编码不够优雅。(提一下,虽然runtime在一些特定场景给我们开发带来一些意想不到的奇效,但是runtime跳过了编译器检查,有时候排除bug比较艰难,所以还是慎用)

    另Protocol方案中商品详情模块同时依赖CRConfirmOrderServiceProtocolCRProtocolManager而Target_Action方案中商品详情模块仅依赖TAConfirmOrderBusinessCategory,依赖关系如下图

    icon

    Protocol方案横向依赖了2者,Target_Action方案纵向依赖。Target_Action设计的更优异

    3.可读性、硬编码

    Target_Action在Category中将常规参数打包成字典,在Target处再把字典拆包成常规参数,这造成了一定量的硬编码,不过在现实开发中,一个模块一个模块提供的category通常是一个人写的,所以造成的影响微乎其微,但是给其他阅读代码的人带来一些不便,甚至同一个人写Cagetory、Target的时候也需要在2个project不停切换查看之前在Target中定义的函数名

    Protocol方案中0硬编码,可读性更高。

    在这里提一下Url注册方案,Url注册方案我觉得最大的问题是大量的硬编码,可读性很差,维护性也很差,对文档的依赖度很高,而且需要有人不停督促文档的更新。我想很多同学对此都深有体会,每个项目第一版的接口文档相对比较详细、全面,随着版本迭代更新,某个接口增加了一些字段,通常后台开发人员都是忘记去更新文档也许是因为忙,甚至有些同学懒的更新文档,一般这时候都是通过qq或者其他通讯工具告知客户端开发人员增加了哪些个字段,字段含义是什么。待时间长了,客户端开发人员忘记字段含义或者换了另一个开发人员接手,不知道这个字段含义是什么,先去翻看以前聊天记录,找不到去看接口文档,文档还是1.0版本。。。。我去。。。

    总结

    综合以上3点,Target_Action更优,我们公司目前也采用的Target_Action方案,如果有同学新开项目并且用纯Swift开发,我建议Protocol方案。事实上没有哪个方案是万能的,具体的采用还得结合自己的业务以及开发人员的整体素质,如果你还是拿不定主意,阿里开源了一个模块解耦框架BeeHive(protocol注册),你就向着大厂靠拢吧。Url注册方案Demo我就先不提供了

    补 业务模块的划分

    有不少同学知道了组件化,但是不知道如何去划分业务模块,我大致拿京东App某几个业务举例见下图

    icon

    图片每个Module组件化后就是一个单独的project,也许很多project里面只有一个ViewController,这也是合理的划分,比如商品详情,很多模块(服装城,京东超市,全球购。。。)会调用到商品详情模块,那把商品详情模块中的业务强行塞到(服装城,京东超市,全球购。。。)任何一个project都是不合理的,确认订单同理。组件化是把业务纵切,具体到某个业务模块中network模块,database模块的划分是横切

    预告

    发现很多同学理解MVC的姿势不对,导致controller很臃肿难以维护,下一篇我会把自己理解的MVC写成一个Demo,这个Demo会是一个业务比较庞大的模块(所以时间会长一点。毕竟我白天要忙公司的项目,还有几个个人项目需要维护)在这个Demo中职责划分会很清楚,敬请期待哈。


    全文完

    相关文章

      网友评论

      • 皮乐皮儿:具体的模块拆分还是无从下手,就拿典型的网络层组件来说,楼主可否提供一个小型的完整项目学习下呢?看了一上午,还是挺迷茫的
      • 黄花菜先生:请问,常量,各个环境的域名,还有一些常用宏,放在哪?
      • 简单coder:B回调A你是放在controller new出来的block中 我个人感觉这种写法不是很强大~~囧 如果我B有多个回调怎么办呢
      • 上升的羽毛:你好,有一个疑问,组件和组件之间的调用可以通过Target-Action方案用runtime找到target里面预留的action调用。但是组件内的页面跳转怎么实现呢?也在target中写方法吗?
        Resory:组件内也是一样的。。你应该用CTMediator+XXX中的方法生成你需要的VC。
        sun6boys:@上升的羽毛 直接跳。
      • 李连毛:有个问题想请教一下,你们实行了组件化了以后,数据库是不是用的SQLite还是自己封装的?
      • 那仅有的执著:你好,问下,新建那个分类的私有库的时候,因为没有CTMediator,所以通不过,上传不上去,怎么破?
        那仅有的执著:@sun6boys 解决了,谢谢
        sun6boys:@那仅有的执著 podspec里面添加依赖了吗
      • 加双芋:页面跳转可以传自定义Model么?
        sun6boys:@木卜小兑 不建议。如果这样做,model就要下沉。
      • Leopx:看了这两篇文章受益匪浅, 谢谢楼主分享
      • Karos_凯:表面上看target action 不需要注册,实际上在openurl时,是需要做判断的,这个判断的代码,也是一种注册。

        另外,target action 的确也有很多好处,可以完全解耦,但是开发维护成本高,写一个模块,需要写target action 还需要写 category,修改的时候又会double一次,对于这个你怎么看
        Resory:是挺繁琐的。而且如果你多段要兼容。你可能还要维护一个path的plist.
      • Zzzzzzzzzzzzzz:大神你好 ,我想问下什么量级的项目适合去做组件化?
      • Vine_Finer:大神如果忙的话,可以把开源项目coding 组件话一部分。让我们看看组件的正确方式。比如冒泡详情页,如果单独抽出来应该怎么抽。他上面的目录介绍也很详细。大神的水平应该分分钟改好,也算是对开源的贡献。
        Vine_Finer:@sun6boys 膜拜大神
        sun6boys:@Vine_Finer没事,我有空给你单独写个demo吧。
        Vine_Finer:@Vine_Finer 大神,我要求的是不是有点多了。抱歉,不好意思。
      • Vine_Finer:大神像你说的京东超市module 这里面几乎用到了所以的基础组件,网络,刷新。如果我没有抽出像网络,分享,搜索这些,基础组件,可以直接抽这个吗?公司还没做这个,我想在新模块中使用。其他还不想改。
        Vine_Finer:@sun6boys 我自己写好了,只是公司一个人不敢改动太大。
        sun6boys:@Vine_Finer 你的基础模块比如网络都很难抽吗?这个按理就是直接做个私有pod呀。
      • herbsun:辛苦楼主, 说一下我的见解吧, 其实简单来讲, 哪种组件化方案都是为了将以前暴露出来的引用链打断 换成 `protocol`或`target&action`, 针对protocol他对于传值方面做的比target&action做的好, 基本上业务类要的啥,就在接口上显现出来了, 但是target&action 对于传参方面用的NSDictionary, 这样灵活但是牺牲了可读性, 幸好的是 这个NSDictionary被限制在了target和业务类层面, 和外界打交道的category弥补了这点. 不知道我的理解对吗? 看了好多 之前也在项目中用蘑菇街的url策略, 感觉那种策略 要做的太多, 不是最优的方案, 准备入手target&action模式了. 看到楼主在项目中已经加入了target&action模式, 请问楼主用到现在遇到的坑有哪些, 希望可以分享一下, 谢谢
      • 251099f95a5b:您好,最近在研究这块,有一个问题想请教下,在进行工程分解之后,对不同模块业务逻辑单独工程开发,这些模块的业务逻辑部门是否需要全部做成静态Lib
        sun6boys:@Lysonson 具体你可以看下cocoapods文档。
        251099f95a5b:@sun6boys 如果不弄成Lib,是否还有其他方式可以达到同样的效果呢
        sun6boys:@Lysonson 如果想编译快一些,可以的。
      • 2922fb61a40e:非常好的文章,期待MVC
      • ynot16:你好,请教个问题。例如把商品详情抽出来做个独立的module了,以后用到的话就可以通过od导入,那么如果几个app都需要用到这个组件,每个app都有一些定制化的需求,怎么解决这部分的需求呢?
        sun6boys:@ynot16 业务入口加个参数type,模块内根据拿到的type,展示不同的定制功能。
      • 32b37300e729:大神, 我遇到了点问题.
        有业务A和业务B,以及提供服务的A_Category和B_Category
        本地测试通过了, 准备提交到仓库去
        已知主工程依赖A_Category推出A控制器, A依赖B_Category推出B控制器
        在我pod repo push 仓库名 A.podspec 的时候报错
        经查实问题是依赖了私有库B_Category导致查找不到,
        后改成pod repo push 仓库名 A.podspec --sources=source=https://github.com/CocoaPods/Specs.git,https://git.oschina.net/neckKnock/B3_Category.git
        报错如下:
        An unexpected version directory `Assets.xcassets` was encountered for the `/Users/longmin/.cocoapods/repos/oschina-neckknock-b3_category/B3_Category` Pod in the `B3_Category` repository.
        我卡这里一星期多了, 求帮助
        32b37300e729:@sun6boys https://git.oschina.net/neckKnock 全部在这个链接
        MainProject -> 这是放podspec文件的仓库
        A2 和 B2是 业务工程
        A2_Category和B3_Category(里面返回的是B2控制器)就是提供服务的分类

        sun6boys:@32b37300e729 你先把podspec提交,把项目地址发给我我去看下
        32b37300e729:上面有一处书写错误,希望不要引起误解
        --sources=source=https://github.com/CocoaPods/Specs.git,https://git.oschina.net/neckKnock/B3_Category.git
        改成
        --sources=https://github.com/CocoaPods/Specs.git,https://git.oschina.net/neckKnock/B3_Category.git
      • 1460d4904512:MVC的demo什么时候有呢
      • 許仙:好极了,正是需要的知识,谢谢分享
      • 590bf6c23733:ctmediator我早就做成公共pod了,swift demo我也已经给到了,完全没必要protocol注册。
        590bf6c23733:@Vine_Finer 不创建对象肯定是不可能的。

        只是URL要注册,runtime不用,省了一笔URL维护的账。

        protocol注册会导致命名域渗透,runtime不会,提高了移植性。
        Vine_Finer:@casa 大神,我研究你那个在现有项目中使用的那个demo研究了两周。
        现在总算是想明白了。卡那个私有库,和Git 卡了一周。
        现在想想就是解耦,你敢信,我们页面跳转用通知。跳的找不到北。
        创建私有库,就是把项目中一个.h和.m文件拖进一个文件夹,想办法让他有我看不到他我看不到他的效果。

        感觉大神要是直接不用cocoapods 会更能接受一些。
        这样大家讨论的重点应该就是runtime 和protocol 和URL 进行页面跳转那个更好一点。但是都逃不过要创建对象,URL 也是使用run time 创建对象。
        Vine_Finer:@casa 哇,活捉大神一只
      • 贝壳绿源:支持,楼主大牛,收藏留着慢慢看 :blush:
        sun6boys:@轩雪陈 为了不让调用方需要引入整个确认订单模块。业务隔离,黑盒操作。
        轩雪陈:您好,请问CTMediator+TAGoodsDetail.h这个类为什么不和TAGoodsDetailViewController 类放在同一个工程里呢?
        sun6boys:@贝壳绿源 谢谢!
      • Vine_Finer:大神这个创建私有库能详细点吗?总是报错
        sun6boys:@Vine_Finer 没提示输入账号密码?你用的什么命令?
        Vine_Finer:@sun6boys 创建私有库,push不到自己的仓库。总是说没权限。但是使用clone 就可以。用sourcetree管理上传的话显示的是上传到cocoapods.git 然后就是没有权限。
        sun6boys:@Vine_Finer 哪一步报错

      本文标题:iOS组件化方案-总结第二篇

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