iOS 组件化实践思考
组件化的应用背景和优势在此不再赘述,下面我们将从实践的角度,讨论一下如何应用组件化的思想,下面将以我自己的理解逐步展开,抛砖引玉。
哪些内容需要组件化
在我的理解中,一个项目可以拆分为以下几种组件:
-
基础组件;
-
功能组件;
-
业务组件;
下面依次来解释几种组件的定义和规则。
基础组件
-
基本配置
-
常量;
-
宏定义;
-
分类
-
各种系统类的扩展;
-
网络
-
对 AFN 的封装;
-
对 SDWebImage 的封装;
-
工具类
-
文件处理;
-
设备信息;
-
时间日期处理;
基础组件的含义就是最基础的东西,每个业务组件都有可能会使用到,基础组件需要抽取的应该是类似上面的代码,举例来说,比如我们定义了一个常量,表示接口的根路径:
let BASEMIRRORURL = "http://rest.mirror.xxxx.com/ios"
那么这个常量在 Home,List,Detail 都有可能会被引用,因此我们将这种最底层的,最下一层的东西归类到基础组件。
又比如分类和扩展,我们给 UIView
的扩展定义一个计算属性:
extension UIView {
var height {
set {
self.frame.size.height = newValue
}
get {
return self.frame.size.height
}
}
}
可以想到,也会有很多的业务组件会使用到这个扩展。
功能组件
-
控件
-
弹幕;
-
轮播;
-
菜单;
-
瀑布流;
-
功能
-
断点续传;
-
音视频处理;
-
CUPImage 封装;
功能组件分为可见和不可见两种,可见的是控件,不可见的是功能。功能组件的作用顾名思义,就是实现了一个功能。
业务组件
业务组件,也就是业务的具体实现了,比如一个 App 的骨架如下:
-
首页;
-
发现;
-
我的;
首页下又分为这样:
-
侧滑菜单;
-
Banner;
-
热门;
这里的每个部分,都可以称为业务组件。
三种组件的关系
三种组件的关系基础组件规则
基础组件和基础组件之间不应该产生依赖,比如我们使用网络请求组件,希望根路径是一个默认参数,但可以对外暴露和修改,像下面这样:
class NetWork {
func request(baseUrl: String = BASEMIRRORURL, path: String, param: [String:Any]) {
}
}
NetWork.request(path: "/g/login.server", param: param)
这时,NetWork
就依赖了 常量
这个基础组件,我们如果使用 NetWork
基础组件,还需要导入 常量
这个基础组件,这是不应该的。
但为了代码的简洁性,这样的封装又是必要的,那么应该怎么做呢?这个问题我们下面会讲到。
功能组件规则
功能组件和基础组件之间不应该产生依赖,比如我们做轮播图,会用到 UIView 的扩展
和 常量
,像下面这样:
imageView.width = SCREENWIDTH
其中 .width
和 SCREENWIDTH
,都在基础组件中,但基础组件中不仅仅是这些东西,如果依赖了基础组件,就需要导入基础组件中其他无用的代码,而且其他人使用轮播图组件,也需要导入基础组件。
因此,在功能组件中,不建议依赖基础组件,�上面的代码应该改成这样:
imageView.frame.size.width = UIScreen.main.bounds.size.width
或者直接复制代码,将需要的基础组件的功能,复制到功能组件当中。
同基础组件一样,功能组件和功能组件也不应该产生依赖,道理是一样的,我们使用一个功能,不应该将另一个功能也导入进来。
业务组件规则
基础组件和功能组件都是为业务服务的,因此业务组件可以依赖于基础组件和功能组件,快速的实现业务,但是业务组件和业务组件之间不应该产生依赖。
比如这样一条业务线,我们要求 发现
这个业务组件,点击一条视频,跳转到 视频播放器
:
func pushToPlayerVC(model: VideoModel) {
let vc = PlayerVC(videoModel: model)
navigationVC.push(vc)
}
这时 发现
就对 视频播放器
产生了依赖,如果将 发现
进行组件化进行剥离,能行吗?不行。
其实这个问题和网络请求使用默认参数封装一样,是组件与组件之间的通讯问题,当然,这个问题我们下面会讲到,现在再提一下是为了一会儿往下写的时候忘了填坑 ...
每个组件存在的形式
-
组件内部;
-
组件外部;
-
组件测试;
组件内部
组件的内部应该使用设计模式划分文件夹的结构,例如 MVVM 结构:
---- PlayerView
-- View
-- Model
-- ViewModel
组件外部
组件的外部应该是一个远程私有 pod
库,使用 CocoaPods 进行管理。
组件测试
单独的测试工程。
怎样集成各个组件
组件集成组件的集成应该像上面的图一样,基础组件和功能组件互不依赖,制作远程 pod
私有库,业务组件依赖于这些 pod
私有库开发,同样制作成远程 pod
私有库,壳工程依赖于 CocoaPods 管理这些私有库,完成整个项目。
当然还有另外的方式,比如将壳工程作为主工程,组件创建为子工程,这方式的缺点是子工程可以修改,缺少约束性,目录结构也比较凌乱。
还有将组件制作为 FrameWork
,壳工程中导入一个个 FrameWork
库,这种方式个人感觉比上一种好一些,但是在物理上,组件和壳还是没能做到分离。
因此,我个人还是更倾向于 pod
库的形式。
组件之间的通讯
-
对外公开 API 接口;
-
通过中间件的中转;
上面我们有两个遗留的问题,归纳为组件之间的通讯问题,下面就通过这两个问题,讨论一下组件之间的通讯。
网络请求默认参数
下面的思路就是暴露出 baseUrl
参数,通过中间件 NetWorkMW
将 NetWork
和 常量
两个基础组件组合,完成默认参数网络请求的封装。
// 基础组件 - 常量
let BASEMIRRORURL = "http://rest.mirror.xxxx.com/ios"
// 基础组件 - 网络请求
class NetWork {
func request(baseUrl: String, path: String, param: [String:Any]) {
}
}
//壳工程 - 网络请求中间件
class NetWorkMW {
func request(baseUrl: String = BASEMIRRORURL, path: String, param: [String:Any]) {
NetWork.request(baseUrl: baseUrl, path: path, param: param)
}
}
NetWorkMW.request(path: "/g/login.server", param: param)
发现跳转视频播放
这个思路是使用代理,对外暴露点击事件,通过中间件,导入 视频播放
业务组件,topVC
基础组件,完成向 视频播放
的跳转:
// 业务组件 - 发现
func pushToPlayerVC(model: VideoModel) {
delegate?.pushToPlayerVC?(videoModel: model)
}
// 中间件 - 发现
func pushToPlayerVC(model: VideoModel) {
let vc = PlayerVC(videoModel: model)
topVC.navigationVC.push(vc)
}
以上实际上是怎么样把多个组件组合使用起来,这种组合是确定的,还有一些是不确定的,例如有一个组件的状态改变了,我要让其他组件知道我的变化,但是我不知道都要告诉谁,怎么办?
眼珠一转,对外暴露状态变化,中间件在变化时发送通知。但是同时我想附带一个模型过去,通知的接收方怎样正确的使用这个模型呢?如果要使用模型,势必要和发送通知的业务组件产生耦合,怎么办?
以后再办,先埋个坑,这些场景我们会在以后再讲到。
组件分离的难点
组件分离的重点和难点也就是解耦,比如我们现在负责一个项目,其中的一个业务或者功能,希望实现组件化,但是它依赖于项目中的其他公共功能,该如何处理呢?这里提供两种思路:
-
拷代码,简单粗暴,摆脱依赖,对于一些不重要的工具方法,可以直接拷贝到内部来使用;
-
把组件依赖的代码先做一个
pod
库,然后依赖这个pod
库;
上面讲到的是代码方面的依赖,还有一种情况是功能方面的依赖,比如我们有一个菜单,这个菜单涉及到网络图片的加载,那么怎样将这个菜单进行组件化呢?
- 使用 Block 或者代理,将网络图片加载这部分的职责交给外部控制;
举例来说,像下面这样:
// 业务组件 - 菜单
self.imageView.sd_setImage(with: url, completed: completed)
那么如果现在将它组件化,这个组件就要依赖于 SDWebImage
,我们应该修改成这样:
// 业务组件 - 菜单
setImage?(for: imageView, completed: ImageLoadCompletedBlock)
// 中间件 - 菜单
menu.setImage = { (imageView, completed) in
imageView.sd_setImage(with: url, completed: completed)
}
现在菜单就摆脱了对 SDWebImage
的依赖。
附加问题
以上的环节掌握了,应该可以尝试简单的组件化了,但是问题没完,还有哪些呢?
库的升级维护
随着项目的迭代,你负责的库升级了,其他的小伙伴们还在用上个版本的库,怎么办?
各种路径资源问题
我们在自己的库里使用了 imageNamed
、mainBundle
,但是小伙伴把我们的库拖过去后,这些路径和我们不是一个路径,Assets.xcassets
跟我们也不是同一个 Assets.xcassets
,怎么办?
这些问题你可以从这篇文章找到答案:你真的会用 CocoaPods 吗?