iOS组件化(上篇)- 拆分基础组件
修改说明:有朋友反映Xcode9编译RAC4.x失败,无法设置Use Legacy Swift Language Version 的问题。现在将RAC版本改成了2.5(OC版本)。感谢朋友们的反馈。git clone 之后执行pod install即可。不用再设置UseLegacySwiftLanguage Version。
组件化在业界已经炒的水深火热,关于组件化的好处和组件化的方案网上已经有大篇的文章了。(Casa的这篇文章对几种方案的利弊讲述的很不错)笔者就不在赘述了。笔者通过拆分一个现有的demo来简单聊一下项目实施组件化的过程(将分为上篇、中篇、下三篇)。
demo可以从github下载(下载之后执行pod install ), 因为之前用RAC+MVVM写项目,这里用了RAC4.x。pod install之后需要修改一下ReactiveCocoa和Result的UseLegacySwiftLanguage Version 为YES。按照下面的方式解决即可:
修改Result 修改ReactiveCocoa设置好之后编译运行就ok了。(接口不太稳定,第一次运行可能没数据,再运行一次就ok了)
demo采用了“去model化”的设计,关于“去model化”的优势《去model化和数据对象》讲述的很清楚。
以下内容摘自《iOS应用架构谈 网络层设计方案》
交付什么样的数据给业务层?
我见过非常多的App的网络层在拿到JSON数据之后,会将数据转变成对应的对象原型。注意,我这里指的不是NSDictionary,而是类似Item这样的对象。这种做法是能够提高后续操作代码的可读性的。在比较直觉的思路里面,是需要这部分转化过程的,但这部分转化过程的成本是很大的,主要成本在于:
数组内容的转化成本较高:数组里面每项都要转化成Item对象,如果Item对象中还有类似数组,就很头疼。转化之后的数据在大部分情况是不能直接被展示的,为了能够被展示,还需要第二次转化。只有在API返回的数据高度标准化时,这些对象原型(Item)的可复用程度才高,否则容易出现类型爆炸,提高维护成本。
调试时通过对象原型查看数据内容不如直接通过NSDictionary/NSArray直观。
同一API的数据被不同View展示时,难以控制数据转化的代码,它们有可能会散落在任何需要的地方。
其实我们的理想情况是希望API的数据下发之后就能够直接被View所展示。首先要说的是,这种情况非常少。另外,这种做法使得View和API联系紧密,也是我们不希望发生的。
在设计安居客的网络层数据交付这部分时,我添加了reformer(名字而已,叫什么都好)这个对象用于封装数据转化的逻辑,这个对象是一个独立对象,事实上,它是作为Adaptor模式存在的。我们可以这么理解:想象一下我们洗澡时候使用的莲蓬头,水管里出来的水是API下发的原始数据。reformer就是莲蓬头上的不同水流挡板,需要什么模式,就拨到什么模式。
以下内容摘自《iOS应用架构谈 组件化方案》
组件化方案中的去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就能很快找到原因。
本文中demo的大致结构如下:
项目初始结构将一个项目组件化拆分掉,一般会拆分一些基础组件、一些功能组件和业务组件。将拆分好的组件放到远程仓库,统一通过Cocoapods进行管理。当然,要实现这个管理的过程,有一些概念还是必须知道的。
概念图如上图所示: 远程索引库、本地索引库、远程代码库、本地代码库。笔者通过拆分demo 中的一个category的基础组件说明上面的四个概念。
第一步:基础组件Category
一、远程索引库
什么是远程索引库?
每创建一个组件都会带有一个 xxx.podspec 的索引文件。专门用来存放这些索引文件的库就叫做索引库。我们需要将这些索引文件上传到远程索引库才能保证其他的同事能够拿来用。
创建远程索引库( 注:这里是在github上创建了一个public的organization名字叫FFComponent)笔者这里创建的public的,自己公司的项目创建private的私有索引库即可,私有索引的步骤和pubic的操作方式一样
FFSpecs索引库远程索引库已经创建成功,可以看到远程索引库的地址
索引库地址二、 本地索引库 (本地索引库就是用来存放本地索引文件的库)
1. 打开终端 pod repo 查看一下当前有哪些本地索引库(如果你之前没有创建过,应该只有一个master)
pod repo查看本地索引库2. 通过pod repo add <本地索引库的名字> <远程索引库的地址> ,创建本地索引库并和远程索引库做关联(注:本地索引库的名字建议和远程索引库起的名字一样)
命令3. 通过下面的方式可以查看本地索引库的物理地址
物理地址三、远程代码库 (代码实际存放的远程仓库)
创建远程代码仓库(和创建远程索引库的方式一样),创建一个FFCategoryKit的远程代码库,用来存放FFCategory组件的代码。同样获取到FFCategoryKit组件的远程代码库地址。
FFCategory组件 远程代码库地址四、本地代码库
创建FFCategoryKit组件本地代码库
1. pod lib create <组件名> 创建本地代码组件模版库(根据自身需求对下面的提示信息做选择就好)
2. 编译swift版本错误解决,修改一下ReactiveCocoa和Result的UseLegacySwiftLanguage Version 为YES。
UseLegacySwiftLanguageVersion 为YES3 .编译运行通过看下效果。接着把FlowerField_Component 里的Others路径下的文件夹拖入到组件FFCategoryKit的classes路径下。
4. 接着cd到Example下进行pod install (把刚才拖入到classes里的文件夹pod进来)
5. 编译组件看是否报错,编译通过后需要修改podspecs索引文件,一般需要修改下面几个问题。
a. 修改版本号
b. 修改项目的简单概述和详细描述
c. 修改homepage和source地址
d. 添加依赖库
修改前的状态如下图所示:
修改前的状态修改对应的地方即可
修改后如下:
podspec修改后6. 编译运行通过后,提交组件到远程代码库并打tag.
- git add .
- git commit -m “xxx"
- git remote add origin 远程代码仓库地址
- git push origin master
- git tag 版本号 (注:这里的版本号必须和podspec里写的版本号一致)
- git push --tags
7. 通过pod spec lint - -verbose - -allow-warnings 命令验证podspec索引文件
错误说明:如果遇到下面的错误,就按照提示在终端输入`echo "2.3" > .swift-version`,然后在继续验证,验证通过后再通过 pod repo push 提交到远程索引仓库,如果没有遇到该问题自动略过.
错误8. 验证通过后,pod repo push <本地索引库> <索引文件名> - -verbose - -allow-warnings 提交索引文件到远程索引库。
本地也可以查看已成功
9. 接下来回到FlowerField_Component工程修改podfile文件,把FFCategoryKit组件pod进来(注:需要在Podfile中指定组件远程索引库地址,如果不指定默认会从master的索引库查找就会报找不到组件)
Podfile文件修改如下 :
执行pod install,按照上面同样的方式修改一下ReactiveCocoa和Result的UseLegacySwiftLanguage Version 成YES
同样在项目查看已经pod进来了。
10. 编译运行如下
第二步:基础组件APIs
对项目FlowerField_Component中Others下的APIs实施组件化,因为APIs组件中会依赖AFN框架,所以这里会说明一下。按照和FFCategoryKit组件同样的方式来创建APIs组件对应的远程代码库,本地代码模版库,索引库就不用创建了。
1. FFAPIs组件远程代码库和本地代码模版库
FFAPIs组件远程代码库 本地代码模版库2. 同样将APIs文件夹拖入到FFAPIsKit下的classes下面。因为APIs中的NetworkHelper类依赖AFN框架,所以需要修改podspec索引文件,修改如下:
3. 回到Example进行pod install, 编译运行通过后进行提交组件到远程代码库。按照同样的方式打好tag,验证podspec并提交索引文件。然后在本地索引库查看发现FFAPIsKit组件已经完成了。
4. 最后,同样回到FlowerField_Component修改Podfile后pod install。修改ReactiveCocoa和Result的UseLegacySwiftLanguage Version 为YES后编译运行。
5. 采用同样的方式对configs、reformerKeys、tools、mainView进行组件化,创建组件FFConfigsKit、FFReformerKeysKit、FFToolsKit、FFMainViewKit。
说明:FFToolsKit 依赖MBProgressHUD库; 别忘记在对应的podspec索引文件中添加依赖库。
6. 基础组件化结束之后,FlowerField_Component中Podfile如下:
FlowerField_Component的Podfile首先感谢您能坚持听我啰嗦到这里。到这里上篇就结束了。后两篇将陆续更新出来。项目组件化过程中一定也会遇到各样的问题。欢迎大家留言交流互相学习。我的邮箱 jiajung@aliyun.com 也欢迎大家Emial交流。