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来编译出二进制库,剩下的集成工作完全交给开发人员。模块变成可执行的二进制文件之后运行速度自然会快很多。
有兴趣的同学可以自行研究。