为什么要使用组件化???
什么是组件化?
在参考了其他方案之后,我对组件化这一概念的理解上存在一些认识偏差。实际上,在iOS开发中存在一种常见的误解:即认为所有进行界面交互的逻辑都应该被视为"组件"。这种描述在Flex开发中虽然有一定的适用性,在Web开发领域同样值得商榷。结合最近学习和应用Vue进行Web开发的经验后,我发现这种理解并不完全准确。那么如何区分核心业务模块化与通用功能组件化的本质区别呢?这个问题的回答往往能带来清晰的认识:核心业务模块化与通用功能组件化是两个互不相交的概念。举个例子来说,在电商App中,产品详情页、列表页、购物车及搜索页面通常具有较高的VC频率(Value Creation, 创作价值);而这些页面之间频繁的交互关系往往导致高度耦合性的问题出现。

类似这种在商品详情页中外部调入的数据仅需提供一个 productID 就可实现,并且对于高度依赖自身业务功能的模块,则可以直接将其作为一个整体进行维护。后续对内部活动展示、业务增删等功能的具体调整也无需影响到其他部分代码。
就如之前所述,在IM类应用中使用的聊天键盘以及集成支付宝和微信等支付功能的小插件都能够在一个系统内自由共享,并且甚至可以开源到社区供更多开发者使用。用组件来形容更为贴切,在Flex、Vue、angular等前端开发框架中这一现象尤为明显
为什么要有组件化(模块化)
在公司业务发展中逐渐扩大了规模的客户端系统中集成了很多业务逻辑代码块,在不同功能模块之间实现了互相调用与嵌套的方式以提升系统的运行效率。每当某个功能模块需要升级时,在修改相关代码时会带来连锁反应的影响;尤其是在系统设计初期未充分考虑接口封装的情况下,在未来维护工作量将变得非常庞大;因此为了提高系统的可维护性必须建立一套符合要求的组件间通信机制;通过模块化设计可以最大限度地将代码的功能逻辑集中在一起对外只提供清晰明确的接口实现并确保各组件间通过弱耦合的方式进行数据交互。
我的模块化架构思路
如何优化模块化之间的通信
实现封装模块的工作需要具备面向对象编程的基本概念。具体操作时较为简单易行,请确保编写好各组件之间的调用接口即可完成任务。这里不再详细展开说明。其中最关键的是各模块之间的通信机制。例如,在一个商品搜索列表页面中,需要同时支持查看购物车状态以及浏览商品详情信息;此外,在购物车内的商品列表中点击任意一件商品会跳转至对应的详情页。其他相关页面之间也会频繁地进行数据交互与通信。
#import "ProductDetailViewController.h"
#import "CartViewController.h"
@implementation ProductListViewController
- (void)gotoDetail {
ProductDetailViewController *detailVC = [[ProductDetailViewController alloc] initWithProId:self.proId];
[self.navigationController pushViewController:detailVC animated:YES];
}
- (void)gotoCart {
CartViewController *cartVC = [[CartViewController alloc] init];
[self.navigationController pushViewController:cartVC animated:YES];
}
@end
熟悉这一类代码的人都知道它们的基本操作流程是什么样的。
通常都是按照这个方式来处理数据传输的问题。
但这种做法本身并没有什么明显的缺陷。
然而随着项目的不断发展规模越来越大问题也随之显现。
在项目中存在模块之间互相调用的情况
会产生相互依赖的关系。
每当进行一次跳转操作时都需要导入相应的控制器类以确保功能正常运行。
值得注意的是在某些细节上进行微调时
例如在商品详情页功能中可能需要增加一个特定的参数输入项
这时候就需要逐一排查并修改相关的接口连接处
这种逐个优化的方式虽然能够解决问题
但却显得效率低下不够理想。
运用中间件
于是想到了一个相对简单的方法。通过引入一个中间层Router来实现,在该Router中预先定义好处理不同请求的逻辑。首先,在需要处理的地方调用该Router函数,并传递相应的请求参数。例如这样的实现方式:

