1. 啥是组件化
打一个比较形象的比喻, 把 App 比作我们的人体, 把胳膊, 大腿, 心, 肝, 肺这些人体器官比作组件, 各个器官分别负责他们各自的功能, 但是他们之间也有主次之分, 试想我们的胳膊, 大腿等是不能独立完成某个任务的, 必须需要心, 肺, 肝, 胆等的能量支持, 那么可以把胳膊, 大腿这种功能性器官比作业务组件, 把我们的心, 肝, 脾, 肺, 肾比作基础组件. 那么我们的业务组件必须要依赖于我们的基础组件才能发挥其应有的功能, 我们的基础组件 (心, 肝, 肺等) 是高度复用的, 胳膊, 大腿等业务组件要解耦合, 难道你的胳膊动, 大腿也要跟着动吗~, 最终由大脑整合(那么大脑可以类比成主工程). 不知道您是否领会了精神, 综上就是我们的组件化的思路.
2. 为什么要做组件化
继续我们上面那个惊悚的例子, 但是我们把人变成机器人
单独项目运行调试更快 ~Tony 直接穿戴手部机甲, 肯定比穿戴全套的快呀~
各组件自由选择开发姿势 ~Tony 开发完手部机甲用的 MVC, 感觉这种不好, 开发腿部的时候用 MVVM 了~
工程可以独立开发, 方便 QA 有针对性地测试 ~Tony 研发手部功能, 直接把手部穿上测试就好喽.~
业务分层, 解耦使代码的可维护性更高 ~Tony 想升级手部的激光, 动手就行了, 别的地方都不用管!~
便于各业务功能拆分, 抽离, 实现真正的功能复用(特别是对于多 App 来说更加突出) ~Tony 想给残疾人研发个假腿, 直接把腿部拿过来用就好喽~
业务隔离, 跨团队开发代码控制和版本风险控制的实现 ~Tony 有钱可以雇好几个团队同步开发, 你开发胳膊, 我开发腿, 互不干扰, 完事装到一起就行~
3. 组件化的设计原则及目的
~ 注: 这里的稳定性指通俗地讲就是是否需要频繁修改代码~
越底层的模块, 应该越稳定, 越抽象, 越高度复用. 稳定性取决于是否需要频繁改变, 底层库之所以要稳定是因为其会被业务层组件频繁调用, 一旦变化, 可能会影响几乎所有业务层组件, 要做到设计一套 API 很久都不用改变, 就需要设计的时候能越抽象, 即需要我们的抽象总结能力.
注意稳定性传递 稳定性是可以传递的, 例如组件 A 稳定, 组件 B 不稳定, 组件 A 依赖于组件 B, 那么 A 其实也是不稳定的.
自完备性有的时候要优于代码复用 紧接上例: 如何实现 A,B 的解耦呢? 假设 A 依赖于 B 中的 X 代码段 1>. 如果 X 是相对独立且高度复用的, 我们当然可以将其提取出来如下
2>. 如果 X 只是一个方法或者函数, 并不适合单独提取出一个模块, 那么直接 copy 一份 X 代码到 B 权衡之下也是没有问题的.
那么我们的最终目的可以总结为: 在基于模块设计原则上, 让模块之间没有循环依赖, 让业务模块之间解除依赖.
4. 组件化耦合关系
综上所述我们项目组件化方案示意图可以是这样
如上图业务组件单向耦和于基础组件, 这样的架构完成了基础组件的高度复用和业务组件的解耦. 但是问题又来了, 各个业务组件之间难免有页面跳转和数据交互, 业务组件不耦合意味着不能直接调用, 那么我们引入一个中间层.
这里注意, 依赖一定是单项的, 否则我们只是把融在一起的代码块拆分成多个代码块, 而且比之前更麻烦了.
5. 中间层的实现方案
关于中间层实现众说纷纭, 这里说下我们实践的方案.
主要分成四部分:
routerManager:routerModules(以字符串存储各个 module 中相应 router 类的名字, 方便用运行时方法调用, 着也是 router 与各个模块之间解耦的关键),routerMap: 维护这 url 和 block 之间的对应关系是界面跳转的关键, methodMap: 与 routerMap 的区别就在与 block 中代码块是调用方法的.
- // 运用的是 swift 中命名空间的概念, 用运行时方法 NSClassFromString 获取到相应的类型
- private static let routerModules:[String] = ["MessageProject.MessageProjectRouter",
- "IMProject.IMProjectRouter",
- "CommunityProject.CommunityProjectRouter",
- "CourseProject.CourseProjectRouter",
- "VideoProject.VideoProjectRouter",
- "QuestionbankProject.QuestionbankProjectRouter",
- "UserinfoProject.UserinfoProjectRouter",
- "CustomUIProject.CustomUIProjectRouter",
- "UIFrameProject.UIFrameProjectRouter",
- "BasicUIServiceProject.BasicUIServiceProjectRouter", "ActivityOperationProject.ActivityOperationProjectRouter"]
- private static var routerMap:Dictionary<String,[RouterHandler]> = [:]
- private static var methodMap:Dictionary<String,MethodHandler> = [:]
** RegisterRoutersProtocol: 声明一个通用接口, 在各个模块的 router 类中去实现 **
- // 每个模块需要实现一个该协议的类, 用于模块内部 VC 和 method 的注册
- public protocol RegisterRoutersProtocol {
- static func registerModuleRouters()
- }
UIViewController+Router: 在各个模块的 router 类中, 将需要跳转的 VC 进行注册
- // VC 注册, 子类需要的话可以重写
- @objc open class func registerRouterVC(_ routerURL:String)
- {
- guard let tempRouterURL = URL(string:routerURL) else {
- return
- }
- SDJGUrlRouterManager.registerRouterWithHandler(handler: { (transferURL:URL, transferType:SDJGTransfromType, sourceVC:UIViewController, userInfo:[String:Any]?, animated:Bool) -> UIViewController? in
- if transferURL.hasSameTrunkWithURL(tempRouterURL) {
- let viewController = self.init()
- viewController.setRouterInfo(userInfo: userInfo)
- if transferType == .push {
- if let nav = sourceVC.navigationController {
- // navController
- nav.pushViewController(viewController, animated: animated)
- } else {
- // modal nav vc
- sourceVC.modelVC(viewController, true, animated)
- }
- } else if transferType == .model {
- sourceVC.modelVC(viewController, false, animated)
- }else if transferType == .modelNav {
- sourceVC.modelVC(viewController, true, animated)
- } else {
- }
- return viewController
- } else {
- return nil
- }
- }, prefixURL: tempRouterURL)
- }
各个模块中 router 类: 继承自 NSObject, 实现 routerProtocol 中的注册方法, 在注册方法中调用各自 VC 的 UIViewController+Route 扩展方法进行跳转和方法注册. 同时这个类中也维护着 key 和类的对应关系.
- import Foundation
- import URLRouteProject
- // 课程下载界面
- public let kCourseFileDownloadURLString = "sina://router/downloadserviceproject/coursefiledownload"
- // 资料下载界面
- public let kDownLoadVCURLString = "sina://router/downloadserviceproject/download"
- class DownloadServiceProjectRouter: RegisterRoutersProtocol {
- public static func registerModuleRouters()
- {
- JCourseFileDownLoadVC.registerRouterVC(kCourseFileDownloadURLString)
- JDownLoadVC.registerRouterVC(kDownLoadVCURLString)
- }
- }
参数传递: 为了有更多的类型参数可以传递, 我们在 router 跳转方法里多加了一个参数, 而不是用 url 拼接的方式, 因为这样的话只能传递基本类型参数, 像 UIImage 这种就无能为力了.
- // 用于注册 VC Router 的闭包定义, 会在页面跳转的时候执行闭包, 参数为 [String:Any] 类型, 这样参数就可以随意传了.
- public typealias SDJGRouterHandler = (_ url:URL, _ transferType:SDJGTransfromType, _ sourceVC:UIViewController, _ userInfo:[String:Any]?, _ animated:Bool) -> UIViewController?
openURL 的处理: 我们为 openURL 提供了单独的方法跳转, 其中包含了参数的解析.
6.iOS 组件化实现方案和实际开发运营
CocoaPods 管理: 代码解耦只需要遵循上述原则就好, 最根本的目的是业务组件的解耦, cocoapod 的原理及使用在这里不在赘述(一搜一大堆)
正规的方式是 项目工程发布 tag->配置本地 podSpec 文件并上传 ->校验 ->私有库发布 ->其他工程引入. 但在实际操作中有很多情况 pod lib link 由于种种原因会失败, 而且发布私有库本身也需要时间, 所以在依赖不变的情况下我们可以用其他的方式引入其他模块代码
- // 拉取对应 commit 代码
- pod 'AFNetworking', :Git => 'https://github.com/gowalla/AFNetworking.git', :commit => '082f8319af'
- // 默认拉取 dev 分支最新代码
- pod 'AFNetworking', :Git => 'https://github.com/gowalla/AFNetworking.git', :branch => 'dev'
- // 拉取 0.7.0tag 的代码
- pod 'AFNetworking', :Git => 'https://github.com/gowalla/AFNetworking.git', :tag => '0.7.0'
直接修改对应提交的 commit, 这样同样缺点也很明显, 需要程序员自己保证代码无误才可以提交, 不会有 pod 的校验, 所以这两点需要我们权衡. 为了提高效率, 我们采用开发时提交 commit, 由各个业务负责人负责维护 commit, 每个版本发版时发布私有库的方式.
- # 社区项目 张三
- pod 'CommunityProject',:Git => 'http://172.16.117.224/ios-team/communityproject.git', :commit => '15407bae8eccafa14eab4d200e2a8ae763810f15'
- #用户信息项目 李四
- pod 'UserinfoProject',:Git => 'http://172.16.117.224/ios-team/userinfoproject.git',:commit => 'f1a408cd747e215f0b4fb08b4999edf00570c085'
- #活动运营 王二麻子
- pod 'ActivityOperationProject',:Git => 'http://172.16.117.224/ios-team/activityoperationproject.git', :commit => '432a21212fc5d6eed9d5d28eacb320e01ec9cc47'
- #课程项目 李六
- pod 'CourseProject',:Git => 'http://172.16.117.224/ios-team/courseproject.git', :commit => 'da10da98af8d53bfe15572958cef8d0cf5e5ba2a'
7. 讲讲坑
实际开发当中会遇到种种坑如下
注意类和方法及属性的权限问题 public,pravite 等(swift,oc 不用)
业务模块当然要有自己的测试入口, 否则很多业务场景都没有入口, 这就需要业务负责人自己添加自己的页面入口, 这也是组件化之后的好处, 每个业务组件都可以单独运行, 单独测试, 更加轻量级.
Unable to satisfy the following requirements:
这类问题是 / Users/xingfan/.CocoaPods/repos/master 也就是 cocoapod 的本地索引库没有更新最新, 里面没有 Charts(3.1.0)版本的 spec 文件, 导致它不知道去哪里拉代码. 执行 pod udate, 一般这种问题都是嫌 pod update 太慢执行 pod update --verbose --no-repo-update 导致的
pod update 会主动更新本地 repo, 如果报错, 可以指定到本地 spec 仓库, 一般在 cd ~/.CocoaPods/repos/iosspecrepo, 然后 Git clean -f, 如果再有问题, 那就是组件间依赖出错, 找相关负责人处理.
最后说说 spec 仓库, 本身就是一个 Git 仓库, pod repo update 就相当于拉取并同步远程 spec 仓库 (Git pull), 通过其中的 spec 文件(描述了目标源所在的地址, tag, 依赖库的版本等) 准确的找到想拉取的代码.
8. 谈谈优化.
1. 用 CocoaPods 的缺点, 代码集成到主工程后同样运行缓慢, 原因是因为拉取的代码依然是需要编译的, 本质上与原本没有区别. 针对这一点我们可以用 Cathage 替代 CocoaPods,CocoaPods (默认)自动建立和更新一个 Xcode workspace, 用来管理你的项目和所有依赖. Carthage 使用 xcodebuild 来编译出二进制库, 剩下的集成工作完全交给开发人员. 模块变成可执行的二进制文件之后运行速度自然会快很多. 有兴趣的同学可以自行研究.
来源: https://juejin.im/post/5c1b5fbd518825508464757f