一、架构模式的选择
1. MVC模式
MVC(Model View Controller)是一种经典的软件架构模式,核心目标是通过职责分离提升代码的可维护性和可扩展性。
1.1 MVC组成部分
-
模型(Model):定义数据结构、数据加工与存储。
-
视图(View):视图内容展示、Touch事件交互。
-
控制器(Controller):处理业务逻辑、页面生命周期管理。
1.2 MVC通信特点
-
Model 和 View 两者不直接通信,通过 Controller 实现信息传递。
-
Model 通过 Notification 和 KVO 机制通知给 Controller。
-
View 通过 Target/Action、delegate 和 dataSource 三种机制与 Controller 进行通信。
1.3 MVC优缺点
-
易用性:与其他几种模式相比最小的代码量,维护起来也较为容易。
-
均衡性:厚重的 ViewController、无处安放的网络逻辑与数据逻辑。
2. MVCS模式
MVCS(Model View Controller Store)是基于 MVC 衍生出来的一套架构模式,从概念上来说,它拆分的部分是 Model 部分,拆出来一个 Store,由 Store 负责数据的加工与存储。
-
模型(Model):定义数据结构。
-
视图(View):视图内容展示、Touch事件交互。
-
控制器(Controller):处理业务逻辑、页面生命周期管理。
-
存储器(Store):数据加工与存储、网络接口请求。
从实际操作的角度上讲,它拆开的是 Controller,把 Controller 负责数据加工与存储的那部分逻辑抽离出来,交由 Store 去负责。
3. MVVM模式
MVVM(Model View ViewModel)是将 MVC 其中的 View 的状态和行为抽象化,让我们将视图展示和业务逻辑分开,ViewModel 在实现了 Model 数据加工的同时,帮忙处理 View 中由于需要展示内容而涉及的业务逻辑。
3.1 MVVM组成部分
-
Model:Model 是指代表内容的数据访问层,包含了本地数据以及网络数据。
-
View:View 包含 MVC 中的 View 和 Controller 部分,View主要是视图展示,Controller 则是处理页面生命周期以及页面跳转。
-
ViewModel:ViewModel 是连接 View 和 Model 的中间层,负责 Model 的数据存储与逻辑处理,以及 View 的状态管理与事件交互。
3.2 MVVM通信特点
-
在 Controller 中初始化 View 与 ViewModel,View 持有 ViewModel。
-
在 View 中 通过 KVO/RAC 实现 View-Model 的单/双向绑定。
-
ViewModel 不依赖 View,通过数据绑定或回调通知 View 更新。
MVVM 的核心是解耦视图逻辑与业务逻辑,View-Model 的单/双向绑定不是 MVVM 的必要条件,View-Model 的单/双向绑定主要是简化了ViewModel 与 View 之间的数据交互。
在没有使用单/双向绑定的情况下,需避免其退化为 MVP 模式,Presenter 持有 View 的弱引用,可直接调用 View 的接口。
3.3 MVVM优点
-
低耦合性:View 不直接与 Model 通讯,通过 ViewModel 来实现。
-
可重用性:内容/交互相同的 View 可以使用同一个 ViewModel。
-
可测试性:ViewModel 不依赖 UIKit,便于单元测试。
MVVM 可以结合 ReactiveCocoa,更加优雅的实现 View 与 Model 的双向绑定、View 与 ViewModel 之间的相互通讯,具体使用可以参照源码 - MVVM-RAC-DEMO。
二、工程结构与Pod应用
1. 壳工程的搭建
工程项目一般由业务模块与基础模块构成。独立的业务模块可以通过 ModuleManager 实现模块相关的配置,包含SDK初始化、接口服务注册、页面路由注册等,业务模块之间可以通过 ServiceProtocol / PageRoute / Notification 实现接口通讯与页面交互;基础模块则是为业务模块提供相关的基础能力。壳工程目录结构设计,如下图:
BaseKit-基础层,存放着通用的基础库,应用于上层业务代码。
-
CommonLib:通用功能组件,具体功能的实现和应用,如分享、数据上报、支付等。
-
Views:通用UI组件,如 BannerView、AlertView、pageControl、loadingView 等。
-
Kernal:基础服务组件,提供基础服务能力,如 Network、Log、dataBase、webImage 等。
-
Basic:基类封装,又分为 MVC+VM 四个子目录(减少 Basic 的依赖可以通过类别扩展的方式代替)。
-
Marco:通用声明,宏定义 - define、常量声明 - extern。
-
Utils:工具类集合,如 Authoritys、Encrypt、NetworkAccessible 等 。
-
Categorys:类别聚合,可按照类属性的不同分为不同的子目录,如 View、Datas、Image 等。
2. 业务/模块组件化
业务组件主要是指作为一个大的业务模块,单独分离成一个组件的形式,如电商模块、聊天模块、博客等。业务模块之间不存在耦合代码,由组件通讯中间层实现彼此的通讯。
2.1 组件管理方式