// Router.m
#import "ProductDetailViewController.h"
#import "CartViewController.h"
@implementation Router
+ (UIViewController *)getDetailWithParam:(NSString *)param {
ProductDetailViewController *detailVC = [[ProductDetailViewController alloc] initWithProId:self.proId];
return detailVC;
}
+ (UIVIewController *)getCart {
CartViewController *cartVC = [[CartViewController alloc]init];
return cartVC;
}
@end
其他界面中这样使用:
#import "Router.m"
UIViewController *detailVC = [[Router instance] jumpToDetailWithParam:param];
[self.navigationController pushViewController:detailVC];
运行 runtime
然而这种做法也存在缺陷。每一个 vc 都会依赖 Router 而 Router 内部会依赖所有这些 VC。Name 如何解除这层循环引用呢?OC 中有一个机制可用:使用 runtime 来解决这个问题。
- (UIViewController *)getViewController:(NSString *)stringVCName {
Class class = NSClassFromString(stringVCName);
UIViewController *controller = [[class alloc]init];
if (controller == nil) {
NSLog("未找到此类:%@",stringVCName);
controller = [[RouterError sharedInstance] getErrorController];
}
return controller;
}
这样上面的图就是这样的:

这样,在 Router 内部就无需导入任何 VC。整个代码量仅限于几十行即可完成功能实现,并且显得非常简洁。此外,在无法找到对应项时会进行异常处理并返回一个预先定义的错误页面。这是否类似于 Web 开发中常见的 404 页面呢?
UIViewController *controller = [[Router shaedInstance] getViewController:@"ProductDetailViewController"];
[self.navigationController pushViewController:controller];
如何传参数
相信很多人都已经意识到,在这种情况下如何传递必要的参数是一个关键问题。
例如,在商品详情页中至少需要传递一个productID字段。
不必着急,请考虑我们可以通过对上一种方法稍作修改来实现这一目标——通过引入一个字典来完成参数的传递。
- (UIViewController *)getViewController:(NSString *)stringVCName {
Class class = NSClassFromString(stringVCName);
UIViewController *controller = [[class alloc]init];
return controller;
}
- (UIViewController *)getViewController:(NSString *)stringVCName witParam:(NSDictionary *)paramdic {
UIViewController *controller = [self getViewController:stringVCName];
if (controller != nil) {
controller = [self controller:controller withParam:paramdict andVCname:stringVCName];
} else {
NSLog(@"未找到此类:%@",stringVCName);
// EXCEPTION Push 啊 Normal Error VC
controller = [[RouterError sharedInstance] getErrorController];
}
return controller;
}
/**
此方法用来初始化参数(控制器初始化方法默认为 initViewControllerParam。初始化方法可以自定义,前提是 VC 必须实现它。要想灵活一点,也可以添加一个参数 actionName,当做参数传入。不过这样你就需要修改此方法了)。
@param controller 获取到的实例 VC
@param paramdic 实例化参数
@param vcName 控制器名字
@return 初始化之后的VC
*/
- (UIViewController *)controller:(UIViewController *)controller withParam:(NSDictionary *)paradic andVCname:(NSString *)vcName {
SEL selector = NSSelectorFromString(@"initViewControllerParam:");
if(![controller respondsToSelector:selector]) { // 如果没定义初始化参数方法,直接返回,没必要在往下做设置参数的方法
NSLog(@"目标类:%@ 未定义:%@方法",controller,@"initViewControllerParam:");
return controller;
}
// 在初始化参数里面添加 key 信息,方便控制器中检验路由信息
if (paradic == nil) {
paramdic = [[NSMutableDictionary alloc] init];
[paradic setValue:vcName forKey:@"URLKEY"];
SuppressPerformSlelctorLeakWarning([controller performSelector:selector withObject:paramdic]);
} else {
[paramdic setValue:vcName forKey:@"URLKEY"];
}
SuppressPerformSelecorLeakWarning([controller performSelector:selector withObject:paramdic]);
return controller;
}
通常情况下,在业务控制器内部,默认会实现一个名为 init_controller_param 的方法。然后在 router 中使用 **selectable 标识符手动调用该方法,并传递参数 paramdic。* 如果希望更灵活一些,则可以在初始化该类的方法时将其作为 actionName 参数传递给 router。* 类似于这样:
- (UIViewController *)controller:(UIViewController *)controller withParam:(NSDictionary *)paramdic andVCName:(NSString *)vcName actionName:(NSString *)actionName {
SEL selector = NSSelectorFromString(actionName);
... 后面就是一样的代码了
}
到此为止基本上模块化就完成了。通过超过100行代码解决了各复杂业务模块间的通信问题以及实现了高度解耦。
总结
模块化的实现方法在iOS开发中相对容易实现。其主要原因在于OC是一门动态语言。对象类型是在运行时阶段被明确定义的,在调用方法时采用发送消息的方式完成。这使得程序具备了诸多操作上的灵活性。这种方法适用于大多数App,并能够有效满足大部分业务需求。
