1.前言
當項目越來越來龐大,參與編寫的人員越來多,代碼分支和接入產品越發複雜時,項目組件化成了不二選擇,什麼是項目組件化?筆者簡略概述爲,以pod庫的形式將複雜的系統業務拆分成不同模塊,進而隔離不同的業務功能,然後分發與不同人員負責開發和維護,降低系統代碼耦合度,方便管理。概括地不全或有誤,請大家指正。
組件化有什麼用呢?組件化最大的作用是隔離組件和功能。組件隔離是不同業務不會有直接的依賴,也就是說不會直接#import其他組件頭文件,在編譯時,各個組件是不耦合的,在進行開發調試測試各個時期時,可以單獨進行,而不需要依賴其他功能模塊,提高工作效率。
2.中間件的選取
要實現組件化,不得不考慮的是組件間的通信問題,如果單純pod第三方,然後在主工程中調用,其實是不需要中間件來轉發組件間的通信的,但是項目組件化過程中,這是一個不得不做的功能。要實現不同組件間的通信,就需要一個消息轉發的中間層,這個中間層就是中間件。
中間件的作用就是減少各個業務的相互依賴關係。各個組件的通信都交由中間件做消息轉發,解耦系統,增加項目工程的可管理性。
2.1 Router
利用router路由的方式實現組件間的通信。用的比較多的就是MGJRouter、HHRouter和FFRouter。路由router實現的方法大致是在提供服務的組件中先註冊block,然後在需要調用的方法組件中通過URL調用block,來達到消息轉發的目的。
MGJRouter是一個單例對象,在其內部維護着一個“URL -> Block”格式的註冊表,通過這個註冊表來保存服務方註冊的Block,以及使調用方可以通過URL映射出Block,並通過MGJRouter對服務方發起調用。MGJRouter是所有組件的調度中心,負責所有組件的調用、切換、特殊處理等操作,可以用來處理一切組件間發生的關係。除了原生頁面的解析外,還可以根據URL跳轉H5頁面。
- (void)demoFallback
{
[MGJRouter registerURLPattern:@"mgj://" toHandler:^(NSDictionary *routerParameters) {
[self appendLog:@"匹配到了 url,以下是相關信息"];
[self appendLog:[NSString stringWithFormat:@"routerParameters:%@", routerParameters]];
}];
[MGJRouter registerURLPattern:@"mgj://foo/bar/none/exists" toHandler:^(NSDictionary *routerParameters) {
[self appendLog:@"it should be triggered"];
}];
[MGJRouter openURL:@"mgj://foo/bar"];
}
通過OpenURL:
方法傳入的URL參數,對詳情頁已經註冊的Block方法發起調用。調用方式類似於Get請求,URL地址後面拼接參數。
[MGJRouter openURL:@"mgj://detail?id=404"];
也可以通過字典方式傳參,MGJRouter提供了帶有字典參數的方法,這樣就可以傳遞非字符串之外的其他類型參數。
[MGJRouter openURL:@"mgj://detail?" withParam:@{@"id" : @"404"}];
+ (void)load
{
DemoDetailViewController *detailViewController = [[DemoDetailViewController alloc] init];
[DemoListViewController registerWithTitle:@"基本使用" handler:^UIViewController *{
detailViewController.selectedSelector = @selector(demoBasicUsage);
return detailViewController;
}];
[DemoListViewController registerWithTitle:@"中文匹配" handler:^UIViewController *{
detailViewController.selectedSelector = @selector(demoChineseCharacter);
return detailViewController;
}];
[DemoListViewController registerWithTitle:@"自定義參數" handler:^UIViewController *{
detailViewController.selectedSelector = @selector(demoParameters);
return detailViewController;
}];
[DemoListViewController registerWithTitle:@"傳入字典信息" handler:^UIViewController *{
detailViewController.selectedSelector = @selector(demoUserInfo);
return detailViewController;
}];
[DemoListViewController registerWithTitle:@"Fallback 到全局的 URL Pattern" handler:^UIViewController *{
detailViewController.selectedSelector = @selector(demoFallback);
return detailViewController;
}];
整體調用流程:
- 在進入程序後,先使用MGJRouter對服務方組件進行註冊。每個URL對應一個Block的實現,Block中的代碼就是服務方對外提供的服務,調用方可以通過URL調用這個服務。
- 調用方通過MGJRouter調用OpenURL:方法,並將被調用代碼對應的URL傳入,MGJRouter會根據URL查找對應的Block實現,從而調用服務方組件的代碼進行通信。
- 調用和註冊Block時,Block有一個字典用來傳遞參數。這樣的優勢就是參數類型和數量理論上是不受限制的,但是需要很多硬編碼的key名在項目中。
2.2 Mediator
Mediator中間件使用開源庫CTMediator。對於服務方的組件來說,每個組件都提供一個或多個Target類,在Target類中聲明Action方法。Target類是當前組件對外提供的一個“服務類”,Target將當前組件中所有的服務都定義在裏面,CTMediator通過runtime主動發現服務。在Target中的所有Action方法,都只有一個字典參數,所以可以傳遞的參數很靈活,這也是CTMediator提出的去Model化的概念。在Action的方法實現中,對傳進來的字典參數進行解析,再調用組件內部的類和方法。
上圖就爲中間件Mediator,CTMediator是一個單例對象,它包含遠程APP調用入口方法和本地組件調用入口方法。
// 遠程App調用入口
- (id)performActionWithUrl:(NSURL *)url completion:(void(^)(NSDictionary *info))completion;
// 本地組件調用入口
- (id)performTarget:(NSString *)targetName action:(NSString *)actionName params:(NSDictionary *)params shouldCacheTarget:(BOOL)shouldCacheTarget;
以下爲CTMediator的消息轉發機制
- (id)performActionWithUrl:(NSURL *)url completion:(void (^)(NSDictionary *))completion
{
NSMutableDictionary *params = [[NSMutableDictionary alloc] init];
NSString *urlString = [url query];
for (NSString *param in [urlString componentsSeparatedByString:@"&"]) {
NSArray *elts = [param componentsSeparatedByString:@"="];
if([elts count] < 2) continue;
[params setObject:[elts lastObject] forKey:[elts firstObject]];
}
// 這裏這麼寫主要是出於安全考慮,防止黑客通過遠程方式調用本地模塊。這裏的做法足以應對絕大多數場景,如果要求更加嚴苛,也可以做更加複雜的安全邏輯。
NSString *actionName = [url.path stringByReplacingOccurrencesOfString:@"/" withString:@""];
if ([actionName hasPrefix:@"native"]) {
return @(NO);
}
// 這個demo針對URL的路由處理非常簡單,就只是取對應的target名字和method名字,但這已經足以應對絕大部份需求。如果需要拓展,可以在這個方法調用之前加入完整的路由邏輯
id result = [self performTarget:url.host action:actionName params:params shouldCacheTarget:NO];
if (completion) {
if (result) {
completion(@{@"result":result});
} else {
completion(nil);
}
}
return result;
}
- (id)performTarget:(NSString *)targetName action:(NSString *)actionName params:(NSDictionary *)params shouldCacheTarget:(BOOL)shouldCacheTarget
{
NSString *swiftModuleName = params[kCTMediatorParamsKeySwiftTargetModuleName];
// generate target
NSString *targetClassString = nil;
if (swiftModuleName.length > 0) {
targetClassString = [NSString stringWithFormat:@"%@.Target_%@", swiftModuleName, targetName];
} else {
targetClassString = [NSString stringWithFormat:@"Target_%@", targetName];
}
NSObject *target = self.cachedTarget[targetClassString];
if (target == nil) {
Class targetClass = NSClassFromString(targetClassString);
target = [[targetClass alloc] init];
}
// generate action
NSString *actionString = [NSString stringWithFormat:@"Action_%@:", actionName];
SEL action = NSSelectorFromString(actionString);
if (target == nil) {
// 這裏是處理無響應請求的地方之一,這個demo做得比較簡單,如果沒有可以響應的target,就直接return了。實際開發過程中是可以事先給一個固定的target專門用於在這個時候頂上,然後處理這種請求的
[self NoTargetActionResponseWithTargetString:targetClassString selectorString:actionString originParams:params];
return nil;
}
if (shouldCacheTarget) {
self.cachedTarget[targetClassString] = target;
}
if ([target respondsToSelector:action]) {
return [self safePerformAction:action target:target params:params];
} else {
// 這裏是處理無響應請求的地方,如果無響應,則嘗試調用對應target的notFound方法統一處理
SEL action = NSSelectorFromString(@"notFound:");
if ([target respondsToSelector:action]) {
return [self safePerformAction:action target:target params:params];
} else {
// 這裏也是處理無響應請求的地方,在notFound都沒有的時候,這個demo是直接return了。實際開發過程中,可以用前面提到的固定的target頂上的。
[self NoTargetActionResponseWithTargetString:targetClassString selectorString:actionString originParams:params];
[self.cachedTarget removeObjectForKey:targetClassString];
return nil;
}
}
}
- (void)releaseCachedTargetWithTargetName:(NSString *)targetName
{
NSString *targetClassString = [NSString stringWithFormat:@"Target_%@", targetName];
[self.cachedTarget removeObjectForKey:targetClassString];
}
#pragma mark - private methods
- (void)NoTargetActionResponseWithTargetString:(NSString *)targetString selectorString:(NSString *)selectorString originParams:(NSDictionary *)originParams
{
SEL action = NSSelectorFromString(@"Action_response:");
NSObject *target = [[NSClassFromString(@"Target_NoTargetAction") alloc] init];
NSMutableDictionary *params = [[NSMutableDictionary alloc] init];
params[@"originParams"] = originParams;
params[@"targetString"] = targetString;
params[@"selectorString"] = selectorString;
[self safePerformAction:action target:target params:params];
}
- (id)safePerformAction:(SEL)action target:(NSObject *)target params:(NSDictionary *)params
{
NSMethodSignature* methodSig = [target methodSignatureForSelector:action];
if(methodSig == nil) {
return nil;
}
const char* retType = [methodSig methodReturnType];
if (strcmp(retType, @encode(void)) == 0) {
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig];
[invocation setArgument:¶ms atIndex:2];
[invocation setSelector:action];
[invocation setTarget:target];
[invocation invoke];
return nil;
}
if (strcmp(retType, @encode(NSInteger)) == 0) {
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig];
[invocation setArgument:¶ms atIndex:2];
[invocation setSelector:action];
[invocation setTarget:target];
[invocation invoke];
NSInteger result = 0;
[invocation getReturnValue:&result];
return @(result);
}
if (strcmp(retType, @encode(BOOL)) == 0) {
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig];
[invocation setArgument:¶ms atIndex:2];
[invocation setSelector:action];
[invocation setTarget:target];
[invocation invoke];
BOOL result = 0;
[invocation getReturnValue:&result];
return @(result);
}
if (strcmp(retType, @encode(CGFloat)) == 0) {
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig];
[invocation setArgument:¶ms atIndex:2];
[invocation setSelector:action];
[invocation setTarget:target];
[invocation invoke];
CGFloat result = 0;
[invocation getReturnValue:&result];
return @(result);
}
if (strcmp(retType, @encode(NSUInteger)) == 0) {
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig];
[invocation setArgument:¶ms atIndex:2];
[invocation setSelector:action];
[invocation setTarget:target];
[invocation invoke];
NSUInteger result = 0;
[invocation getReturnValue:&result];
return @(result);
}
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
return [target performSelector:action withObject:params];
#pragma clang diagnostic pop
}
#pragma mark - getters and setters
- (NSMutableDictionary *)cachedTarget
{
if (_cachedTarget == nil) {
_cachedTarget = [[NSMutableDictionary alloc] init];
}
return _cachedTarget;
}
而CTMediator的Category提供了一個獲取控制器VC並且跳轉的功能,外界的其他組件依賴於CTMediator,通過CTMediator的擴展提供的方法進行參數的傳遞或者界面的跳轉控制,下面爲Category提供的方法接口。
#import "CTMediator+CTMediatorModuleAActions.h"
NSString * const kCTMediatorTargetA = @"A";
NSString * const kCTMediatorActionNativFetchDetailViewController = @"nativeFetchDetailViewController";
NSString * const kCTMediatorActionNativePresentImage = @"nativePresentImage";
NSString * const kCTMediatorActionNativeNoImage = @"nativeNoImage";
NSString * const kCTMediatorActionShowAlert = @"showAlert";
NSString * const kCTMediatorActionCell = @"cell";
NSString * const kCTMediatorActionConfigCell = @"configCell";
@implementation CTMediator (CTMediatorModuleAActions)
- (UIViewController *)CTMediator_viewControllerForDetail
{
UIViewController *viewController = [self performTarget:kCTMediatorTargetA
action:kCTMediatorActionNativFetchDetailViewController
params:@{@"key":@"value"}
shouldCacheTarget:NO
];
if ([viewController isKindOfClass:[UIViewController class]]) {
// view controller 交付出去之後,可以由外界選擇是push還是present
return viewController;
} else {
// 這裏處理異常場景,具體如何處理取決於產品
return [[UIViewController alloc] init];
}
}
而組件中提供CTMediator消息轉發服務的對象,定義在了Target_A中,下面代碼爲Target_A中具體實現,在Target_A(kCTMediatorTargetA)返回具體控制器。這些Target_A的服務可以被CTMediator通過runtime的方式調用,而且在Target_A可以對相關的參數進行處理和block調用等,保證組件間的業務邏輯是隔離的,只通過CTMediator來進行調用。
當外界組件依賴並開始使用CTMediator的Category方法時,通過CTMediator底層的消息轉發機制,獲取相關Target_A中相關的控制器,再交由CTMediator的Category方法進行跳轉或者參數的傳遞,完成不同組件間的通信。
- (UIViewController *)Action_nativeFetchDetailViewController:(NSDictionary *)params
{
// 因爲action是從屬於ModuleA的,所以action直接可以使用ModuleA裏的所有聲明
DemoModuleADetailViewController *viewController = [[DemoModuleADetailViewController alloc] init];
viewController.valueLabel.text = params[@"key"];
return viewController;
}