iOS组件化开发从开始到完整总结
一.组件化介绍
需求来源
随着项目规模不断扩大,业务模块增多,开发过程中会有多条产品线(多人或多小组开发不同的功能);如果用传统的开发模式,会导致代码臃肿,编译速度越来越慢,开发效率低下,代码维护成本越来越高.
组件化优势
代码逻辑和项目结构清晰;代码利用率高,迭代效率高;可以快速集成,并能做单元测试;每个组件可以单独运行,组件之间的耦合度低.
组件化模块划分
基础组件: 宏定义/自定义分类/自定义工具类
功能组件: 项目中常用功能,如:定位/推送/分享
业务组件: 根据具体业务而定,如:聊天/商城
中间组件: 负责界面路由/传参/回调
宿主工程: 类似一个壳子,组合各个组件,形成一个完整的App
组件化实质
组件化其实是把每一个功能模块拆分成一个一个的Pod库;比如项目中要用到AFN,只要Pod一下,便触手可及~;现在我们制作自己的Pod库,然后把它集成到项目中.
二.需要了解
Trunk账号
认证CocoaPods API的服务
用来管理公共仓库中的自己的组件
索引文件(.podspec文件)
记录一个组件的名称/版本/资源储存路径/维护者信息等
每个组件都必须有一个索引文件
索引文件库(Spec Repo)
存放索引文件的仓库
储存在CocoaPods服务器上,我们下载或更新Pod的时候会把这个仓库拷贝一份到本地,本地存放路径:~/.cocoapods/repos/
CocoaPods提供一个公共库,储存在本地的路径为:~/.cocoapods/repos/master/
我们可以创建私有仓库,储存在本地的路径为:~/.cocoapods/repos/自定义仓库名/
组件模板
CocoaPods提供用于快速创建组件的模板
里边可以制作我们的代码,可以做单元测试等,包含一个对应的索引文件
组件化就是以这个模板为基础,制作自己的组件
三.思路梳理(注意划重点了)
有了以上基础知识的了解我们来梳理一下思路
本文会使用私有索引仓库来维护组件(不使用公共仓库master)
组件添加到公共仓库中需要注册Trunk账号: 传送门
在码云(或者其他Git仓库)创建一个私有的仓库,当做<私有索引文件仓库>,后边用来储存索引文件(项目名称:xxSpecs)
在码云(或者其他Git仓库)创建一个公开的仓库,当做<组件仓库>,后边用来储存组件(项目名称:xxKit)
CocoaPods服务器不储存我们的代码,只储存索引文件
制作好组件之后,索引文件里会储存<组件仓库>的地址,把索引文件传给CocoaPods服务器,告诉它储存在指定的<私有索引文件仓库>
使用时,先通过CocoaPods服务器更新<私有索引文件仓库>到本地;项目中Pod某个组件的时候,会在本地<私有索引文件仓库>中找到这个组件的索引文件,从索引文件里拿到<组件仓库>的地址,从这个地址把代码下载到项目中
总结:思路梳理介绍了组件化制作过程的主干,只要大体明白我们在干什么,下边具体操作时会有详细步骤
四.具体操作
索引文件仓库
关联索引文件仓库
把码云上创建的索引文件仓库关联拷贝到本地
pod repo add [仓库名] [仓库URL地址]
之后输入远端Git仓库的账号和密码
检查是否安装成功
cd 到索引文件仓库
cd ~/.cocoapods/repos/[仓库名]
验证索引文件仓库
pod repo lint .
组件
本地新建一个文件夹,用于存放管理组件(起名:xxPod)
下载组件模板到xxPod文件夹
cd 到xxPod文件夹
cd [文件夹全路径]/xxPod
下载组件模板并设置组件名
[组件名] : xxKit (跟码云上组件仓库的名字一致)
pod lib create [组件名]
组件基本设置
// 使用哪种系统的模板
What platform do you want to use?? [ iOS / macOS ]
> ios
// 使用哪种语言
What language do you want to use?? [ Swift / ObjC ]
> objc
// 是否创建测试Demo
Would you like to include a demo application with your library? [ Yes / No ]
> yes
// 使用哪种测试框架
Which testing frameworks will you use? [ Specta / Kiwi / None ]
> specta
// 是否需要测试视图
Would you like to do view based testing? [ Yes / No ]
> yes
// 测试Demo的类前缀
What is your class prefix?
> XX
代码制作
把自己的代码(类文件)直接复制到xxPod/xxKit/xxKit/Classes里
配置组件索引文件: 传送门
检查索引文件格式是否规范
cd 到组件根目录
cd [文件夹全路径]/xxPod/xxKit
检查本地索引文件(passed validation 表示通过验证;–allow-warnings可忽略警告)
pod lib lint
如果提示标签类错误可暂时不用管,往下继续
制作好的代码Pod到组件测试工程中(可进行编译,运行,发现代码问题)
cd 到组件的Example文件夹
cd [文件夹全路径]/xxPod/xxKit/Example
Pod集成
pod install
把做好的组件推送到自己的组件仓库
cd 到组件根目录
cd [文件夹全路径]/xxPod/xxKit/
初始化git
git init
git add .
提交一个Git版本
git commit -m “xxKit组件初始化”
关联码云上的组件仓库
git remote add origin [组件仓库URL]
推送版本到master分支(-f强制推送,覆盖掉之前的所有文件)
git push origin master -f
添加版本标签(标签号必须与索引文件里的标签号一致)
git tag 0.1.0
标签推送到组件仓库
git push –tags
检查远程索引文件(passed validation 表示通过验证)
pod spec lint
关联CocoaPods服务器
制作好的组件关联CocoaPods服务器
cd 到xxKit组件根目录
cd [文件夹全路径]/xxPod/xxKit
推送组件的索引文件到服务器,并告诉服务器存在哪个私有仓库中
[私有仓库名] : xxSpecs
[组件名] : xxKit
pod repo push [私有仓库名] [组件名].podspec –allow-warnings
查看本地的CocoaPods仓库(可看到公共库和自己的私有库)
pod repo
检查组件
更新本地CocoaPods仓库
pod repo update
搜索刚才制作的组件
[组件名] : xxKit
pod search [组件名]
项目中引用私用组件
新建一个项目工程,并添加Pod
配置Podfile文件
全局添加(<私有索引文件仓库>地址)
source ‘https://gitee.com/xxSpecs.git’
单独添加(<组件仓库>地址)
pod ‘xxKit’, :git => ‘https://gitee.com/xxKit.git’
五.iOS组件化开发架构设计
六.iOS组件化方案探索
一、什么是组件化?
1、什么是组件?
“组件”一般来说用于命名比较小的功能块,如:下拉刷新组件、提示框组件。而较大粒度的业务功能,我们习惯称之为”模块”,如:首页模块、我的模块、新闻模块。
这次讨论的主题是组件化,这里为了方便表述,下面模块和组件代表同一个意思,都是指较大粒度的业务模块。
2、什么是组件化?
组件化,或者说模块化,用来分割、组织和打包软件。每个模块完成一个特定的子功能,所有的模块按某种方法组装起来,成为一个整体,完成整个系统所要求的功能。
从工程代码层面来说,组件化的实施通常是通过中间件解决组件间头文件直接引用、依赖混乱的问题;从实际开发来说,组件之间*大的需求就是页面跳转,需要从组件A的pageA页面跳转到组件B的pageB页面,避免对组件B页面ViewController头文件的直接依赖。
二、为什么要组件化?
1、组件化是为了解决什么问题?
一个 APP 有多个模块,模块之间会通信,互相调用,如我们的app,有首页、行情、资讯、我的等模块。这些模块会互相调用,例如 首页底部需要展示部分资讯、行情;行情底部需要展示个股资讯;资讯详情页需要跳转到行情,等等。
2、组件化的好处?
一般意义:
加快编译速度(不用编译主客那一大坨代码了);
各组件自由选择开发姿势(MVC / MVVM / FRP);
组件工程本身可以独立开发测试,方便 QA 有针对性地测试;
规范组件之间的通信接,让各个组件对外都提供一个黑盒服务,减少沟通和维护成本,提高效率;
对于公司已有项目的现实意义:
业务分层、解耦,使代码变得可维护;
有效的拆分、组织日益庞大的工程代码,使工程目录变得可维护;
便于各业务功能拆分、抽离,实现真正的功能复用;
业务隔离,跨团队开发代码控制和版本风险控制的实现;
模块化对代码的封装性、合理性都有一定的要求,提升开发同学的设计能力;
在维护好各级组件的情况下,随意组合满足不同客户需求;(只需要将之前的多个业务组件模块在新的主App中进行组装即可快速迭代出下一个全新App)
3、什么情况下进行组件化比较合适?
当然组件化也有它的缺点:
学习成本高,对于开发人员对各种工具的掌握要求也比较高,对于新手来说入门较为困难。
由于工具和流程的复杂化,导致团队之间协作的成本变高,某些情况下可能会导致开发效率下降。
当项目App处于起步阶段、各个需求模块趋于成熟稳定的过程中,组件化也许并没有那么迫切,甚至考虑组件化的架构可能会影响开发效率和需求迭代。
而当项目迭代到一定时期之后,便会出现一些相对独立的业务功能模块,而团队的规模也会随着项目迭代逐渐增长,这便是中小型应用考虑组件化的时机了。这时为了更好的分工协作,团队安排团队成员各自维护一个相对独立的业务组件是比较常见的做法。
在这时这个时候来引入组件化方案,是比较合适的时机。长远来看,组件化带来的好处是远远大于坏处的,特别是随着项目的规模增大,这种好处会变得越来越明显
三、如何组件化?
1、如何划分组件?
基础功能组件
基础产品组件
个性化业务组件
对于一个没有实施过组件化拆分的工程来说,其中很可能充满了大量不合理的类、方法、头文件和各种错乱的依赖关系,因此首先要进行的*步是模块拆分。
模块拆分可以分成两个部分,基础模块拆分和业务模块拆分。基础模块通常是稳定的依赖代码,业务模块是涉及到业务的需要频繁改动的代码。
基础模块拆分
基础模块是任何一个App都需要用到的,如:性能统计、Networking、Patch、网络诊断、数据存储模块。对于基础模块来说,其本身应该是自洽的,即可以单独编译或者几个模块合在一起可以单独编译。所有的依赖关系都应该是业务模块指向基础模块的。
基础模块之间尽量避免产生横向依赖。
业务模块拆分
对于业务模块来说,考虑到旧有代码可能没有相关的横向解耦策略,业务模块之间的依赖会非常复杂,难以单独进行拆分,因此我们采用的方法是首先从 group 角度进行重新整理。
对业务量很大的工程来说,我个人更加推荐“业务-分层”这样的结构,而不是“分层-业务”,即类似下面的 group 结构:
– BusinessA
– Model
– View
– Controller
– Store
– BusinessB
– Model
– View
– Controller
-Store
而非目前项目中采用的:
– Controllers
– BusinessA_Controller
– BusinessB_Controller
– Views
– BusinessA_View
– BusinessB_View
– Models
– BusinessA_Model
– BusinessB_Model
2、组件化的技术难点?
组件化的实施,直观上看,只是需要将各业务组件的代码放到各自的文件夹或者 jar包里就行了。
这里引出的是:
2.1、组件的拆分方式问题:
可以利用CocoaPods 配合 git 做代码版本管理,独立业务模块单独成库。
但这仅仅是物理上拆分了,拆分后的代码编译是肯定通不过的,因为如下:
#import “MainViewController.h”
#import “HomeViewController.h”
#import “NewsViewController.h”
#import “MeViewController.h”
#import …
@implementation MainViewController
@end
MainViewController 会找不到依赖的其它各个模块的头文件而报错。这里引出的又是另一个问题:
2.2、组件间如何解耦?
组件间解耦,是组件化必须解决的一个问题。换句话说,就是如何解除业务模块间的横向依赖。还是拿上边举得例子来说:
App的根视图MainViewController需要管理首页、新闻、我的等等页面时,如何做到 MainViewController 中,不用去 import这一大堆 XXViewController ?
很简单,按软件工程的思路,下意识就会加一个中间层Mediator:
这样一来,各个模块直接都不需要再互相依赖,而是仅需要依赖 Mediator 层即可。
可直观上看,这样做并没有什么好处,依赖关系并没有解除,Mediator 依赖了所有模块,而调用者又依赖 Mediator,*后还是一坨互相依赖,跟原来没有 Mediator 的方案相比除了更麻烦点其他没区别。
我们希望*终能过实现的是单向的依赖,即:
七.组件化细分设计
一.组件化方案中的去model设计
组件间调用时,是需要针对参数做去model化的。如果组件间调用不对参数做去model化的设计,就会导致业务形式上被组件化了,实质上依然没有被独立。
假设模块A和模块B之间采用model化的方案去调用,那么调用方法时传递的参数就会是一个对象。如果对象不是一个面向接口的通用对象,那么mediator的参数处理就会非常复杂,因为要区分不同的对象类型。如果mediator不处理参数,直接将对象以范型的方式转交给模块B,那么模块B必然要包含对象类型的声明。假设对象声明放在模块A,那么B和A之间的组件化只是个形式主义。如果对象类型声明放在mediator,那么对于B而言,就不得不依赖mediator。对于响应请求的模块而言,依赖mediator并不是必要条件,因此这种依赖是完全不需要的,这种依赖的存在对于架构整体而言,是一种污染。
如果参数是一个面向接口的对象,那么mediator对于这种参数的处理其实就没必要了,更多的是直接转给响应方的模块。而且接口的定义就不可能放在发起方的模块中了,只能放在mediator中。响应方如果要完成响应,就也必须要依赖mediator,然而前面我已经说过,响应方对于mediator的依赖是不必要的,因此参数其实也并不适合以面向接口的对象的方式去传递。
因此,使用对象化的参数无论是否面向接口,带来的结果就是业务模块形式上是被组件化了,但实质上依然没有被独立。在这种跨模块场景中,参数*好还是以去model化的方式去传递,在iOS的开发中,就是以字典的方式去传递。这样就能够做到只有调用方依赖mediator,而响应方不需要依赖mediator。然而在去model化的实践中,由于这种方式自由度太大,我们至少需要保证调用方生成的参数能够被响应方理解,然而在组件化场景中,限制去model化方案的自由度的手段,相比于网络层和持久层更加容易得多。
因为组件化天然具备了限制手段:参数不对就无法调用!无法调用时直接debug就能很快找到原因。所以接下来要解决的去model化方案的另一个问题就是:如何提高开发效率。
在去model的组件化方案中,影响效率的点有两个:调用方如何知道接收方需要哪些key的参数?调用方如何知道有哪些target可以被调用?其实后面的那个问题不管是不是去model的方案,都会遇到。为什么放在一起说,因为我接下来要说的解决方案可以把这两个问题一起解决。
二.解决方案就是使用category
mediator这个repo维护了若干个针对mediator的category,每一个对应一个target,每个category里的方法对应了这个target下所有可能的调用场景,这样调用者在包含mediator的时候,自动获得了所有可用的target-action,无论是调用还是参数传递,都非常方便。接下来我要解释一下为什么是category而不是其他:
category本身就是一种组合模式,根据不同的分类提供不同的方法,此时每一个组件就是一个分类,因此把每个组件可以支 持的调用用category封装是很合理的。
在category的方法中可以做到参数的验证,在架构中对于保证参数安全是很有必要的。当参数不对时,category就提供了补救的入口。
category可以很轻松地做请求转发,如果不采用category,请求转发逻辑就非常难做了。
category统一了所有的组件间调用入口,因此无论是在调试还是源码阅读上,都为工程师提供了*大的方便。
由于category统一了所有的调用入口,使得在跨模块调用时,对于param的hardcode在整个App中的作用域仅存在于category中,在这种场景下的hardcode就已经变成和调用宏或者调用声明没有任何区别了,因此是可以接受的。