背景
最近公司主App3.0大升级 , 基本框架及大部分功能会参照另一个已有App, 为了方便升级以及后续两个App的开发 , 经过开发与产品讨论, 决定将已有App进行组件化
方案
组件化其实并不是一件全程明朗的事情, 在最开始定下使用cocoapods作为组件化的工具时, 开发只能根据目前App的功能点及功能间相关逻辑进行了一个简单的分类 , 并且按照这个分类大致分为3个阶段进行提测
组件化操作(参考之前写过的工具类组件化步骤, 可以跳过)
-
创建一个测试的iOS工程
image.png -
在该目录下创建一个 文件夹MeishaPod
image.png -
用命令行cd入MeishaPod中 并执行命令
pod lib create MeishaTools
image.png
得到该文件
image.png -
把自已弄好的库扔进这个弄好的容器中 ,Classes中的replace文件可以删除掉
image.png -
在命令行中进入Example目录下
pod install
;然后打开Example中的工程 ,发现库已经全部进来了,并且是跟刚刚复制进Classes中的文件是一个源的。(既修改这里会改到Classes中的文件)
image.png -
修改这个库中的信息->打开MieshaTools.podspec。
image.png 要提交上gitlab上面,在我们写的MeishaTools下面开始进行git操作
git init
git add .
git commit -m '描述'
git remote add origin https://gitlab.meishakeji.com/daniel/meishatools.git
git push origin master
- 接下来进入打tag环节,为什么要打tag呢?因为cocoapods要找到你这个库必须是通过tag来找的,并且在MieshaTools.podspec中的信息中要更新你的tag;如果不写,cocoapods发布你的库时候会找不到你在git上的库。
可以这么理解 MieshaTools.podspec文件中的s.version
就是 cocoapods要发布的你的库的git上面的版本 一一对应
git tag 1.0.3
git push --tag
- 这时候我们要上传到cocoapods了;用命令行进入MeishaTools,然后执行命令
pod lib lint --verbose --use-libraries --allow-warnings
-
--verbose
是查看打印的日志信息 -
--use-libraries
如果库中使用到framework就需要加上 -
--allow-warnings
是忽略警告
如果显示passed validation 就是证明编译通过 那么就可以执行发布了 执行命令
pod trunk push MeishaTools.podspec --verbose --use-libraries --allow-warnings
提交成功后那么这个库就存在cocoapod上面了-
使用这个库 要cd进iOS测试工程根目录中pod init 然后在podfile中导入这个库
然后继续再执行命令pod install
就可以再我们工程中使用这个基础工具类的库了
image.png
组件化过程
第一阶段
- 根据预定目标 , 首先进行项目主框架的组件化 , 这一过程涉及三个点:
1. 项目的基础工具类 2. 项目的基础UI类 3.项目的框架结构类
为了增加组件的通用性, 把工具类和基础UI类另外抽成了两个组件
image.png
在这一过程中, 由于视频上传和视频转场工具涉及到FFmpeg, 在进行组件化时, 这两部分暂时没有封进组件中(此过程相对比较顺利, 因为底层组件及控件一般不涉及业务, 组件化也就简单得多) - 学生列表模块/登录模块/微主页模块的组件化, 这三个模块除了登录模块为了迎合两个App不同的弹窗逻辑, 做了较大改动, 另外的两个模块则十分顺利的完成了
第二阶段
- 营期相册/班级课表/班级空间 这三个模块组件化时, 因为模块内部没有与其他模块的关联逻辑, 也没有跳转逻辑, 因为组件化十分快速顺利
- 在班级动态组件化时, 由于第一阶段的视频上传模块没有完成, 因此大部分时都在处理FFmpeg及视频上传模块的组件化
- 成长记录的数据结构大部分是跟其他组件相关联, 并且有些处理逻辑也和对应组件有联动, 因此把成长记录的组件化放到最后, 等所有的其他模块组件化完成再考虑这个模块
此外, 由于成长手册中使用的转场工具类是Swift与OC混编, 而在组件内不支持swift类转oc头文件使用, 因此在该模块浪费了大量时间之后决定暂不进行组件化, 直接复制到两个项目中使用
第三阶段
-
通讯录及聊天模块处理时, 考虑到聊天的表情包处理相对较独立, 所以把它独立成一个小组件, 并且后面发现除了通讯录模块需要依赖这个小组件, 在消息首页的显示也需要, 刚好减少了后期重新再次拆分的工作量
image.png - 班级通知/学生作业/课程直播, 单个模块组件化基本按照前面的步骤进行, 把关联其他模块的逻辑释和/耦合严重的代码暂时注释掉
到这个阶段, 基本上大部分的模块已经组件化完成, 除了消息模块中涉及大量其他模块的消息类型, 还有成长记录涉及其他模块的入口管理和记录显示问题
第四阶段(在提测后继续进行)
-
在成长记录页面上, 顶部控件显示管理着其他各个模块入口 , 底部列表显示各种其他模块类型的记录数据 , 因此跟其他模块会有很大的耦合性
image.png
为了解决这个问题, 参考之前看过的路由实现模式, 最后决定采用注册组件的方式 , 由App进行管理注册显示的模块入口
image.png
在App启动完成后, 第一时间注册应用入口, 并且配置相关信息, 包括组件的入口/标题/图标和应用Id, 同时注册一个初始化时特殊处理的回调, 方便一些组件的特殊配置; 注册完成后, 在成长记录组件内请求接口获取应用列表时就可以根据注册的组件map来匹配并展示模块入口以及跳转进组件内部
-
在处理底部展示的数据列表时, 由于之前已经考虑过多模块的情况, 因此成长记录的数据模型会通过一层中间层把各种模块的数据模型处理为统一的数据结构,再进行展示; 因此在组件化时, 把中间处理层也封装为注册管理对象, 在获取数据后通过回调, 交给外界来处理
image.png -
除了成长记录关联性高, 首页消息模块的关联性也高(班级通知/学生请假/直播通知/教学反馈/作业通知), 并且有更多的组件间跳转逻辑; 为了统一组件逻辑, 这里也同样用了跟成长记录一样的方式, 通过注册来管理各种消息类型
image.png
同样为了解耦和灵活性, 也有一个进入消息详情时的回调
image.png -
最后一步, 处理每一个组件内部需要跳转到其他组件的耦合逻辑; 这里有考虑过市场上已经成熟的一些路由第三方库, 但是大部分都比较复杂并且除了模块路由还有scheme路由/URI路由等等, 对于我们的项目来说, 只需要用到模块路由, 因此决定不使用这些库, 而是自己写一个路由管理组件之间的逻辑
image.png -
首先在每一个模块的头文件中定义好模块内部需要跳转其他模块的跳转唯一Id
image.png -
然后同样在app启动后, 注册该跳转Id对应的实际跳转的位置
image.png -
最后在模块内部通过跳转id 跳转的对应的页面并传递必要数据
image.png
通过这种注册跳转Id的方式, 模块内部只需要告诉外界内部有这么一个跳转事件, 而不需要知道具体要跳转的页面是哪里, 完全交给外界来控制, 从而达到解耦
其他遇到的问题
1. 多架构问题
组件化时经常验证组件有效性时无法通过, 查看verbose相关信息后, 发现是编译链接i386架构时报错, i386是mac上32位模拟器的支持架构, 因此直接在.podSpec中配置指定验证的架构, 去掉i386 s.pod_target_xcconfig = { 'VALID_ARCHS' => 'x86_64 arm64 arm64e armv7' }
2.FFmpeg的取巧组件化
在封装FFmpeg作为底层组件时, 由于源代码包含.c .m , 而在组件内不支持引入C++系统模块, 因此一直会报错, 最终想出了一个比较绕弯路的方法: 首先新建一个静态库工程, 将FFmpeg中包含的.a文件, fdk-aac-ios插件, x264-iOS插件都拖入工程内
再将FFmpeg的工具处理源代码以及OC管理工具加入工程中
根据报错信息处理ffmpeg_filter.c中的相关代码后, 将该工程编译成一个x86_64架构的模拟器静态库, 以及一个arm64架构的真机静态库, 并用 lipo工具将它们合并成一个.a多架构静态库, 最终放到VSBaseFFmpeg组件中, 完成FFmpeg的组件化
3. 调试与代码同步
前面也说过, 之所以决定将已有App组件化, 是为了方便梅沙教育的升级以及后续两个App的开发, 但是cocoapods组件化时 , 如果通过私有索引库来链接到每一个组件的话, 每一个组件都是一个静态库, 对于静态库内部的代码是无法修改和调试的; 并且对于组件内部的每一次修改都需要重新提交上传打Tag, 再发布到索引库中, 然后才能更新, 不管是多人还是单人开发时都十分麻烦
-
为了解决这两个问题, 首先采用cocoaPods的本地引用方式来引用模块, 这样就可以直接调试组件内部代码 image.png
查阅资料时发现git本身有子模块管理逻辑, 并且能做到仅引用子模块而不需全部下载, 某个子模块的提交有更新也可以及时提醒并更新该子模块, 基本上完全能解决现有问题, 所以最后决定不使用cocoapods的三方库管理模式, 采用git子模块管理
将两个以及组件文件夹放到同一个目录下, 以统一引用组件的的相对路径 , 然后通过git命令产生子模块关联git submodule add [子模块的git地址] [子模块名]
最终git父模块将会依赖于各个git子模块, .gitmodules 文件会有如下配置信息
在多人开发时, 直接拉取git父模块就可以拥有两个项目及关联的组件库
-
第一种拉取方式
git clone xxx.git
这样拉取下来的项目只有目录, 目录里面都是空的, 只有配置文件和依赖关系, 需要另外一部子模块操作
git submodule init
git submodule update
image.png 第二种拉取方式
git clone xx.git --recurse-submodules #递归拉取子模块
我们的项目中有一些文件比较大, 而gitlab服务端的设置有限制, 可能会导致一些组件拉取失败, 这时候可以用下面这个命令, 让每一个子模块执行checkout命令
git submodule foreach git checkout master
最终项目组件结构如下:
项目组件化进行到这里已经基本上成功了, 剩下的是一些比较小的耦合逻辑, 会在之后的版本开发时慢慢解决
总结
- 完成组件化后, 对于之后的版本, 开发起来会变得更加巴适, 代码管理也会规范, 业务逻辑同样会更加清晰;
- 组件化过程最大的收获是对于耦合逻辑的解耦思考, 代码也变得更加健壮了;
- 这次项目组件化, 是对个人能力的一次巨大提升, 在以后编写代码时也会更加多的注意耦合及灵活性;
- 希望能找时间把教师端也组件化了(造福后人)。