一、架構模式的選擇
1. MVC模式
MVC全名是Model View Controller,是模型(model)-視圖(view)-控制器(controller)的縮寫,一種軟體設計典範,用一種業務邏輯、資料、界面顯示分離的方法組織代碼,将業務邏輯聚集到一個部件裡面,在改進和個性化定制界面及使用者互動的同時,不需要重新編寫業務邏輯。MVC被獨特的發展起來用于映射傳統的輸入、處理和輸出功能在一個邏輯的圖形化使用者界面的結構中。
1.1 MVC 程式設計模式
MVC 是一種使用 MVC(Model View Controller 模型-視圖-控制器)設計應用程式的模式
- 視圖(View):負責資料展示、監聽使用者觸摸等工作
- 控制器(Controller):負責業務邏輯、事件響應、資料加工等工作
- 模型(Controller):負責封裝資料、存儲和處理資料運算等工作
1.2 MVC通信特點
![](https://img.laitimes.com/img/_0nNw4CM6IyYiwiM6ICdiwiIn5GcukTN3ATOwcTM1ETNwEjMwIzLc52YucWbp5GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.png)
1. Model和View永遠不能互相通信,隻能通過Controller傳遞。
2. Controller可以直接與Model通信(讀寫調用Model),Model通過Notification和KVO機制與Controller間接通信。
3. Controller與View通過Target/Action,delegate和datasource三種模式進行通信。通過這三種模式,View就可以向Controller通信,Action/Target 模式來讓Controller 監聽View 觸發的事件。View又通過Data source和delegate進行資料擷取和某些通信操作。
1.3 MVC優缺點
1.易用性:與其他幾種模式相比最小的代碼量,維護起來也較為容易。
2. 可測性:由于糟糕的分散性,隻能對Model進行測試。
3. 均衡性:厚重的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模式的組成部分
- 模型:模型是指代表真實狀态内容的領域模型(面向對象),或指代表内容的資料通路層(以資料為中心)。
- 視圖:視圖是使用者在螢幕上看到的結構、布局和外觀(UI)。
- 視圖模型:視圖模型是暴露公共屬性和指令的視圖的抽象。在視圖模型中,綁定器在視圖和資料綁定器之間進行通信。
引用特點:
View <=> Controller <=> ViewModel <=> Model。
在controller中引用view與viewmodel;實作view與model的雙向綁定
;
viewModel
負責
model的資料存儲與邏輯處理
。
3.2 MVVM優點
- 低耦合:View可以獨立于Model變化和修改,一個ViewModel可以綁定到不同的View上。
- 可重用性:你可以把一些視圖邏輯放在一個ViewModel裡面,讓很多view重用這段視圖邏輯。
- 獨立開發:開發人員可以專注于業務邏輯和資料的開發(ViewModel),設計人員可以專注于頁面設計。
- 可測試:界面素來是比較難于測試的,而現在測試可以針對ViewModel來寫。
備注:MVVM可以結合ReactiveCocoa,更加優雅的實作view與viewmodel的雙向綁定,具體使用可以參照部落格iOS MVVM+RAC 從架構到實戰(MVVM-RAC-DEMO)
二、通用基礎庫與cocoaPod應用
1. 單工程項目
單工程項目的業務代碼統一放在一個project工程,沒有實作業務元件化,适用于混合開發架構(原生業務較少),或者APP團隊規模較小且業務較為單一的情況。業務元件化既需要快速的內建,也可以作為一個單獨的工程跑起來,下文會簡單講一下業務元件相關内容。
下圖是本人在以往單工程項目的架構設計,僅供參考,實際應用以具體項目為主。
細說IOS工程架構一、架構模式的選擇二、通用基礎庫與cocoaPod應用三、業務元件&元件通訊
2. 通用基礎庫的目錄分布
【Service】業務服務層,存放公共業務邏輯與通用服務,如公共接口、基礎元件配置、使用者資訊、共享資料、頁面路由配置等。
【BaseKit】基礎層,存放着通用的基礎庫,應用于各個業務子產品。
CommonLib:通用功能元件,具體功能的實作和應用,如APM、資料上報、頁面路由、OCR等。
Views:通用視圖元件,如BannerView、AlertView、pageControl、loadingView等。
Kernal:基礎服務元件,提供基礎服務能力,如Network、Log、dataBase、webImage等。
Basic:基礎類,又分為MVC三個子目錄,封裝存放Basic類。
Marco:通用聲明,宏定義-define、常量聲明-extern。
Utils:工具類集合,如Tools、Authoritys、Encrypt、NetworkAccessible等 。
Categorys:公共類别擴充,可按照類屬性的不同分為不同的子目錄,如View、Datas、Image等。
備注:自上而下的依賴關系
3. cocoaPod應用
cocoaPod應用IOS開發者應該都比較熟悉,主要是用關聯第三方庫與私有元件庫,友善版本疊代管理。
3.1 GitHub第三方庫
CocoaPods詳解之-使用篇:CocoaPods詳解之----使用篇_SpeedBoy007的專欄-CSDN部落格
3.2 GitLab私有庫
CocoaPod-spec私有庫配置:cocoapod-podspec私有庫配置_z119901214的部落格-CSDN部落格
三、業務元件&元件通訊
1. 業務元件
業務元件主要是指作為一個大的業務子產品,單獨分離成一個元件的形式,如電商子產品、聊天子產品、部落格等。業務元件之間不存在耦合代碼,由元件通訊中間層實作彼此的通訊。
1.1 組價分層方式
通過xcworkspace的方式分層,A為主工程,B、C為兩個業務子產品
- 目錄分層:在主業務目錄下,按照不同的業務元件建立不同的目錄,業務子產品獨立,元件之間不直接調用API。
- xcworkspace分層:通過xcworkspace的方式,不同的業務子產品建立各自的project工程,業務project工程run成功後,将framework引入主工程中。
- pod元件分層:以pod-specs的方式引入業務元件,各個業務子產品都獨立成一個工程,具體可以參考github元件化項目-iOS-Component-Pro
1.2 元件分層方式的特點
- 目錄分層:目錄分層比較簡單,沒有實作真正的代碼分割,是以目錄與目錄直接的類是可以引用的,這樣就很依賴于團隊開發的規範性。
- xcworkspace分層:xcworkspace分層實作了不同業務元件代碼的分割,作為framework的形式引入。動态編譯的形式引入會影響到編譯打包的效率;打包成靜态framework的形式引入更新疊代成本比較高,而且不利于cocoaPod的使用。
- pod元件分層:實作了不同業務元件代碼的分割,又解決了xcworkspace分層影響編譯效率與不利于cocoaPod使用的問題,業務元件與基礎元件統一使用cocoaPod管理。
2. 業務元件之間的通訊
添加中間件實作元件解耦,避免元件之間循環依賴。
2.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='jsonString的URL編碼'"
原生商詳頁: "product/detail?param='jsonString的URL編碼'"
weex商詳頁: "weex/page?params='jsonString的URL編碼'"
web商詳頁: "web/page?params='jsonString的URL編碼'"
flutter商詳頁: "flutter/page?params='jsonString的URL編碼'"
2.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
外部->原生錄音頁: "urlScheme://appPage?pageId=audioPageId¶ms="、"urlScheme://appRouter?url=pageUrl"(通過url正則比對到頁面的相關配置,頁面key與映射表比對出對應的原生頁)
打開hybrid容器頁:通過url正則比對到頁面的相關配置,再調用業務容器元件的公共API實作頁面的跳轉和渲染
2.3 Protocol-Class
面向接口程式設計,通過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>
@property (nonatomic, assign) BOOL isPresent;
@end
/* 實作者-MKOpenURLExecutor.m */
@implementation MKOpenURLExecutor
- (BOOL)openURLWithURLString:(NSString *)URLString params:(NSDictionary *)params {
// do something
return YES;
}
@end
上面講的這種聲明協定再到類的實作,雖然是應用了接口程式設計的方式,但是業務元件之間還是會有直接調用的關系,是以需要有個中間者實作protocol與接口對象的一一比對。
/* 中間者-MKProtocolManager.h */
@interface MKProtocolManager : NSObject
+ (instancetype)sharedInstance;
- (void)setClass:(Class)protocolClass protocol:(Protocol *)protocol;
- (Class)classWithProtocol:(Protocol *)protocol;
@end
/* 中間者-MKProtocolManager.m */
@interface MKProtocolManager ()
@property (nonatomic, strong) NSMutableDictionary* protocolSet;
@end
@implementation MKProtocolManager
+ (instancetype)sharedInstance {
static MKProtocolManager* instance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [[self alloc] init];
instance.protocolSet = [NSMutableDictionary dictionary];
});
return instance;
}
- (void)setClass:(Class)protocolClass protocol:(Protocol *)protocol {
if (protocolClass && protocol) {
[_protocolSet setObject:protocolClass forKey:NSStringFromProtocol(protocol)];
}
}
- (Class)classWithProtocol:(Protocol *)protocol {
if (protocol) {
return [_protocolSet objectForKey:NSStringFromProtocol(protocol)];
}
return nil;
}
@end
業務元件A的協定與接口對象的綁定,以及其他業務元件中調用業務元件A的協定接口。
/* 業務元件A的協定與接口對象的一一比對 */
+ (void)registerProtocol {
[[MKProtocolManager sharedInstance] setClass:[MKOpenURLExecutor class] protocol:@protocol(MKOpenURLProtocol)];
}
/* 其他業務元件中調用業務元件A的協定接口 */
+ (void)openURL:(NSString *)URLString params:(id)params {
Class protocolClass = [[MKProtocolManager sharedInstance] classWithProtocol:@protocol(MKOpenURLProtocol)];
id <MKOpenURLProtocol> executor = [[protocolClass alloc] init];
executor.isPresent = NO; // 内部屬性配置
[executor openURLWithURLString:URLString params:params];
}
原生->原生錄音頁:直接調用對應業務元件的公共API
外部->原生錄音頁: "urlScheme://appPage?pageId=audioPageId¶ms="、"urlScheme://pageRouter?url=pageUrl"(通過url正則比對到頁面的相關配置,頁面key與映射表比對出對應的原生頁)
打開hybrid容器頁:通過url正則比對到頁面的相關配置,再調用業務容器元件的公共API實作頁面的跳轉和渲染
2.4 三種通訊方式的特點
- URL-Block:能解決元件間的依賴,路由配置靈活,但路由注冊與中間件存在耦合,且需要去注冊&維護路由表,方法調用不夠直覺。
- Target-Action:統一了元件api服務,接口實作不依賴中間件,但需要額外維護中間件類擴充,方法調用不夠直覺。
- Protocol-Class:面向接口程式設計,方法調用比較直覺,但協定注冊與中間件存在耦合,且每個業務元件都需要聲明一個對外的協定,和一個用于接口實作的類。
3. 頁面路由規則配置(推送、scheme、通用連結、JSAPI-openURL)
1)頁面路由配置
2)具體實作
通過url正則比對到相關的路由配置,生成對應類型的ViewController(scheme和universal link的頁面連結需要url編碼)。Native頁面可以通過MGJRouter的方式生成;或者通過頁面key映射VC類名,用runtime的方式生成。頁面的通用參數用UIViewController類别添加。
native:通過頁面配置資訊與url參數,生成對應的原生頁面,最終實作頁面的跳轉與渲染。
web:直接通過url,生成WebVC,實作頁面跳轉和H5資源加載。
weex:擷取wx的資源連結及頁面配置參數,傳遞給wx容器實作資源加載和頁面渲染。
react naive:擷取rn的資源連結及頁面配置參數,傳遞給rn容器實作資源加載和頁面渲染。
flutter:配置相關的routeName及頁面配置參數,傳遞給Flutter容器實作flutter端頁面的渲染。
3)場景應用
scheme喚起:urlScheme://pageRouter?url=https%3a%2f%2fm.domain.com%2forder_detail.html
universal link喚起:https://host/path?url=https%3a%2f%2fm.domain.com%2forder_detail.html
内部打開(比對頁面路由):https://m.domain.com/order_detail.html
備注:在雲端頁面路由配置較多的情況下,頻繁正則比對會有一定的性能開銷,native與web頁可以不采用雲端下發的路由比對機制,直接通過本地規則實作(以url字首是否為http來區分)。