-
目录分隔:在工程目录下,按照不同的业务组件创建不同的目录,业务模块独立,组件之间不直接调用API。
-
xcworkspace:通过 xcworkspace 的方式,不同的业务模块创建各自的 project 工程,业务 project 工程 run 成功后,将 framework 引入主工程中。
-
podspecs:以 pod-specs 的方式引入业务组件,各个业务模块都独立成一个工程,具体可以参考源码 - iOS-Component-Pro。
2.2 三种管理方式的特点
-
目录分隔:目录分层比较简单,没有实现真正的代码分割,所以目录与目录直接的类是可以引用的,这样就很依赖于团队开发的规范性。
-
xcworkspace:xcworkspace 分层实现了不同业务组件代码的分割,作为 framework 的形式引入。动态编译的形式引入会影响到编译打包的效率;打包成静态 framework 的形式引入更新迭代成本比较高,而且不利于 cocoaPod 的使用。
-
podspecs:实现了不同业务组件代码的分割,又解决了 xcworkspace 分层影响编译效率与不利于 cocoaPod 使用的问题;优点 - 方便组件的版本管理与业务模块的独立测试,缺点 - 细微的代码修改也需要先升级组件再引入工程。
3. 组件分层与调用
调用链自上而下,上层组件通过通讯中间件进行解耦通讯(业务组件、功能组件),底层组件通过直接 #import 的方式调用(UI 组件、基础组件、工具/扩展)。
4. cocoaPod应用
cocoaPod 应用 iOS 开发者应该都比较熟悉,主要是用关联第三方库与私有组件库,方便版本迭代管理。
4.1 GitHub第三方库
CocoaPods 详解之-使用篇:CocoaPods详解之使用篇
4.2 GitLab私有库
CocoaPod-spec 私有库配置:cocoapod-podspec私有库配置
三、组件通讯 & 页面路由
1. 组件通讯
添加中间件实现组件解耦,避免组件之间循环依赖。
1.1 URL-Block(MGJRouter)
/*********** 业务组件中注册 ***********/
[[MKRouter sharedInstance] registerHandler:^(MKRouteRequest *request) {
// 跳转至商祥页
// request.callBack(nil, @{@"productId" : request[@"productId"]});
} forRoute:MKString(@".*product/detail.*\\?(.*)\\&(.*)$")];
/*********** 模块调用 ***********/
[[MKRouter sharedInstance] handleURL:[NSURL URLWithString:@"weixin://com.apple.iphone/product/detail?productId=ID985632"] params:nil targetCallBack:^(NSError *error, NSDictionary *responseObject) {
}];
-
数据埋点:"appData/click?params="
-
原生商详页:"product/detail?params="
-
web商详页:"web/page?params="
1.2 Target-Action(CTMediator)
/*********** 公共实现 ***********/
#import "MKMediator+Feature.h"
static NSString * const kTargeFeature = @"Feature";
static NSString * const kActionViewControllerForFeature = @"viewControllerForFeature";
@implementation MKMediator (Feature)
- (UIViewController *)mediator_ViewControllerForFeature:(MKFeatureActionModel *)params {
return [self call:kTargeFeature action:kActionViewControllerForFeature parameters:params];
}
@end
/*********** 业务组件中实现 ***********/
#import "MKMediator+Feature.h"
#import "MKTarget_Feature.h"
#import "AudioVideoPraticeVC.h"
@implementation MKTarget_Feature
- (UIViewController *)action_viewControllerForFeature:(MKActionModel *)params {
AudioVideoPraticeVC* audioVC = [[AudioVideoPraticeVC alloc] init];
return audioVC;
}
@end
/*********** 模块调用 ***********/
MKActionModel* acitonModel = [[MKActionModel alloc] init];
actionModel.keyValues = @{};
actionModel.complete = ^(id result) {
};
UIViewController* audioVC = [[MKMediator shared] mediator_ViewControllerForFeature:acitonModel];
-
外部打开原生页:"appScheme://nativePage?pageId=&args="(通过 pageId 映射到对应组件的 API)
-
添加数据埋点:调用数据埋点组件的 API
-
打开Hybrid容器页:通过 url 正则匹配到页面的相关配置,再调用容器组件的 API 实现页面的跳转和渲染
1.3 Protocol-Class(BeeHive)
面向接口编程,通过 @protocol 定义协议接口与属性(注意:Protocol 的属性只是一个声明,并没有实际用途,需要实现协议的类本身定义了该属性)。
/* 定义协议方法与属性 */
@protocol MKOpenURLProtocol <NSObject>
@property (nonatomic, assign) BOOL isPresent;
- (BOOL)openURLWithURLString:(NSString *)URLString params:(NSDictionary *)params;
@end
MKOpenURLExecutor 声明了 MKOpenURLProtocol,并实现其协议方法。
/* 执行者-MKOpenURLExecutor.h */
@interface MKOpenURLExecutor : NSObject <MKOpenURLProtocol>
@end
/* 实现者-MKOpenURLExecutor.m */
@implementation MKOpenURLExecutor
@synthesize isPresent;
- (BOOL)openURLWithURLString:(NSString *)URLString params:(NSDictionary *)params {
// do something
return YES;
}
@end
上面讲的这种声明协议再到类的实现,虽然是应用了接口编程的方式,但是业务组件之间还是会有直接调用的关系,所以需要有个中间件实现 protocol 与 service 的一一匹配。
/* 中间件-MKServiceManager.h */
@interface MKServiceManager : NSObject
+ (instancetype)sharedInstance;
- (void)registerService:(id)service protocol:(Protocol *)protocol;
- (id)serviceWithProtocol:(Protocol *)protocol;
@end
/* 中间件-MKServiceManager.m */
@interface MKServiceManager ()
@property (nonatomic, strong) NSMutableDictionary* serviceSet;
@end
@implementation MKServiceManager
+ (instancetype)sharedInstance {
static MKProtocolManager* instance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [[self alloc] init];
instance.serviceSet = [NSMutableDictionary dictionary];
});
return instance;
}
- (void)registerService:(Class)service protocol:(Protocol *)protocol {
if (service && protocol) {
[_serviceSet setObject:service forKey:NSStringFromProtocol(protocol)];
}
}
- (Class)serviceWithProtocol:(Protocol *)protocol {
if (protocol) {
return [_serviceSet objectForKey:NSStringFromProtocol(protocol)];
}
return nil;
}
@end
注册组件 A 的接口服务,在组件 B 中获取组件 A 的接口服务,再执行相应的能力。(通过 APP 构造函数 / load 方法实现协议的动态注册,省去了手动注册这一步。具体实现可以参考 BeeHive 源码,或者博客:iOS模块启动项自注册实现)
/* 业务组件A的协议与接口对象的一一匹配 */
+ (void)registerProtocol {
[[MKServiceManager sharedInstance] registerService:[MKOpenURLExecutor class] protocol:@protocol(MKOpenURLProtocol)];
}
/* 其他业务组件中调用业务组件A的协议接口 */
+ (void)openURL:(NSString *)URLString params:(id)params {
id <MKOpenURLProtocol> service = [[MKServiceManager sharedInstance] serviceWithProtocol:@protocol(MKOpenURLProtocol)];
[service openURLWithURLString:URLString params:params];
}
-
外部打开原生页:"appScheme://nativePage?pageId=&args="(通过 pageId 映射到对应组件的 API)
-
添加数据埋点:调用数据埋点组件的 API
-
打开hybrid容器页:通过 url 正则匹配到页面的相关配置,再调用容器组件的 API 实现页面的跳转和渲染
1.4 三种通讯方式的特点
-
URL-Block:能解决组件间的依赖,路由配置灵活,但需要去注册 / 维护路由表,方法调用不够直观,API 参数为硬编码,大量的正则匹配也会带来一定的性能开销。
-
Target-Action:统一了组件 API 服务,接口实现不依赖中间件,但需要额外维护中间件类扩展,方法调用不够直观,API 参数为硬编码。
-
Protocol-Class:面向接口编程,方法调用比较直观,但组件方需要声明一个对外的协议和用于协议实现的类,并进行一对一绑定。
1.5 通讯方式的选型建议
-
组件间的页面路由采用 URL-Block 实现,通过页面 URI(scheme://host/path?query)命中路由规则,保持原生与前端页面跳转协议的一致性。
-
组件间的方法调用采用 Protocol-Class 实现,调用链路相对于其他两种更加直观,与依赖倒置原则更为贴切。
2. 页面路由(推送 / scheme / 通用链接 / JSAPI-openURL)
2.1 页面路由配置
2.2 具体实现
通过 url 正则匹配到相关的路由配置,生成对应类型的 ViewController(URL Scheme 和 Universal Link 的页面链接需要 url 编码)。Native 页面可以通过 URL-Block 的方式生成;或者通过页面 key 映射 ViewController 类名,用 runtime 的方式生成。页面的通用参数用 UIViewController 类别添加。
-
native:通过页面配置信息与 url 参数,生成对应的原生页面,最终实现页面的跳转与渲染。
-
web:直接通过 url,生成 WebVC,实现页面跳转和 H5 资源加载。
-
weex:获取wx的资源链接及页面配置参数,传递给 Weex 容器实现资源加载和页面渲染。
-
react naive:获取 RN 的资源链接及页面配置参数,传递给 RN 容器实现资源加载和页面渲染。
-
flutter:配置相关的 routeName 及页面配置参数,传递给 Flutter 容器实现flutter端页面的渲染。
2.3 页面协议
1)http链接的形式(url匹配路由配置)
"https://domain/nt/home_page" // native页
"https://domain/wb/order_detail" // web页
"https://domain/wx/star_info" // weex页
"https://domain/rn/order_list" // RN页
"https://domain/ft/mine_detail" // flutter页
2)appscheme的形式
// 原生页(命中本地路由 | 获取home_page路由配置)
"appscheme://native/page/home_page"
// web页(获取order_detail的路由配置 | url跳转)
"appscheme://web/page/order_detail"
"appscheme://web/page?url=https%3A%2F%2Fm.domain.com%2Forder_detail.html"
"appscheme://weex/page/star_info" // weex页(获取star_info路由配置)
"appscheme://rn/page/order_list" // RN页(获取order_list路由配置)
"appscheme://ft/page/mine_detail" // flutter页(获取mine_detail路由配置)
备注:在云端页面路由配置较多的情况下,频繁正则匹配会有一定的性能开销,native 与 web 页可以不采用云端下发的路由匹配机制,通过已注册的路由实现页面的生成与跳转(native 由 url-path 命中规则)。