组件化开发是大家经常讨论的话题,也是随着项目的迭代、工程会变得越来越臃肿而想到的解决方案,其实目前网上组件化方案非常多,每一种都有自己的优缺点,我个人觉得没有最好的方案,只有最适合自己项目的方案。
在这里给大家介绍下最近我在公司用的一套组件化解决方案:项目中组件通过protocol协议依赖,达到组件间的解耦合的效果,组件不需要知道其它组件具体实现类,即使其它组件不存在也可以正常编译运行,业务组件可快速集成到其它项目中,同时可通过配置文件解决多个项目中存在差异化的问题。
下图是项目的架构层级图:
这里主要介绍下基础组件层中三个组件的具体实现:
YPLaunchManager 用于管理各个业务模块在 UIApplicationDelegate 事件中的任务,隔离业务与App Delegate之间的耦合;
YPProtocolMediator Protocol-ClassName 组件化实现方案的中间件,用于各个业务模块之间的通信;
YPAppModuleConfigManager 管理各个业务module的配置,主要解决一个module用于不同项目中时且存在差异的问题。
1,YPLaunchManager 用于对AppDelegate与业务功能/模块解耦
顾名思义是一个启动任务管理器,iOS 项目启动相关的事件都在AppDelegate.m 文件里面,因此YPLaunchManager是为了解决 AppDelegate 过于复杂且易耦合其它业务模块的问题。
YPLaunchManager 用于管理YPLaunchTask任务,每个业务功能/模块需继承自YPLaunchTask,并且实现相应的协议方法,通过 load 方案进行自动注册,还可以通过finishAfter 来控制业务功能/模块的生命周期,
@interface YPXXXXXLaunchTask : YPLaunchTask
@end
......
// load 方法里面增加 YPLaunchTaskInitNotification 初始化注册的通知,在通知里面调用 +add 来注册self
+ (void)load {
[[NSNotificationCenter defaultCenter] addObserverForName:YPLaunchTaskInitNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification * _Nonnull note) {
[[self class] add];
}];
}
// 通过 priority 来控制每个任务的优先级调用
- (NSUInteger)priority {
return YPCoreSessionPriority;
}
// 通过finishAfterRun控制Task的生命周期
- (BOOL)finishAfterRun {
return NO;
}
// 实现 YPLaunchTaskDelegate,与 UIApplicationDelegate 代理方法一致
- (void)runAppDidFinishLauchingBeforeUI {
// 初始化网络相关的数据
}
- (void)runAppDidFinishLauchingAfterUI {
}
最后AppDelegate 里面的代码如下:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// 发送 YPLaunchTaskInitNotification 通知,注册所有的LaunchTask
[[YPLaunchManager defautlManager] loadAllLaunchTask];
[[YPLaunchManager defautlManager] runAppDidFinishLauchingBeforeUI];
//这里需要 设置 self.window.rootViewController = xxx;
[[YPLaunchManager defautlManager] runAppDidFinishLauchingAfterUI];
return YES;
}
- (void)applicationDidEnterBackground:(UIApplication *)application {
[[YPLaunchManager defautlManager] runAppDidEnterBackground];
}
附YPLaunchManager的时序图:
2,YPProtocolMediator 是一种 Protocol-ClassName 组件化方案的中间件
YPProtocolMediator 通过Protocol与Modlue 的 ClassName匹配进行查找到具体实现的组件类,然后组件类实现的Protocol协议方法来实现通信的一种机制。
每个组件如果需要外界主动调用则需要实现port协议,如果需要向外界传递数据则应实现delegate协议,以下是 YPProtocolMediator 的框架图:
XXX_Port 定义其它模块调用的接口协议
XXX_Delegate 定义其它模块接收数据的接口协议;
以下是A与B模块的通信的实例:
// ModuleA 从ModuleB 获取数据
// 方式一,通过 port_excuteSelector 方法通信,即使@selector 为声明编译也不会报错
NSString *uid = [YPMediatorPort(YPUserCenterPort) port_excuteSelector:@selector(port_Uid)];
// selector 带参数时用下面的方法,注意的是args 传入的是可变参数,参数不能为nil
[YPMediatorPort(YPUserCenterPort) port_excuteSelector:@selector(port_Login:password:) args:PhoneNumber, YPParamVaule(Value), nil];
// 方式二:通过获取 ModuleB 的实例,直接调用方法
NSNumber *userId = [YPMediatorPort(YPUserCenterPort) port_Uid];
[YPMediatorPort(ModuleXXXB_Port) port_Login:@"phoneNumber" password:@"password"];
方式一 和 方式二的区别是,如果 ModuleXXXB_Port 更新且不兼容旧版,删除了某个方法。
方式一可以编译通过,但是会报警告;
方式二编译不通过,直接报错;
所以当 ModuleA 强依赖 ModuleB 里面的某个方法时,则建议使用方式二通信,其它情况则建议方式一进行通信。
ModuleA 接收 ModuleB 的数据传递:
// 方法一:通过 YPProtocolMediator 组件实现的给类对象动态添加函数的方法(-addSelector:forProtocol:block:bindTarget:)进行通信
// 向 ModuleA 类添加方法通过 block 回调来实现数据传递
[self yp_addDelegateSelector:@selector(userCenter:loginStatus:) paramBlock:^(YPParams *params) {
id targetB = params[0];
NSNumber *loginStatus = [params objectAtIndex:1];
NSLog(@"target %@ %@, status is %@",targetB, loginStatus);
} forProtocol:@protocol(YPUserCenterDelegate)];
// 方法二:可以像传统的delegate来实现数据传递
// 1,将self 与 ModuleXXXB_Delegate 代理绑定
YPMediatorBind(self, ModuleXXXB_Delegate);
// 2,ModuleA 实现 ModuleXXXB_Delegate 的代理方法
- (void)userCenter:(id<ModuleXXXB_Port>)moduleXXXB loginStatus:(BOOL)loginStatus {
}
YPProtocolMediator 组件查找实现:
诚如上面说的 ,YPProtocolMediator 查找Module 是通过Protocol 与 ClassName 匹配进行查找的。
实现代码如下:
/**根据传入的组件协议返回实现该协议的类的对象*/
- (id)moduleInstanceFromProtocol:(Protocol *)protocol {
// 获取协议名字
NSString *className = NSStringFromProtocol(protocol);
//......此处省略代码
if ([className hasSuffix:self.portSuffix]) {
className = [className substringToIndex:className.length - self.portSuffix.length];
}
// 通过协议名字来查找对应class
Class aClass = NSClassFromString(className);
//......此处省略代码
// 没有找到对应的module,是否设置了 defaultModule 来处理相关事件
if (!aClass && self.defaultModule) {
aClass = NSClassFromString(self.defaultModule);
}
// 生成 module 实例
module = [[aClass alloc] init];
if ([module conformsToProtocol:protocol]){
self.modulesDictionary[NSStringFromProtocol(protocol)] = module;
return module;
}
// 未找到实现该协议的组件
return nil;
}
3,YPAppModuleConfigManager 是各个业务组件的配置管理类,主要解决业务组件用于不同项目中又存在差异的问题
YPAppModuleConfigManager 通过加载json配置文件数据,然后 Module 通过 YPAppModuleConfigManager 获取自己模块的ModuleConfig
设计框架图如下:
实际使用:
// 可结合 YPLaunchManager 使用
- (void)runAppDidFinishLauchingBeforeUI {
// 加载 SystemConfig
NSString *systemConfigPath = [[NSBundle mainBundle] pathForResource:@"SystemConfig" ofType:@"json"];
[YPConfigManager loadModuleConfigWithPath:ModuleConfigPath];
// 加载 Module 的config
NSString *ModuleConfigPath = [[NSBundle mainBundle] pathForResource:@"ModuleAConfig" ofType:@"json"];
[YPConfigManager loadModuleConfigWithPath:systemConfigPath];
}
------ moduleA 获取配置
YPModuleA_ModuleConfig *configModel = [[YPAppModuleConfigManager sharedManager] parseModuleConfigClass:[YPModuleA_ModuleConfig class]];
// 设置相应的颜色和字体 等等
self.navigation.color = configModel.navigationBarTintColor
self.navigation.titleFont = configModel.navigationTitleFont
json 文件示例:
{
"YPBaseControllerModuleConfig" : {
"navigationBarTintColor":"#ffffff", // navigation 颜色
"navigationTitleFont":"Medium 18.0f", // navigation 字体
"backNavigationItemImageName" : "yp_navigation_back", // navigation 返回图片
},
}
最后附项目Demo地址
参考资料:iOS组件化方案对比