一,概述  

随着公司业务需求的不断迭代发展,工程的代码量和业务逻辑也越来越多,原始的开发模式和架构已经无法满足我们的业务发展速度了,这时我们就需要将原始项目进行一次重构大手术了。这时我们应该很清晰这次手术的动刀口在哪,就是之前的高度耦合的业务组件和功能组件,手术的目的就是将这些耦合拆分成互相独立的各个组件。

二,为什么要用组件化

  我们先来张图看看在没有使用组件化前,我们各个模块间的依赖关系

从上面这种各个业务组件的依赖关系来看,他们是互相依赖的,业务组件和业务组件间产生了严重的耦合关系,这样一来对我们工程的扩展性就会大大的降低,维护成本就会变高。

举个例子:

假设某天产品经理说,咱们公司的业务发展的太好了,咱们的营销模块需要独立出来成一个单独的应用,以便于咱们可以添加更多高效的营销手段。这时我们就傻眼了,需要独立出一个app出来,这可怎么搞啊,营销模块的代码和其他的很多业务代码耦合在一起了,现在要独立出来,那就只能重新写一个营销应用了,之前的代码剥离不干净了。

从上面我们列举的一个简单的例子可以体会到:

在项目没有做到真真意义上的组件化之前,各个业务模块和业务模块间的高度耦合,功能组件和功能组件间的高度耦合对未来公司的业务扩展来说,成本很高,不能做到同样业务逻辑的代码的高度复用,这样对我们开发来说也是效率的降低。

好了,有的同学可能会说,既然上面各个模块间耦合这么高,那我就来将这些耦合解耦,于是,可能会出现下面这张图的模块间的关系。

从下面这张图来看,我们发现,现在确实能做到各个业务模块间完全的解耦了,他们不再互相依赖了,同时我们引入了一个中间调度者的一个角色,现在是各个业务模块和这个中间调度者角色产出了严重的依赖。我们思考下发现,我们的各个业务模块依赖这个中间调度者,这个是完全正常的,因为他们需要这个调度者来做统一的事件分发工作,但是这个调度者却又依赖了每个业务模块,这层依赖是有必要的吗?我们回头想想真正的组件化开发是完全的去依赖化,这个依赖是完全没有必要的。

例如:假设我们现在有一个新的B APP需要开发,这时我们也需要用到这个中间调度者组件,但是我们不能直接拿过来用,因为它又依赖了很多A App的业务组件。因此,我们的组件化架构设计又需要一次升级变更了,升级成如下图所示的模型。

从上面的这张图,我们可以看出,各个业务模块间只会依赖中间调度者,并且中间调度者不对各个模块产生任何的依赖。

三,各个组件该如何进行拆分

关于组件该如何拆分,这个没有一个完整的标准,因为每个公司的业务场景不一样,对应衍生出来的各个业务模块也就不一样,所以业务组件间的拆分,这个根据自己公司的业务模块来进行合理的划分即可。这里我们来说下整个工程的组件大致的划分方向

  1. 项目主工程:当我们工程完全使用组件化架构进行开发后,我们会惊奇的发现我们的主工程就成了一个空壳子工程。因为所有的主工程呈现出来的内容都被拆分成了各个独立的业务组件了,包括各个工具组件也是各自互相独立的。这样我们发现开发一个完整的APP就像是搭建乐高积木一样,各个部件都有,任我们随意的组合搭建,这样是不是感觉很爽。
  2. 业务组件:业务组件就是我们上面示例图所示的各个独立的产品业务功能模块,我们将其封装成独立的组件。例如示例Demo中的电子发票业务组件,业务组件A,业务组件B。我们通过组装各个独立的业务组件来搭建一个完整的APP项目。
  3. 基础工具类组件:基础工具类是各个互相独立,没有任何依赖的工具组件。它们和其它的工具组件、业务组件等没有任何依赖关系。这类组件例如有:对数组,字典进行异常保护的Safe组件,对数组功能进行扩展Array组件,对字符串进行加密处理的加密组件等等。
  4. 中间件组件:这个组件比较特殊,这个是我们为了实现组件化开发而衍生出来的一个组件,上面示例图中的中间调度者就是一个功能独立的中间件组件。
  5. 基础UI组件:视图组件就比较常见了,例如我们封装的导航栏组件,Modal弹框组件,PickerView组件等。
  6. 业务工具组件:这类组件是为各个业务组件提供基础功能的组件。这类组件可能会依赖到其他的组件。例如:网络请求组件,图片缓存组件,jspatch组件等等

  至于组件的拆分颗粒度,这个着实不好去断定,因人而异,不同的需求功能复杂度拆分出来的组件大小也不尽相同

