掌上链家iOS组件化
组件化目标
我们组件化的目标就是每一个组件都是一个私有的仓库,都是一个pod,壳工程用的时候直接pod install
每个组件可以看做一个小的app,里面使用MVC架构
不参考原来的逻辑,在写这个模块的时候和产品确认逻辑,把重要的模块全部重写,你在抽离这个模块的时候,要跟其他模块完全解耦,在你的模块中不能调用其他模块的任何代码,你要保证你的模块在你的测试App(你的测试壳工程)里能够完全运行起来,就要达到这个目标
准备工作:
1、制定代码规范,类前缀,文件前缀,缩进,注释规范
2、建议在开始的时候大家都在一个工程里面就行开发,因为在开发重构的过程中,代码在不断的变化相当于每一个私有仓库版本号在不断的进行更新,每新增一些代码都必须重新打个包工作量比较大
3、通过分文件夹的方式每个组件都有一个对应的文件夹, 对应组件的源码都在对应组件文件夹下面开发。需要保证的是你在组件里面的代码,不会引用到其他组件的代码,不会import其他组件的文件,也不会使用其他组件的方法
4、开发完以后再给对应的组件编写podspec文件,再把它弄到仓库里面,再给它更新版本号,再把最终的版本号填写到壳工程里面去,创建整个壳工程
抽离公共基础组件:
跨产品使用的东西,和业务没关系和产品也是没关系的,log日志,网络,地图服务,社会化分享,文件服务,category扩展
产品相关的基础库:
和当前公司产品强相关的,包括通用的base(BaseViewController等),自定义的HUD、图片选择器、用户行为统计库、应用基本配置信息(token、签名、cookie、加解密相关)以及其他自定义的UI基础组件 (和产品强相关和具体业务不相关)
把以上公共基础组件、链家产品基础库进行剥离,所谓的剥离就是把以上的代码变成一个个的私有库,以源码的形式打成一个pod,使用的时候pod install 怎么使用Cocoa Pod 发布自己的SDK?
组件内使用MVC架构的调用规则
对于比较复杂的逻辑:
1、可以加一个logic层,把一些复杂的业务逻辑放到里面,所谓的复杂业务逻辑 就可能会在你的业务里面保存一些状态机或者处理复杂的业务情况
2、比如登录模块,可能要处理一些状态机,还要处理各种异常的情况,还有注册,可能还有第三方登录,相对来说登录还是比较复杂的,所以我在里面又加了一层Logic层,专门处理一些复杂的业务逻辑,对于复杂的组件还是很有必要的,它可以把那些ViewController的代码给解放出来
1、UI层只能调用Resource、Logic、Model的代码,不能在Logic层反调UI层的代码,如果Logic层想要调用UI层代码,只能通过通知/Block的方式回调
2、Service层只做service相关的动作,它只能够请求网络,然后把请求回来的数据返回,不能去调用Logic层的任何东西,更不能去调UI层的东西
3、Model层就完全是Model,完全解耦,没任何依赖,Model里面不能有任何的业务代码,不能有任何的业务逻辑
Service层:
处理该组件网络接口的请求、以及服务器返回的数据转model的工作
UI层:
页面相关的事务处理就放在对应的ViewController里面,界面相关的全部放在storyboard里面去做
组件管理中心(LJComponentManager)
组件之间的交互无外乎就两个东西:下面这两个问题解决了,组件与组件之间的通讯问题就解决了
1、组件之间页面之间的跳转
2、组件之间服务的调用
组件之间跳转:
可以采用openUrl的方式,就是schemes的方式,因为苹果本身是支持scheme的,只要传一个scheme一个类似于HTTP后面是名字然后?后面跟参数
都在组件管理中心存在的问题?
如果所有的跳转的都在组件管理中心的router函数来跳转的话,如果有30个组件,每个组件跳转10个界面,router函数就300多行了,函数超过50行,100行就不是一个好的编程方式
解决方案:URL导航去中心化
1、去中心化,页面的跳转交给每个组件自己去做,业务组件注册自己能够处理的URL,并且返回对应的VC
2、如果某个URL你这个组件无法处理,或者是参数不对,就直接返回nil
3、ComponentManager这边什么都不做,ComponentManager做的事情就是一个中间件,就是做一个转发,ComponentManager接到一个URL,它把这个URL传递给对应的组件,让这个组件自己去查询这个URL对应的VC
4、这个组件查询到VC过后,把VC返回给ComponentManager,ComponentManager拿到VC过后,它再去调用LJNavigator去进行路由跳转,至于是push也好,present也好就根据具体的参数去控制,跳转根据参数解析
组件之间服务调用:
服务之间的调用也很简单,比如说我在某个组件想要调用首页组件的某一个服务,那我只要传递首页组件协议的名称,通过传入协议名的方式去调用它某一个对外暴露的接口就可以了
1、每个组件提供对外的接口文件,把所有需要对外暴露的接口,都写在接口文件里面
2、组件实现和组件管理中心之间增加了一层协议接口层(LJServiceProtocol集合)
接口文件的设计原则:
1、最复杂的二手房组件对外提供的接口也就5个左右,也就查询二手房,搜索,跳转,一个组件对外提供的接口我认为是不会超过10个的,如果一个组件对外提供的服务接口超过10个,那我就建议你把你的这个组件拆分成两个组件,或者更多组件
2、不建议使用动态的方法(runtime)实现,用这种对外提供接口,已经能够满足要求了
方案对比:
1、这种方案比较直观,比较容易接受,你只要把对外服务集合的接口拉下来,就可以看到所有组件对外暴露哪些接口
2、动态化的方式可能就不那么直观,直接把string变成类名,要看一下这个string对应哪个类,这个类提供了哪些方法,动态性高会失去一部分可读性
对外服务协议集合
二手房组件有个SecondHandHouseProtocol,地图组件有个LJMapProtocol,搜索组件有个SearchProtocol,所有的接口文件,我把它组成一个集合叫做业务组件对外服务协议集合,所有对外服务的接口必须要有一个独立的版本仓库去管理
为什么要用单独的git仓库地址去管理?
1、一旦你对外提供的接口文件确定了,那么你这个组件就不能对这个接口进行增删改了
2、如果你要新增一个接口或者修改接口名字修改一个参数,你这个组件版本更新了一定要更新对应的协议接口文件的版本号,一旦你这个服务协议集合的某一个接口版本变化了,一定要把整个协议集合的版本进行更新
3、把整个对外服务协议集合把它抽出来,做一个大的git地址,用一个git仓库进行管理,一旦你变化组件对外提供的服务变化,就要进行版本变化,一旦版本变化就要通知所有引用协议服务集合的依赖方,让他去更新你的版本号,让他去pod update
接入点:
1、中国要加入世贸组织,怎么加入呢,你肯定是要遵守这个组织的一些协议一些规章制度,组件也一样,你这个组件想要接入组件管理中心,想要接入这套组件管理框架,你就要遵守这套组件管理的接入协议,这个协议就叫做接入点协议LJConnectorProtocol,你这个组件只要遵循了我这个协议,那么你就可以接入我这个管理中心,你就可以被我这个中心管理器发现,进行调度
2、同时这个接入点也可以遵循一个LJComponentServiceProtocol协议,这个协议就是一个对外服务的协议,所谓的对外提供的服务就是对外提供的接口都写在了这个协议里,其他组件都可以通过组件管理器进行发现调度,就是组件之间可以进行服务的调度,任何组件只要你遵循这两个协议,你就可以接入我这个组件化的架构
任何组件只要遵守了这些协议,就可以接入组件管理中心,组件管理中心在进行任务调度的时候就可以发现这个组件,进行页面跳转服务的调用
1、ComponentManager判断你这个组件能否handle这个URL
2、这个URL对应的ViewController
3、业务组件注册自己能提供的Service,返回服务实例
解耦妥协:公共Model下沉
Model与字典传参的优缺点:
1、使用字典优点是完全解耦,如果有几十个参数的话,要一个一个拼接字典容易出错,可读性差,接收方又要转model进行解析
2、使用Model传递的话,会有一定的耦合性,非硬编码不会出现拼写错误
组件之间传递复杂参数: 开发效率,可读性,耦合上做一个平衡
1、几百个参数传递起来非常的麻烦,我们就想看看在组件里面有哪些Model是被公共使用的,我们有三十多个组件,用到的公共Model也就十几个,能够想出来的也就房子的Model,经纪人的model,搜索的Model,用户的Model,其实也就几个Model 经常用的,为什么不把这些Model下沉到底部,把它专门做成一个podspec,把它下沉到底层库去,所有的模块都可以依赖这个公共Model
2、好处就是接口的暴露可以使用Model进行传递而不是使用字典传递,可能会打破一些封装性,打破一些组件之间的依赖,但是便利性和效率上非常快,下沉的过程中你会发现也不会太多,也就十几个 公共Model,设计好之后只会往里面加一些属性,并不会删改一些属性,变化频率不会太高,但一定会变化的,所以必须把公共Model库单独拿出来,使用podspec管理,一旦公共model的属性增删改了以后,一定要更新它的版本号,周知业务方
掌上链家组件化架构
1、上面的每一个小豆腐块都是单独的私有仓库,单独的podfile,单独可以去打包,打包成framework,或者打包成源码都可以
2、整个链家其实是两个app,一个是卖家APP,一个是买家APP, 有两个入口,所有的源码都是在podfile里面,最后理想的情况写一个壳,把需要的功能podfile进去,就是一个新的APP
管理器仓库
1、有一些管理器一些组件要用另一些组件也要用,用的多了给外部暴露太多的接口不好,那还不如把它做成一个单例,把这些作为单例的manager做成一个管理器库,把它下沉到下面去,这样所有上层的一些业务都可以调用它
2、而且管理器内部又可以管理他自己的一些状态,一些变量,相对而言也比较独立,扩展也比较好
3、 定位相关的,数据缓存、持久化相关的,版本管理、push管理、城市管理、 广告的管理、联系人的管理,单例上层调用起来非常方便
4、在登录组件中会有一个LoginManager,我们把它下沉到底部去了,LoginManager管理登录相关的底层信息,比如登录的状态,当前在登录状态,还是非登录状态,还是注册状态,这个状态机我们是下沉管理的,我们把一个LoginManager放到下面去,其他组件如果需要用到登录相关的状态,比如点赞,发评论,个人设置需要知道登录的状态机,只要调用LoginManager单例的状态就可以了,LoginManager如果需要监听登录成功的事件,只要注册manager里面登录的一些回调, 或者登录的一些通知,就可以在组件里面监听登录的事件
URL动态部署和容错处理
使用场景:
由于一些政策原因,不能显示之前跳转的页面,要重定向一些页面的跳转
使用方法:
程序启动时从服务器下载Json格式的Scheme集合,?之前是原来跳转的界面,?之后是新跳转的界面,解析Url时进行处理
遇到的一些问题
切换环境的页面怎么做?
我们当时在首页用三根手指点击四次会弹出一个页面,页面上可以选择测试环境,生产环境,然后切换之后会提示你,环境已切换需要重新启动,就是你设置之后在文件里面写一个值,然后根据这个值去判断是生成环境还是测试环境,启动时会优先读值,然后网络环境跟着这个值走,发布的时候要把这个入口给关掉,避免用户切换到测试环境
组件的拆分粒度就是你组件化的抽象能力
比如最初设计我们二手房,新房,学区房模块的时候,最初是把房子和搜索做在一起的,我们发现搜索在二手房也有,新房里面也有,学区房里面也有,而且搜索的内容差不多,都是传入一定的参数,返回的都是房子列表,房子列表的数据结构其实都是差不多 ,后来我们就想能不能把搜索这边再抽出来做一个搜索的组件,如果你在房子的组件里面要用到搜索相关的功能,去调用它的接口去传相应的参数给它就可以了
组件的scheme是如何管理的?
每一个组件管理自己的scheme,管理自己的URL, 它里面有一个函数叫做openUrl,在这个openUrl函数里面其实就是一个if else的结构,每个组件可能有五六个甚至十几个if else ,所有它能够handle的scheme都在这个组件内部进行管理的
业务code是放在网络层处理,还是在业务层处理?
1、业务的code肯定是放在业务的code里面处理的,网络层不会处理任何的业务,组件内部不是有一个Service吗?Service层只做发送请求,服务端返回json文件,把json简单的抛给Logic层处理去做json转model的操作,Service层不会把某一个字段解析出来,看是什么type要不要做一些特殊的处理,网络层只做网络层应该做的事,只做收发信令,只做回调这个事情,至于业务是上层的logic层在做的
不同组件中,资源重复怎么处理?
如果把资源文件下沉,由于很多组件的资源在不停的变化,一同去维护资源的版本号非常的麻烦,最后我们决定把每一个资源拆分到自己的组件里面去,虽然会造成各个组件里面一些资源的重复,但是这些资源浪费是在合理的范围之内,也没有太大,像一些启动图片,引导图片完全可以放到壳工程里面去,建议每个组件里面要有自己的资源
同一个工程多个组件如何协同开发?
组件化设计建议:
1、组件之间如果用到相同的cell,建议多份拷贝,不要下沉,在新房、二手房的房源列表里面发现其实很多cell都长得差不多 ,开始我们想要复用就把cell抽到底层的公共控件库去了,后来我们发现没有这个必要,哪怕两个组件的cell一模一样,你单独拷一份重新命名,跟着组件走,千万不要把它下沉到底部去,一旦你开了这个先例,你会发现以后往下沉的东西越来越多,慢慢的就失去了组件的意义了,所有东西都往下沉就变成原来的MVC架构了
2、非常不建议在项目中有common控件和common字样,任何组件一定是分工明确功能单一的,如果一开始有common,慢慢的越来越多的垃圾往里面放,到最后就没法维护了千万不要有common组件