IOS组件化方案总结

1.啥是组件化

timg.jpeg

打一个比较形象的比喻,把APP比作我们的人体,把胳膊、大腿、心、肝、肺这些人体器官比作组件,各个器官分别负责他们各自的功能,但是他们之间也有主次之分,试想我们的胳膊、大腿等是不能独立完成某个任务的,必须需要心、肺、肝、胆等的能量支持,那么可以把胳膊、大腿这种功能性器官比作业务组件,把我们的心、肝、脾、肺、肾比作基础组件。
那么我们的业务组件必须要依赖于我们的基础组件才能发挥其应有的功能,我们的基础组件(心、肝、肺等)是高度复用的,胳膊、大腿等业务组件要解耦合,难道你的胳膊动,大腿也要跟着动吗~,最终由大脑整合(那么大脑可以类比成主工程)。
不知道您是否领会了精神,综上就是我们的组件化的思路。


2.为什么要做组件化

继续我们上面那个惊悚的例子,但是我们把人变成机器人


钢铁侠.jpeg
  • 单独项目运行调试更快
    Tony直接穿戴手部机甲,肯定比穿戴全套的快呀
  • 各组件自由选择开发姿势
    Tony开发完手部机甲用的MVC,感觉这种不好,开发腿部的时候用MVVM了
  • 工程可以独立开发,方便 QA 有针对性地测试
    Tony研发手部功能,直接把手部穿上测试就好喽。
  • 业务分层、解耦使代码的可维护性更高
    Tony想升级手部的激光,动手就行了,别的地方都不用管!
  • 便于各业务功能拆分、抽离,实现真正的功能复用(特别是对于多APP来说更加突出)
    Tony想给残疾人研发个假腿,直接把腿部拿过来用就好喽
  • 业务隔离,跨团队开发代码控制和版本风险控制的实现
    Tony有钱可以雇好几个团队同步开发,你开发胳膊,我开发腿,互不干扰,完事装到一起就行

3.组件化的设计原则及目的

注:这里的稳定性指通俗地讲就是是否需要频繁修改代码

  • 越底层的模块,应该越稳定,越抽象,越高度复用。
    稳定性取决于是否需要频繁改变,底层库之所以要稳定是因为其会被业务层组件频繁调用,一旦变化,可能会影响几乎所有业务层组件,要做到设计一套API很久都不用改变,就需要设计的时候能越抽象, 即需要我们的抽象总结能力。
  • 注意稳定性传递
    稳定性是可以传递的,例如组件A稳定,组件B不稳定,组件A依赖于组件B,那么A其实也是不稳定的。
  • 自完备性有的时候要优于代码复用
    紧接上例:如何实现A、B的解耦呢?假设A依赖于B中的X代码段
    1>. 如果X是相对独立且高度复用的,我们当然可以将其提取出来如下
    未命名文件-5.png

    2>.如果X只是一个方法或者函数,并不适合单独提取出一个模块,那么直接copy一份X代码到B权衡之下也是没有问题的。

那么我们的最终目的可以总结为: 在基于模块设计原则上, 让模块之间没有循环依赖, 让业务模块之间解除依赖。


4.组件化耦合关系

综上所述我们项目组件化方案示意图可以是这样


未命名文件-8.png

如上图业务组件单向耦和于基础组件,这样的架构完成了基础组件的高度复用和业务组件的解耦。但是问题又来了,各个业务组件之间难免有页面跳转和数据交互,业务组件不耦合意味着不能直接调用,那么我们引入一个中间层。


改进耦合图-9.png

这里注意,依赖一定是单项的,否则我们只是把融在一起的代码块拆分成多个代码块,而且比之前更麻烦了。
未命名文件-4.png

5.中间层的实现方案

关于中间层实现众说纷纭,这里说下我们实践的方案。


未命名文件-10.png

主要分成四部分:

  • 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的原理及使用在这里不在赘述(一搜一大堆)


d9feb1e6d6503d261c9e90f0fdd942a4.png

正规的方式是
项目工程发布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:
    3951523621579_.pic_hd.jpg

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,884评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,347评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,435评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,509评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,611评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,837评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,987评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,730评论 0 267
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,194评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,525评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,664评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,334评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,944评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,764评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,997评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,389评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,554评论 2 349

推荐阅读更多精彩内容