四,如何从零到一搭建组件化架构

在讲如何从零到一来实现一个组件化架构项目前,我们需要熟练掌握使用pod来制作组件库。下面我们就围绕提供的组件化示例项目来展开讲解。

首先,我们来看示例Demo中包含哪些业务组件(如下图所示:):

示例Demo中,我提供了三个业务组件来作为演示效果,其中业务模块A和业务模块B是临时业务模块组件,电子发票业务组件时真实的企业需求功能组件。

我们再来看下示例Demo中都提供了哪些工具组件(如下图所示)

注意了:这里提供的6个工具组件也都是作者已经封装好的功能组件

五,详细操作步骤

  • 第一步:
    我们先创建一个空的iOS工程项目:MainProject,这个空项目作为我们的主工程项目,就是上面所说的壳子工程项目,然后初始化pod,这里不清楚pod的使用的小伙伴们请自行查阅资料。

  • 第二步:
    我们创建一个空工程项目:ModuleA,这个ModuleA 项目作为我们的业务A组件。然后我们初始化pod,初始化podspec文件。
  • 第三步:
    我们创建一个空工程项目:ModuleB,这个ModuleB 项目作为我们的业务B组件。然后我们初始化pod,初始化podspec文件。
  • 第四步:
    我们创建一个空工程项目:ComponentMiddleware,这个项目就是我们上面所说的中间调度者。然后我们初始化pod,初始化podspec文件。
  • 第五步:
    我们创建一个空工程项目: ModuleACategory,这个工程是对应业务组件A的一个分类工程。然后我们初始化pod,初始化podspec文件。
  • 第六步:

    我们创建一个空工程项目: ModuleBCategory,这个工程是对应业务组件B的一个分类工程。然后我们初始化pod,初始化podspec文件。

    好了,上面的主工程和两个业务组件工程,以及两个组件分类工程都已创建完毕,下面我们来讲解他们各个之间如何工作的。我就从主工程加载业务组件开始往下捋,顺藤摸瓜式的引出每个工程的用意。

  • 第七步:

    我们在主工程MainProject的Podfile中引入我们的业务组件B工程ModuleB,以及引入我们的ModuleB的分类工程:ModuleBCategory。然后我们pod install。这时已将这两个组件库引入到我们的主工程中了。

    示例代码如下:

    1. # Uncomment the next line to define a global platform for your project
    2. platform :ios, '8.0'
    3.  
    4. source 'https://github.com/CocoaPods/Specs.git'
    5. source 'https://github.com/guangqiang-liu/GQSpec.git'
    6.  
    7. target 'GQComponentDemo' do
    8.  
    9. pod 'ModuleB'
    10. pod 'ModuleBCategory'
    11. end

    然后我们在主工程中添加一个按钮事件,这个事件是点击 push 到业务组件B的 页面。

    示例代码如下:

    1. #import <ModuleBCategory/ComponentScheduler+ModuleB.h>
    2.  
    3. - (void)moduleB {
    4. UIViewController *VC = [[ComponentScheduler sharedInstance] ModuleB_viewControllerWithCallback:^(NSString *result) {
    5. NSLog(@"resultB: --- %@", result);
    6. }];
    7. [self.navigationController pushViewController:VC animated:YES];
    8. }
  • 第八步:

    上面第七步中,我们用到了ModuleBCategory 这个分类工程。这个工程我们只对外暴露了两个文件。这两文件是上面的中间调度者的分类,也就是说是中间件的分类。我们先来看下这个分类文件的.h 和.m 实现。

    .h

    1. #import "ComponentScheduler.h"
    2.  
    3. @interface ComponentScheduler (ModuleB)
    4. - (UIViewController *)ModuleB_viewControllerWithCallback:(void(^)(NSString *result))callback;
    5. @end

    .m

    1. #import "ComponentScheduler+ModuleB.h"
    2.  
    3. @implementation ComponentScheduler (ModuleB)
    4. - (UIViewController *)ModuleB_viewControllerWithCallback:(void(^)(NSString *result))callback {
    5.   NSMutableDictionary *params = [[NSMutableDictionary alloc] init];
    6.   params[@"callback"] = callback;
    7.   return [self performTarget:@"ModuleB" action:@"viewController" params:params shouldCacheTarget:NO];
    8. }
    9. @end

    我们发现这个分类实现非常的简单,就是对外暴露一个函数,然后执行

    1. [self performTarget:@"ModuleB" action:@"viewController" params:params shouldCacheTarget:NO];

    并将执行的返回值返回出去。

    这个分类的作用你可以理解为我们提前约定好Target的名字和Action的名字,因为这两个名字中间件组件中会用到。

    上面的performTarget:action:params:shouldCacheTarget函数是中间件提供的函数。因为ModuleBCategory 是 ComponentScheduler(中间件)的分类文件,所以可以调用到这个函数啦。

    在ModuleBCategory 工程中需要引用到了中间件工程所以我们需要在ModuleBCategory 的Podfile文件中引用 中间件组件

    示例代码如下:

    1. # Uncomment the next line to define a global platform for your project
    2. platform :ios, '8.0'
    3.  
    4. source 'https://github.com/CocoaPods/Specs.git'
    5. source 'https://github.com/guangqiang-liu/GQSpec.git'
    6.  
    7. target 'ModuleB-Category' do
    8. # Uncomment the next line if you're using Swift or would like to use dynamic frameworks
    9. # use_frameworks!
    10.  
    11. # Pods for ModuleB-Category
    12.  
    13. pod 'ComponentScheduler'
    14.  
    15. end
  • 第九步:

    因为上面第八步中引用到中间件工程,这里我们就来看下中间件工程到底做了什么工作。还记得上面第八步中,我们调用了一个中间件提供的函数:performTarget:action:params:shouldCacheTarget吧,这个是中间件核心函数。

    核心函数代码块如下:

    还记得上面第八步中,我们调用这个函数传递的参数吧,我们在把调用代码拿过来看下

    1. [self performTarget:@"ModuleB" action:@"viewController" params:params shouldCacheTarget:NO];

    我们可以看到 TargetName是我们传递的 ModuleBaction是我们传递的viewController,然后我们将 这两个参数传给了下面的函数:

    1. [self safePerformAction:action target:target params:params];

    我们来看下这两个参数的值具体是什么:

     

    这个函数最终调用到苹果官方提供的函数:

    1. [target performSelector:action withObject:params];

    看到 performSelector: withObject:大家应该就比较熟悉了,iOS的消息传递机制。

    1. [Target_ModuleB performSelector:Action_viewController withObject:params];

    上面这行伪代码意思是: Target_ModuleB这个类 调用它的 Action_viewController:方法,然后传递的参数为 params

    细心的小伙伴们就会发现,我们没有看到过哪里有这个Target_ModuleB类啊,更没有看到Target_ModuleB调用它的 Action_viewController:方法啊。

    是的,这个Target_ModuleB类和类的Action_viewController方法就在第十步中讲解到。

  • 第十步:

    终于到了最后一步了,写的好艰辛,嗯,小伙们不要捉急,快了,快讲完了

    细心的小伙们发现,我们上面讲的9步中,好像都没有提业务组件B的东西。是的,业务组件B除了提供组件B的业务功能外,业务组件B还需要为我们提供一个Target文件。

    我们先来看下业务组件B的业务代码:

    示例代码如下:

    1. #import "ModuleBViewController.h"
    2. #import "PageBViewController.h"
    3.  
    4. @interface ModuleBViewController ()
    5.  
    6. @end
    7.  
    8. @implementation ModuleBViewController
    9.  
    10. - (void)viewDidLoad {
    11. [super viewDidLoad];
    12. // Do any additional setup after loading the view.
    13. self.title = @"我是模块B业务组件";
    14.  
    15. self.view.backgroundColor = [UIColor whiteColor];
    16.  
    17. UIButton *btn = [UIButton buttonWithType:UIButtonTypeCustom];
    18. btn.frame = CGRectMake(, , , );
    19. btn.backgroundColor = [UIColor greenColor];
    20. btn.center = self.view.center;
    21. [btn setTitle:@"模块B业务功能组件" forState: UIControlStateNormal];
    22. [btn addTarget:self action:@selector(push) forControlEvents:UIControlEventTouchUpInside];
    23. [self.view addSubview:btn];
    24. }
    25.  
    26. - (void)push {
    27. PageBViewController *VC = [[PageBViewController alloc] init];
    28. [self.navigationController pushViewController:VC animated:YES];
    29. }
    30.  
    31. - (void)didReceiveMemoryWarning {
    32. [super didReceiveMemoryWarning];
    33. // Dispose of any resources that can be recreated.
    34. }

    我们发现,业务组件B的业务代码也很简单,就是做一个push 跳转操作,从PageA 控制器跳转到 PageB 控制器。 这个没有什么好讲的

    我们再来看上面提到的target文件

    示例代码如下:

    .h

    1. #import <Foundation/Foundation.h>
    2. #import <UIKit/UIKit.h>
    3.  
    4. @interface Target_ModuleB : NSObject
    5.  
    6. - (UIViewController *)Action_viewController:(NSDictionary *)params;
    7.  
    8. @end

    .m

    1. #import "Target_ModuleB.h"
    2. #import "ModuleBViewController.h"
    3.  
    4. @implementation Target_ModuleB
    5.  
    6. - (UIViewController *)Action_viewController:(NSDictionary *)params {
    7. ModuleBViewController *VC = [[ModuleBViewController alloc] init];
    8. return VC;
    9. }
    10. @end

    从上面的实现文件中,我们可以看到,Target文件的作用也很简单,就是为我们提供导航跳转的目标控制器实例对象。这里的目标控制器实例就是业务组件B的ModuleBViewController实例。

    细心的小伙伴们发现,咦!我们在第九步中打印出来的target和 action不就正是Target文件的Target_ModuleB和 Action_viewController:

    上面我们只是串讲了业务组件B的一系列流程,业务组件A的用法和业务组件B的用法一样,如果后面再有业务组件C,D,都是一样的道理,就不再一一讲解了。

    好了,现在小伙伴们应该看懂了这一连串的工作流程了吧,如果还没有看懂,可以看看Casa的讲解CTMediator。作者建议直接运行提供的示例Demo项目进行调试,这样便于理解各个组件之间的关系。

