前言
随着公司业务需求的不断迭代发展,工程的代码量和业务逻辑也越来越多,原始的开发模式和架构已经无法满足我们的业务发展速度了,这时我们就需要将原始项目进行一次重构,主要是对之前的高度耦合的业务组件和功能组件,将这些耦合拆分成互相独立的各个组件。
组件化定义
将APP拆分成各个组件或模块,相互之间不直接引用(解除这些模块之间的耦合),然后通过主工程将项目所需要的组件组合起来。实现代码的高内聚低耦合,方便多人多团队开发!。
为什么要进行组件化?
我们先来张图看看在没有使用组件化前,我们各个模块间的依赖关系
从各个业务组件的依赖关系来看,他们是互相依赖的,业务组件和业务组件间产生了严重的耦合关系,这样一来对我们工程的扩展性就会大大的降低,维护成本就会变高。
举个例子:假设某天产品经理说,咱们公司的业务发展的太好了,咱们的营销模块需要独立出来成一个单独的应用,就需要重新写一个营销应用,之前的代码剥离不干净了。
体会:
各个业务模块和业务模块间的高度耦合,功能组件和功能组件间的高度耦合对未来公司的业务扩展来说,成本很高,不能做到同样业务逻辑的代码的高度复用,这样对我们开发来说也是效率的降低。
能做到各个业务模块间完全的解耦了,他们不再互相依赖了,同时我们引入了一个中间调度者的一个角色,现在是各个业务模块和这个中间调度者角色产出了严重的依赖。
思考
我们的各个业务模块依赖这个中间调度者,这个是完全正常的,因为他们需要这个调度者来做统一的事件分发工作,但是这个调度者却又依赖了每个业务模块,这层依赖是有必要的吗?
例如:假设我们现在有一个新的B APP需要开发,这时我们也需要用到这个中间调度者组件,但是我们不能直接拿过来用,因为它又依赖了很多A App的业务组件。因此,我们的组件化架构设计又需要一次升级变更了,升级成如下图所示的模型。
升级版
单项调用
此时,各个业务模块间只会依赖中间调度者,并且中间调度者不对各个模块产生任何的依赖。
各个组件该如何进行拆分
项目主工程
当我们工程完全使用组件化架构进行开发后,我们会惊奇的发现我们的主工程就成了一个空壳子工程。因为所有的主工程呈现出来的内容都被拆分成了各个独立的业务组件了,包括各个工具组件也是各自互相独立的。这样我们发现开发一个完整的APP就像是搭建乐高积木一样,各个部件都有,任我们随意的组合搭建,这样是不是感觉很爽。
业务组件
业务组件就是我们上面示例图所示的各个独立的产品业务功能模块,我们将其封装成独立的组件。我们通过组装各个独立的业务组件来搭建一个完整的APP项目。
基础工具类组件
基础工具类是各个互相独立,没有任何依赖的工具组件。它们和其它的工具组件、业务组件等没有任何依赖关系。这类组件例如有:对数组,字典进行异常保护的Safe组件,对数组功能进行扩展Array组件,对字符串进行加密处理的加密组件等等。
中间件组件
这个组件比较特殊,这个是我们为了实现组件化开发而衍生出来的一个组件,上面示例图中的中间调度者就是一个功能独立的中间件组件。
基础UI组件
视图组件就比较常见了,例如我们封装的导航栏组件,Modal弹框组件,PickerView组件等。
业务工具组件
这类组件是为各个业务组件提供基础功能的组件。这类组件可能会依赖
其他的组件
例如:网络请求组件,图片缓存组件,jspatch组件等等
至于组件的拆分颗粒度,这个着实不好去断定,因人而异,不同的需求功能复杂度拆分出来的组件大小也不尽相同
1.耦合比较严重
(因为没有明确的约束,「组件」间引用的现象会比较多)
2.容易出现冲突
(尤其是使用 Xib,还有就是 Xcode Project,虽说有脚本可以改善)
3.业务方的开发效率不够高
(只关心自己的组件,却要编译整个项目,与其他不相干的代码糅合在一起)
组件化优缺点
优点
降低耦合度。组件化抽取出来是一个模块,可以直接用pods进行管理的
组件单独开发,单独测试。每个模块都可以由专门维护的人去进行单独开发,开发测试过程中,可以只编译自己那部分代码,不需要编译整个项目代码。
加快编译速度
代码结构更加清晰,维护更加方便快捷
代码复用性高,提高开发效率
缺点
增加了代码的冗余;
增加了项目的复杂度;
版本同步问题
如何进行组件化
- App启动时实例化各组件模块,然后这些组件向ModuleManager注册Url,有些时候不需要实例化,使用class注册。
- 当组件A需要调用组件B时,向ModuleManager传递URL,参数跟随URL以GET方式传递,类似openURL。然后由ModuleManager负责调度组件B,最后完成任务。
- 组件和Mediator之间只能进行单向调用,避免过度耦合
组件化拆分
基础组件拆分
(宏定义、分类、工具类...)
功能拆分
(数据库、网络框架、图片加载...)
业务拆分
(登录、聊天、商城、社区...)
组件化通信
Router(负责模块之间的业务往来)
以 iOS 为例,由于之前就是采用的 URL 跳转模式,理论上页面之间的跳转只需 open 一个 URL 即可。所以对于一个组件来说,只要定义「支持哪些 URL」即可
[MGJRouter registerURLPattern:@"mgj://detail?id=:id" toHandler:^(NSDictionary *routerParameters) {
NSNumber *id = routerParameters[@"id"];
// create view controller with id
// push view controller
}];
首页只需调用 [MGJRouter openURL:@"mgj://detail?id=404"]
就可以打开相应的详情页
组件化问题
蘑菇街问题
1.采用openURL进行App组件化是一个错误的做法,使用注册的方式发现服务是一个不必要的做法
- URL注册对于实施组件化方案是完全不必要的,且通过URL注册的方式形成的组件化方案,拓展性和可维护性都会被打折
- 在组件化过程中,注册URL并不是充分必要条件,组件是不需要向组件管理器注册Url的。而且注册了Url之后,会造成不必要的内存常驻,如果只是注册Class,内存常驻量就小一点,如果是注册实例,内存常驻量就大了。至于蘑菇街注册的是Class还是实例.
2.采用openURL致命缺陷非常规对象无法参与本地组件间调度
- 在iOS领域里,一定是组件化的中间件为openUrl提供服务,而不是openUrl方式为组件化提供服务
一个App的组件化方案一定不是建立在URL上的,openURL的跨App调用是可以建立在组件化方案上的。当然,如果App还没有组件化,openURL方式也是可以建立的,就是丑陋一点而已。 - App实施组件化方案的过程中是基于openURL致命缺陷==非常规对象无法参与本地组件间调度==
- 参数只能通过在字符串Url后面进行拼接,参数多了比较麻烦
3.业务工程师在本地间调用时,是不需要知道URL的,传params容易懵逼
4.蘑菇街没有拆分远程调用和本地间调用
5.蘑菇街的本地间调用无法传递非常规参数,复杂参数的传递方式非常丑陋
6.蘑菇街必须要在app启动时注册URL响应者
7.新增组件化的调用路径时,蘑菇街的操作相对复杂
8.蘑菇街没有针对target层做封装
解决
1.参数问题解决
比如你要调用一个图片编辑模块,不能传递UIImage到对应的模块上去的话,这是一个很悲催的事情。 当然,这可以通过给方法新开一个参数,然后传递过去来解决
[a openUrl:"http://casa.com/detail" params:@{
@"id":"123",
@"type":"0",
@"image":[UIImage imageNamed:@"test"]
}]
2.注册问题解决(通过runtime调用不同的target)
注册部分的代码的维护是一个相对麻烦的事情,每一次支持新调用时,都要去维护一次注册列表。如果有调用被弃用了,是经常会忘记删项目的。runtime由于不存在注册过程,那就也不会产生维护的操作,维护成本就降低了。
通过runtime做到了服务的自动发现,拓展调用接口的任务就仅在于各自的模块,任何一次新接口添加,新业务添加,都不必去主工程做操作,十分透明。
使用
1.创建一个HKOCRouter的Category
#import "HKOCRouter.h"
NS_ASSUME_NONNULL_BEGIN
@interface HKOCRouter (HKModuleAActions)
- (UIViewController *)viewControllerForDetail;
@end
NS_ASSUME_NONNULL_END
-----------------------------------------------------
#import "HKOCRouter+HKModuleAActions.h"
NSString * const kHKOCRouterTargetA = @"A";
#pragma mark - Method-List
NSString * const kHKRouterActionNativFetchDetailViewController = @"nativeFetchDetailViewController";
@implementation HKOCRouter (HKModuleAActions)
- (UIViewController *)viewControllerForDetail
{
UIViewController *viewController = [HKOCRouter hk_localPerformTarget:kHKOCRouterTargetA action:kHKRouterActionNativFetchDetailViewController param:@{@"key":@"value"} shouldCacheTarget:NO];
if ([viewController isKindOfClass:[UIViewController class]]) {
// view controller 交付出去之后,可以由外界选择是push还是present
return viewController;
} else {
// 这里处理异常场景,具体如何处理取决于产品
return [[UIViewController alloc] init];
}
}
@end
2.创建Target_A和DemoModuleADetailViewController
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface Target_A : NSObject
- (void)Action_getUserInfo:(NSDictionary *)params;
- (UIViewController *)Action_nativeFetchDetailViewController:(NSDictionary *)params;
@end
NS_ASSUME_NONNULL_END
-----------------------------------------------------
#import "Target_A.h"
#import "DemoModuleADetailViewController.h"
@implementation Target_A
- (void)Action_getUserInfo:(NSDictionary *)params {
[HKMessageBox showMessage:@"Coming"];
}
- (UIViewController *)Action_nativeFetchDetailViewController:(NSDictionary *)params
{
// 因为action是从属于ModuleA的,所以action直接可以使用ModuleA里的所有声明
DemoModuleADetailViewController *viewController = [[DemoModuleADetailViewController alloc] init];
viewController.valueLabel.text = params[@"key"];
return viewController;
}
@end
3.调用
if (indexPath.row == 0) {
UIViewController * viewController = [
[HKOCRouter sharedInstance] viewControllerForDetail
];
// 获得view controller之后,在这种场景下,到底push还是present,其实是要由使用者决定的,mediator只要给出view controller的实例就好了
[self presentViewController: viewController animated: YES completion: nil];
}
if (indexPath.row == 1) {
UIViewController * viewController = [
[HKOCRouter sharedInstance] viewControllerForDetail
];
[self.navigationController pushViewController: viewController animated: YES];
}
if (indexPath.row == 2) {
// 这种场景下,很明显是需要被present的,所以不必返回实例,mediator直接present了
[HKOCRouter hk_remotePerformWithUrl: @ "hk://A/getUserInfo"
handler: ^ (NSDictionary * result) {
}
];
}
参考
https://limboy.me/tech/2016/03/10/mgj-components.html
https://casatwy.com/iOS-Modulization.html
https://www.cnblogs.com/oc-bowen/p/5885476.html