一、框架模式的选择
1. MVC模式
MVC全名是Model View Controller,是模型(model)-视图(view)-控制器(controller)的缩写,一种软件设计典范,用一种业务逻辑、数据、界面显示分离的方法组织代码,将业务逻辑聚集到一个部件里面,在改进和个性化定制界面及用户交互的同时,不需要重新编写业务逻辑。MVC被独特的发展起来用于映射传统的输入、处理和输出功能在一个逻辑的图形化用户界面的结构中。
1.1 MVC 编程模式
MVC 是一种使用 MVC(Model View Controller 模型-视图-控制器)设计应用程序的模式
-
视图(View):负责数据展示、监听用户触摸。
-
控制器(Controller):负责业务逻辑、事件响应、数据加工。
-
模型(Controller):负责封装数据、存储和处理数据运算。
1.2 MVC通信特点
-
Model和View永远不能相互通信,只能通过Controller传递。
-
Controller可以直接与Model通信(读写调用Model),Model通过Notification和KVO机制与Controller间接通信。
-
Controller与View通过Target/Action,delegate和datasource三种模式进行通信。通过这三种模式,View就可以向Controller通信,Action/Target 模式来让Controller 监听View 触发的事件。View又通过Data source和delegate进行数据获取和某些通信操作。
1.3 MVC优缺点
-
易用性:与其他几种模式相比最小的代码量,维护起来也较为容易。
-
均衡性:厚重的ViewController、无处安放的网络逻辑与数据逻辑。
2. MVCS模式
苹果自身就采用的是这种架构思路,从名字也能看出,也是基于MVC衍生出来的一套架构。从概念上来说,它拆分的部分是Model部分,拆出来一个Store。这个Store专门负责数据存取。
从实际操作的角度上讲,它拆开的是Controller。因为Controller做了数据存储的事情,就会变得非常庞大,那么就把Controller专门负责存取数据的那部分抽离出来,交给另一个对象去做,这个对象就是Store。这么调整之后,整个结构也就变成了真正意义上的MVCS。
-
视图(View):用户界面
-
控制器(Controller):业务逻辑及处理
-
模型(Model):数据存储
-
存储器(Store):数据处理逻辑
MVCS是基于瘦Model的一种架构思路,把原本Model要做的很多事情中的其中一部分关于数据存储的代码抽象成了Store,在一定程度上降低了Controller的压力。
3.MVVM模式
MVVM是Model-View-ViewModel的简写。它本质上就是MVC 的改进版。MVVM 就是将其中的View 的状态和行为抽象化,让我们将视图 UI 和业务逻辑分开。当然这些事 ViewModel 已经帮我们做了,它可以取出 Model 的数据同时帮忙处理 View 中由于需要展示内容而涉及的业务逻辑。
3.1 MVVM模式的组成部分
-
Model:Model是指代表内容的数据访问层,包含了本地数据以及网络数据。
-
View:View包含MVC中的View和Controller部分,View主要是视图展示,Controller则是处理页面生命周期以及页面跳转。
-
ViewModel:ViewModel是View和Model的桥接着,即管理着Model的数据逻辑,又要支持与View之间的相互通讯。
引用特点:在Controller中关联View与ViewModel(View引用ViewModel);在View中实现View与Model的单/双向绑定(KVO/RAC);ViewModel 负责Model的数据存储与逻辑处理,以及与View的交互通讯(Update/Action)。
备注:MVVM不依赖于View-Model的单/双向绑定,View-Model的单/双向绑定主要是简化了ViewModel与View之间的数据交互。
3.2 MVVM优点
-
低耦合:View不直接与Model通讯,通过ViewModel来实现。
-
可重用性:视图/数据逻辑相同的View可以共用一个ViewModel来实现。
结语:MVVM可以结合ReactiveCocoa,更加优雅的实现View与Model的双向绑定、View与ViewModel之间的相互通讯,具体使用可以参照博客iOS MVVM+RAC 从框架到实战(MVVM-RAC-DEMO)
二、工程结构与Pod应用
1.壳工程的搭建
工程项目一般由业务模块与基础模块构成,添加服务层主要为了实现基础组件配置与方法包装,以及提供具有业务特性的工具方法、宏定义/静态声明等,通过服务层来聚合应用的基础能力,以及降低业务模块之间的耦合。如果要实现业务模块的完全解耦,则需要通过业务模块组件化来实现。
壳工程目录结构设计,如下图:
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的方式引入业务组件,各个业务模块都独立成一个工程,具体可以参考github组件化项目-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];
-
内部->数据埋点:直接调用对应组件的API
-
外部->原生录音页:"appScheme://nativePage?pageId=&args="(通过pageId映射到对应组件的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];
}
-
内部->数据埋点:直接调用对应组件的API
-
外部->原生录音页:"appScheme://nativePage?pageId=&args="(通过pageId映射到对应组件的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的资源链接及页面配置参数,传递给wx容器实现资源加载和页面渲染。
-
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命中规则)。