六,总结

上面我们讲解的只是简单的项目组件化架构的基础框架搭建,但是在真正的企业开发中,我们只搭建这样一个简单项目框架结构还远远不能满足需求的开发,我们还需要在项目框架中添枝加叶来满足现有需求。

  最后,我们再来看张组件化完整的架构图:

 

参考文献

Cocoapods组件化之搭建组件化项目框架的更多相关文章

  1. Github+yeoman+gulp-angular初始化搭建angularjs前端项目框架

    在上篇文章里面我们说到了Github账号的申请与配置 那么当你有了Github账号并创建了一个自己的Github项目之后,首要的当然是搭建自己的项目框架啦! 本人对自己的定位是web前端狗,常用开发框 ...

  2. 基于mpvue搭建小程序项目框架

    简介: mpvue框架对于从没有接触过小程序又要尝试小程序开发的人员来说,无疑是目前最好的选择.mpvue从底层支持 Vue.js 语法和构建工具体系,同时再结合相关UI组件库,便可以高效的实现小程序 ...

  3. Android 从零开始搭建一个主流项目框架—RxJava2.0+Retrofit2.0+OkHttp

    我这里的网络请求是用的装饰者模式去写的,什么是装饰者模式呢?在不必改变原类文件和使用继承的情况下,动态地扩展一个对象的功能.它是通过创建一个包装对象,也就是装饰来包裹真实的对象.我的理解就是一个接口, ...

  4. 搭建一个SSH项目框架的步骤

    1.导入jar包(38个) 2.配置文件 applicationContext,xml (beans.xml) (数据源.LocalSessionFactoryBean.事务管理器.事务通知.AOP切 ...

  5. 简单搭建iOS开发项目框架

    今天我们来谈谈如何搭建框架,框架需要做一些什么. 第一步:找到我们的目标我们的目标是让其他开发人员拿到手后即可写页面,不再需要考虑其他的问题. 第二步:我们需要做哪些东西各位跟着我一步一步来进行. 假 ...

  6. 手把手教你从零开始搭建SpringBoot后端项目框架

    原料 新鲜的IntelliJ IDEA.一双手.以及电脑一台. 搭建框架 新建项目 打开IDE,点击File -> New Project.在左侧的列表中的选择Maven项目,点击Next. 填 ...

  7. asp.net -mvc框架复习(7)-基于MVC搭建用户登录项目框架

    整体框架: 一.搭建Model层 1.添加通用数据访问类 2.添加实体类(封装和传递数据,和数据库中数据表对应) 3.添加数据访问类(通常和实体类同名,但是后缀名发生改变) 二.搭建控制器层Contr ...

  8. SpringCloud:搭建微服务项目框架 microservicecloud

    1.搭建整体父工程 microservicecloud 新建父工程microservicecloud,切记是Packageing是pom模式 主要是定义POM文件,将后续各个子模块公用的jar包等统一 ...

  9. asp.net mvc 简单项目框架的搭建(二)—— Spring.Net在Mvc中的简单应用

    摘要:上篇写了如何搭建一个简单项目框架的上部分,讲了关于Dal和Bll之间解耦的相关知识,这篇来把后i面的部分说一说. 上篇讲到DbSession,现在接着往下讲. 首先,还是把一些类似的操作完善一下 ...

随机推荐

  1. [Usaco2005 mar]Yogurt factory 奶酪工厂

    接下来的N(1≤N10000)星期中,奶酪工厂在第i个星期要花C_i分来生产一个单位的奶酪.约克奶酪工厂拥有一个无限大的仓库,每个星期生产的多余的奶酪都会放在这里.而且每个星期存放一个单位的奶酪要花费 ...

  2. kNN算法实例(约会对象喜好预测和手写识别)

    import numpy as np import operator import random import os def file2matrix(filePath):#从文本中提取特征矩阵和标签 ...

  3. Spring Framework基础学习

    Spring Framework基础学习 Core support for dependency injection,transaction management,web applications,d ...

  4. Node.js实战10:“流”是Node.js最强大的功能之一。

    流是Nodejs的高级应用,掌握流的使用,才能真正胜任NodeJS开发. Nodejs中,流是基于事件的API,用于管理和处理数据,而且效率很好! 什么是流? 流是一个抽象接口,Node 中有很多对象 ...

  5. [Web 前端] 008 css 颜色表示方法

    css 颜色表示法 颜色名表示 如 red 红色 green 绿色 blue 蓝色 16 进制数值表示 常见颜色 正常表示 缩写表示 红色 #ff0000 #f00 绿色 #00ff0 #0f0 蓝色 ...

  6. python的继承、重载和重写???

    继承语法:<1>单继承:class(父类名)<2>多继承class(父类1,父类2,父类n...) 继承的特点:<1>减少代码量和灵活指定型类<2>子类 ...

  7. Python 3实现网页爬虫

    1 什么是网页爬虫 网络爬虫( 网页蜘蛛,网络机器人,网页追逐者,自动索引,模拟程序)是一种按照一定的规则自动地抓取互联网信息的程序或者脚本,从互联网上抓取对于我们有价值的信息.Tips:自动提取网页 ...

  8. Oracle 常用统计视图汇总

         Oracle统计信息对数据库性能优化和故障排除都相当重要,目前接触到的与统计信息相关的视图大体有 4 个:   1.v$sysstat 视图      该视图用于记录系统级的统计信息,共 5 ...

  9. 分布式唯一ID生成器

    在应用程序中,经常需要全局唯一的ID作为数据库主键.如何生成全局唯一ID? 首先,需要确定全局唯一ID是整型还是字符串?如果是字符串,那么现有的UUID就完全满足需求,不需要额外的工作.缺点是字符串作 ...

  10. canvas画随机的四位验证码

    效果图如下: <!DOCTYPE html> <html lang="en"> <head> <meta charset="UT ...