美文网首页自个儿读其它技术点iOS 技术文档收录
iOS组件化思路-大神博客研读和思考

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

作者: 淡淡如水舟 | 来源:发表于2016-04-08 15:34 被阅读45646次

    一、大神博客研读

    随着应用需求逐步迭代,应用的代码体积将会越来越大,为了更好的管理应用工程,我们开始借助CocoaPods版本管理工具对原有应用工程进行拆分。但是仅仅完成代码拆分还不足以解决业务之间的代码耦合,为了更好的让拆分出去的业务工程能够独立运行,必须进行组件拆分并且实现组件服务化。

    下面是最近在行业内几个大神的博客辩论对战,具体资料如下:

    最近在参考大神们的讨论和之前的LDBusBundle方案基础上上,提炼出了一个适合中小型应用的LDBusMediator中间件,正逐渐在项目中使用。

    博客介绍:http://www.jianshu.com/p/196f66d31543
    中间件Git开源地址:https://github.com/Lede-Inc/LDBusMediator.git

    (1)蘑菇街的组件化方案

    文章来源:

    2016.03.10 蘑菇街App的组件化之路: http://limboy.me/ios/2016/03/10/mgj-components.html

    为什么要组件化?
    • 组件和组件之间没有明确的约束;
    • 组件单独开发、单独测试,不能揉入主项目中开发,测试也可以针对性的测试;
    如何管理短链?(url跳转)
    [MGJRouter registerURLPattern:@"mgj://detail?id=:id" toHandler:^(NSDictionary *routerParameters) {
        NSNumber *id = routerParameters[@"id"];
        // create view controller with id
        // push view controller
    }];
    
    [MGJRouter openURL:@"mgj://detail?id=404”]
    
    

    短链如何管理?

    1. 后台专门管理短链;平台生成所需的文件,ios平台生成h,m文件,android生成java文件,注入到项目中;
    2. 开发人员查看生成文件了解所有可用URL;
    3. 缺点:无法把参数传递也通过生成方式获得;
    同步的Action调用?(服务调用)

    方法一:通过url的方式

    [MGJRouter registerURLPattern:@"mgj://cart/ordercount" toObjectHandler:^id(NSDictionary *routerParamters){
        // do some calculation
        return @42;
    }]
    NSNumber *orderCount = [MGJRouter objectForURL:@"mgj://cart/ordercount”]
    

    方法二:通过protocol-class对应的方式

    把公共协议文件统一放到PublicProtocolDomain.h中,所有业务组件只依赖这个文件;protocol只能通过类方法提供?

    @protocol MGJCart <NSObject>
    + (NSInteger)orderCount;
    @end
    [ModuleManager registerClass:MGJCartImpl forProtocol:@protocol(MGJCart)]
    [ModuleManager classForProtocol:@protocol(MGJCart)]
    
    组件生命周期的管理:(组件管理)

    启动初始化时,实例APP中所有组件的module实例,让每个组件的module实例执行一遍didFinishLaunchingWithOptions方法:在这方法中每个组件注册自己的URL,使用class注册;每个组件可以自行监控系统的通知,如UIApplicationDidBecomeActiveNotification, 对于没有系统通知消息则将此方法写入module的protocol中,依次执行实例的这些protocol方法;

    [[ModuleManager sharedInstance] loadModuleFromPlist:[[NSBundle mainBundle] pathForResource:@"modules" ofType:@"plist"]];
        NSArray *modules = [[ModuleManager sharedInstance] allModules];
        for (id<ModuleProtocol> module in modules) {
            if ([module respondsToSelector:_cmd]) {
                [module application:application didFinishLaunchingWithOptions:launchOptions];
            }
        }
    
    组件化版本管理的问题
    1. 版本同步问题: API接口改动升级(旧接口不存在了,不向下兼容),版本的中位号发生改变;需要所有依赖其的调用都发生改变,才能保证壳工程和主工程能够同步编译通过;
    2. pod update之后编译太长: 考虑通过framework的方式进行修改;
    3. 持续集成问题: 不能只是把podspec直接扔到private repo里完事,需要扔到主工程进行打包编译,编译通过允许提供版本升级,不通过扔回去进行处理;CI编译检查,通过之后再将版本号升级到private repo中,同时修改主工程中Podfile的版本依赖号; 但如果是其它工程呢,被多个业务工程所依赖,如何办?
    蘑菇街开源组件:

    MGJRouter: https://github.com/mogujie/MGJRouter.git

    1. JLRoutes 的问题主要在于查找 URL 的实现不够高效,通过遍历而不是匹配。还有就是功能偏多。
    2. HHRouter 的 URL 查找是基于匹配,所以会更高效,MGJRouter 也是采用的这种方法,但它跟 ViewController 绑定地过于紧密,一定程度上降低了灵活性。

    (2)反革命的组件化方案

    文章来源:
    蘑菇街的方案为什么不好?
    • url注册对于实施组件化是完全没有必要的,拓展性和可维护性都降低;
    • 基于openURL的方案的话,有一个致命缺陷:非常规对象无法参与本地组件间调度;但是可以通过传递params来解决,但是这样区分了远程调用和本地调用的入口;
    • 模块内部是否仍然需要使用URL去完成调度?是没有必要的,为啥要复杂化?
    反革命的组件化方案:

    基于Mediator模式和Target-Action模式:

    [CTMediator sharedInstance]  
    openUrl:url] //call from other app with url
    parseUrl
    performTarget:action:params //call form Native Module
    runtime
    [TargetA action1], [TargetA action2]
    [TargetB action1], [TargetB action2]
    
    反革命组件化方案的调用方式:

    本地跨组件间调用:

    [[CTMediator sharedInstance] performTarget:targetName action:actionName params:@{…}]
    

    远程应用调用:

    openUrl + parseUrl的方式; 针对请求的路由操作,直接将Target和Action的名字封装到url中;
    
    反革命组件化方案的好处:
    1. 将远程调用和本地调用做了拆分,而且由本地应用调用位远程应用调用提供服务;
    2. 组件仅通过Action暴露可调用接口;
    3. 组件化方案必需去Model设计:只有调用方依赖Mediator,响应方依赖是没有必要的;
    4. 调用方如何知道接收方需要哪些Key的参数,如何知道有哪些target可被调用?:在mediator中维护针对Mediator的Category,每个category对应一个target,categroy中的方法对应Action场景;
    • category为组合模式,根据不同的分类提供不同的方法,每个组件对应一个category分类;
    • 参数验证和补救入口;
    • 轻松的请求转发;
    • 统一了所有组件间调用入口;
    • param的hardcode在整个app的作用域仅仅存在于category中,跟调用宏差不多;
    • 安全保证,对url中进行native前缀验证;
    • 保证动态调度考虑;
    反革命组件化方案开源Demo:

    代码Git地址:https://github.com/casatwy/CTMediator.git

    二、实际项目中的组件化问题

    (1) 为什么要组件化?

    • 解决人多(更好的协作)、需求多(更好的功能模块划分)的问题;
    • 解决项目模块间的代码耦合问题;(坚决抵制业务组件间代码直接引用)

    (2)如何拆分组件?(神仙们讨论的主要是产品业务组件化的问题)

    • 基础功能组件:(类似于性能统计、Networking、Patch、网络诊断等)

      • 按功能分库,不涉及产品业务需求,跟库Library类似
      • 通过良好的接口拱上层业务组件调用;
      • 不写入产品定制逻辑,通过扩展接口完成定制;
    • 基础UI组件:(例如下拉刷新组件、iCausel类似的组件)

      • 产品内通用UI组件;(各个业务模块依赖使用,但需要保持好定制扩展的设计)
      • 公共通用UI组件;(不涉及具体产品的视觉设计, 目前较少)
    • 产品业务组件:(例如圈子、1元购、登录、客服MM等)

      • 业务功能间相对独立,相互间没有Model共享的依赖;
      • 业务之间的页面调用只能通过UIBus进行跳转;
      • 业务之间的逻辑Action调用只能通过服务提供;

    (3)组件化工程需要解决的问题?

    • 组件化页面跳转(UIBus)方案要求:

      • 能够传递普通参数(系统基础数据类型)和复杂参数(url无法负载的对象),不负责CustomModel的传递处理;
      • 能够获取url对应的controller进行TabController的动态配置;
      • 能够对controller的present方式进行定制;
      • url的注册须用代码完成,必需去中心化处理;
    • 组件服务化(ServiceBus)方案要求:

      • 能够传递普通参数和复杂参数,尽量不使用CustomModel的传递;
      • 通过接口文件的统一基础库进行依赖;(业务开发方开发阶段只需要依赖接口文件依赖库,在主项目集成测试阶段依赖所有业务组件进行测试)
      • 接口和实现类的的对应注册须用代码完成,由中间件去控制服务实现类的生成;

    (通用问题:复杂参数传递 key值的硬编码问题)

    (4)组件维护问题?

    三、关于组件化的思考和总结

    MGJRouter+ModuleManager方案 (蘑菇街方案)
    CTMediator+Target-Action方案 (反革命方案)

    (1)主要解决本地业务组件之间的通信问题

    组件化主要还是解决本地业务组件间的调用,至于跨App或者Hybrid页面通过openUrl方式调用页面和服务的方式其实是可以拆分成两个步骤的问题:特定模块解析处理+中间件调用。跨App通过info.plist配置的scheme跳转进入,hybrid页面通过JSBridge框架跳转进入,这部分都有特定的模块去解析完成。在特定的模块中是否要调用其它业务组件的页面或者服务由特定模块自行决定,这不是组件化中间件要去完成的事情。

    (2)从工程代码层面来说,组件化就是通过中间件解决组件间头文件直接引用、依赖混乱的问题;

    从实际开发来说,组件之间最大的需求就是页面跳转,需要从组件A的pageA1页面跳转到组件B的pageB1页面,避免对组件B页面ViewController头文件的直接依赖。其次就是服务的调用,服务调用模块绝不是为了解决url跳转的问题,只是服务调用方式可以用来解决页面跳转的需求,但是没有url跳转方案成本低。所以才有了蘑菇街方案的MGJRouter和ModuleManager的class-protocol方案的区别;而反革命的方案仍然用Target-Action方案来解决页面跳转问题,成本稍大;而且url跳转和服务调用是两种不同的组件间通信需求,用两种不同的方式来完成更有区分度。

    (3)纯中间件只负责挂接节点的通信问题,不应涉及挂接点具体业务的任何逻辑。

    中间件如果涉及到具体的业务逻辑,势必造成中间件对业务模块的直接依赖,所以中间件只需要抽象出业务通信的基本职责,规定好协议接口,完成调度功能即可。

    而每个挂接节点(这里指业务组件)遵循中间件的协议完成挂接工作,当然这会造成挂接节点对中间件的协议依赖;调用方同样也必须通过挂接点提供的方法将调用操作push到中间件上,而不用管具体的调用过程,这样也是挂接节点依赖中间件,业务逻辑并没有直接依赖中间件。这就是之前阿里无线分享的bus总线的思路,通过这种思路即使切换或者去掉中间件,都只需要在挂接节点中进行修改就可以完成,避免了对业务逻辑代码的直接调用修改。

    至于去掉中间件,应用仍然能够跑的命题? 如果没有任何代码的修改,就相当于把解藕的桥梁给拆除了,再牛逼的框架也不能满足。

    • 反革命框架的调用方直接依赖中间件提供的调用方法,拆除中间件,至少需要修改调用方法。

    (4)中间件是否应该解决组件对外披露url调用和服务接口信息?

    中间件解决了组件间的通信解藕问题,势必会将组件对外提供调用的信息隐藏起来,不然就不能达到解藕通信的目标。

    蘑菇街方案的披露方法:

    • url短链在后台管理,自动生成可查看的.{h,m,java}文件,开发人员通过这个文件进行查看,代码文件跟文档功能类似;无法解决参数key、类型的问题;
    • 服务调用接口统一放到PublicProtocol.h文件上,其它所有业务组件均需要依赖这个文件;

    (是否把url短链和publicProtocol文件统一放到一个repo里,其实就相当于说明文档的作用)

    反革命方案的披露方法:

    • 通过依赖中间件的category(target)方式,将业务组件的所有调用都通过category的方法暴露出来;
    • 优点:解决了url参数key、类型检查的问题;通过Target-Action方式同时解决了url短链和服务调用的通信需求,而且更加适合程序猿的风格来解决问题。

    四、我们的组件化方案

    之前听阿里的组件化分享之后,自己做了一套有关Bus总线的方案,但是在具体的产品使用过程中用起来还是麻烦,在项目中推广起来难度还是比较大。特别是关于组件对外披露信息的部分,到现在都没有一个好的思路,虽然反革命的方案解决了披露的问题,但是我觉得扩展性和可维护性上还是比较差。

    git开源地址:https://github.com/Lede-Inc/LDBusBundle_IOS.git

    最近研读几个大神的博客和讨论之后,有了一些新的思路,希望能够继续按照bus+category的思路上去专研一下,希望能够一个真正适合在项目里推行起来的方案。

    最近在参考大神们的讨论和之前的LDBusBundle方案基础上上,提炼出了一个适合中小型应用的LDBusMediator中间件,正逐渐在项目中使用。

    博客介绍:http://www.jianshu.com/p/196f66d31543
    中间件Git开源地址:https://github.com/Lede-Inc/LDBusMediator.git

    相关文章

      网友评论

      • 月半的瘦子:您好,对您的文章讲解非常感兴趣,自己简单梳理了下,您看下是这个思路吗?
        基础库(长年不变包括ui基础库)+业务库(针对业务线做的模块),主工程就相当于剩下一个空壳需要做的是中间件的注册,通过中间件解耦合业务模块,是这个意思吗?
      • 笑对人生2017:你好,我想请教的问题是模块化后,每个模块内所有网络请求应该怎么处理呢?
        o凌尘o:其实请求也可以封装成一个组件,把接口暴露出来就可以了
      • 大良造L:本人觉得,针对以上几种方案的缺点进行优化:1、仍然使用plist管理url, 但并不是info.plist, 而是自己的plist, 如果主项目大,更可以每个项目用一个自己的plist进行管理,每个项目模块,有自己独立的plist,非常可观清晰,并且远程和本地用同一套规则,scheme区分。 2、不依赖中间件, 直接针对VC,利用runtime自动拥有跳转功能,但也不特别依赖vc。 [vc openUrl:@"" params:dict option:option]; 3、特殊参数的传递基本上只存在于内部的跳转, 如果是执行上面的2走的完全是自己的跳转流程, 可用param进行传递, model 字典化传递 4、 model没有依赖,dict参数的传递自动在下行vc中接收 。 此方案解决 1、url注册。2、本地服务远程。 3、遍历(app开启后创建规则到全局)4、扩展性, 可从后台获取新规则, 替换全局中的指定规则。的问题。
      • tom555cat:这么巧,上周还面试我来着:smile:
        tom555cat:@philon 对
        淡淡如水舟:@tom555cat 你是仝?
      • 啊哈呵:我说说的观点:

        反革命方案:runtime 接口的 className + selectorName -> IMP ,这种好像强制配对,为了少用import来达到解耦的假象,如果说这种是是解耦,那我可以按照这种方式,整个项目都不要import别的类,全部用NSString反射class,好了整个项目都解耦。

        蘑菇街:注册表的 key -> block 是一种,很简单巧妙,是对block实质使用,大家block只是当Delegate的替代品,其实block有一些巧妙的用法,比如Masonry,RAC等。

        我喜欢蘑菇街方案。
        findM:@hahand 你最后说的很对,页面跳转和参数 应该就仅仅是一个功能组件而已。
        hahand:再仔细看看casa的博文,其实博主也是没看仔细的。casa的中介者方案相当于一个调用器,每个功能组件对应一套协议,在中介者的category里面写好实现,其实和BeeHive这种registClass大同小异,都是要保证业务隔离,复用率,迭代方便。
        casa的博文点主要在于打击蘑菇街,但是自己思想部分写的很空很泛,如果去下载CTMedicator才会恍然大悟。
        而蘑菇街和博主还是过于纠结业务逻辑和参数传递,我之前也一样,把组件化的router和页面跳转的navigator没有去分开理解。两者不是一个层级上的东西。CTMedicator才是真正组件化思想,MGJRoute说白了只是为了响应推送的方案。而且MGJRoute实现类似notification,比玩block远远达不到mansory和rac的程度。
        其实本文下面已经有其他人提出说,为什么不用一个navigator组件,我很赞同,页面跳转和参数传递应该是仅仅作为一个功能组件去被包装的,而不是作为一个隔离所有功能组件的一层。
        smlsjq:我感觉你的理解1偏激了一点,组件化的作用主要是解耦复用还有可维护。
        注册的话 根本没有使用到的组件实例都造成有内存常驻的问题
      • 6f97753e92ae:大神,求指点 QQ1210257083
      • davidyff:从系统层级来看,每一个独立的App其实也是被iOS组件化了么
      • Jabir_Zhang:不明白蘑菇街的方式为什么要先注册,再openurl,直接openurl不好吗。而且它将跳转逻辑变复杂了,URL路由的中间件我感觉管理起来很困难
      • jameiShi:支持这种分享精神
      • 巫师学徒:请问为什么一定要去Model? model作为参数传递的问题是什么?(例如:params:@{@"model":model})
        时间已静止:会引入头文件
        Jabir_Zhang:model中存在业务,会引起耦合
        031856a96170:@巫师学徒 之前看casa的方案的时候也有这个疑问,他给出的解释是model放在哪个模块?放在哪个模块都会产生模块间的依赖,我是觉得如果把model层作为基础功能组件呢?
      • 海浪萌物:表示完全看不懂
      • inoryshu:两种方案都下载下来看了,表示对组件化这个概念彻底懵逼了!
      • 1795c3cbc65e:mark一下
      • BeginnerMind:将注册实力改为注册class应该会好些吧
        +(void)registerConnector:(nonnull id<LDBusConnectorPrt>)connector{
      • 改变自己_now:刚刚我们公司也考虑要用,不错,学习了。谢谢大神
      • wg689:反革命工程师的链接貌似挂了
      • qiushuitian:这么好的文章,,,,不行,我一定要请博主喝瓶饮料!
      • 十一岁的加重:值得反复品读
      • 刘小壮:我们公司也是组件化开发,一个老项目重新重构的,开发过程中确实踩了不少坑。

      本文标题:iOS组件化思路-大神博客研读和思考

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