在一個(gè)APP開(kāi)發(fā)過(guò)程中豪嗽,如果項(xiàng)目較小且團(tuán)隊(duì)人數(shù)較少,使用最基本的MVC豌骏、MVVM開(kāi)發(fā)就已經(jīng)足夠了龟梦,因?yàn)榫S護(hù)成本比較低。
但是當(dāng)一個(gè)項(xiàng)目開(kāi)發(fā)團(tuán)隊(duì)人數(shù)較多時(shí)窃躲,因?yàn)槊總€(gè)人都會(huì)負(fù)責(zé)相應(yīng)組件的開(kāi)發(fā)计贰,常規(guī)開(kāi)發(fā)模式耦合會(huì)越來(lái)越嚴(yán)重,而且導(dǎo)致大量代碼沖突蒂窒,會(huì)使后期維護(hù)和升級(jí)過(guò)程中代碼“牽一發(fā)而動(dòng)全身”躁倒,額外帶來(lái)很大的工作量荞怒,并且會(huì)導(dǎo)致一些潛在的BUG。
在這時(shí)秧秉,組件化開(kāi)發(fā)就派上很大用場(chǎng)了褐桌,所謂的組件化開(kāi)發(fā),就是把APP根據(jù)業(yè)務(wù)拆分為各獨(dú)立的組件象迎,各個(gè)組件相互寫(xiě)作荧嵌,組成完整的APP。
一砾淌、各組件的引入
關(guān)于組件的拆分啦撮,就根據(jù)具體項(xiàng)目進(jìn)行拆分,假如APP被拆分了AModule拇舀、BModule逻族、CModule,那么骄崩,應(yīng)該如何引入這些組件呢聘鳞?你可能會(huì)想到APP的入口AppDelegate。在平時(shí)開(kāi)發(fā)中要拂,AppDelegate中往往初始化了好多組件抠璃,比如推送、統(tǒng)計(jì)等組件脱惰,這樣就會(huì)導(dǎo)致AppDelegate的臃腫搏嗡。
所以,我們可以增加一個(gè)ModuleManager
拉一,專門(mén)用來(lái)初始化各組件采盒。
首先增加一個(gè) ModuleProtocol
:
#import <Foundation/Foundation.h>
@import UIKit;
@import UserNotifications;
@protocol ModuleProtocol <UIApplicationDelegate, UNUserNotificationCenterDelegate>
@end
我們?cè)?code>ModuleManager中hook住UIApplicationDelegate
和 UNUserNotificationCenterDelegate
中的方法,使相應(yīng)的組件中實(shí)現(xiàn)了對(duì)應(yīng)方法蔚润,在相應(yīng)時(shí)機(jī)就會(huì)調(diào)用組建里的對(duì)應(yīng)方法:
#import "ModuleManager.h"
#import "AppDelegate.h"
#import <objc/runtime.h>
#define ALL_MODULE [[ModuleManager sharedInstance] allModules]
#define SWIZZLE_METHOD(m) swizzleMethod(class, @selector(m),@selector(module_##m));
@interface ModuleManager ()
@property (nonatomic, strong) NSMutableArray<id<ModuleProtocol>> *modules;
@end
@implementation ModuleManager
+ (instancetype)sharedInstance { ...... }
- (NSMutableArray<id<ModuleProtocol>> *)modules { ...... }
- (void)addModule:(id<ModuleProtocol>) module { ...... }
- (void)loadModulesWithPlistFile:(NSString *)plistFile { ...... }
- (NSArray<id<ModuleProtocol>> *)allModules { ...... }
@end
@implementation AppDelegate (Module)
+ (void)load
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class class = [self class];
SWIZZLE_METHOD(application:willFinishLaunchingWithOptions:);
SWIZZLE_METHOD(application:didFinishLaunchingWithOptions:);
......
});
}
static inline void swizzleMethod(Class class, SEL originalSelector, SEL swizzledSelector) { ...... }
- (BOOL)module_application:(UIApplication *)application willFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
BOOL result = [self module_application:application willFinishLaunchingWithOptions:launchOptions];
for (id<ModuleProtocol> module in ALL_MODULE) {
if ([module respondsToSelector:_cmd]) {
[module application:application willFinishLaunchingWithOptions:launchOptions];
}
}
return result;
}
- (BOOL)module_application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
BOOL result = [self module_application:application didFinishLaunchingWithOptions:launchOptions];
for (id<ModuleProtocol> module in ALL_MODULE) {
if ([module respondsToSelector:_cmd]) {
[module application:application didFinishLaunchingWithOptions:launchOptions];
}
}
return result;
}
......
@end
ModuleManager.h
:
#import <Foundation/Foundation.h>
#import "ModuleProtocol.h"
@interface ModuleManager : NSObject
+ (instancetype)sharedInstance;
- (void)loadModulesWithPlistFile:(NSString *)plistFile;
- (NSArray<id<ModuleProtocol>> *)allModules;
@end
之后我們通過(guò)一個(gè) ModulesRegister.plist
文件管理需要引入的組件:
如上圖磅氨,假如我們要引入AModule、BModule嫡纠、CModule烦租,那么這三個(gè)Module只需要實(shí)現(xiàn)協(xié)議ModuleProtocol
,然后實(shí)現(xiàn)AppDelegate
中對(duì)應(yīng)的方法除盏,在對(duì)應(yīng)方法中初始化自身即可:
AModule.h
:
#import <Foundation/Foundation.h>
#import "ModuleProtocol.h"
@interface AModule : NSObject<ModuleProtocol>
@end
AModule.m
:
#import "AModule.h"
@implementation AModule
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions{
//初始化AModule
return YES;
}
@end
之后在AppDelegate
的load
方法中通過(guò)ModulesRegister.plist
引入各組件即可:
@implementation AppDelegate
+ (void)load {
//load modules
NSString* plistPath = [[NSBundle mainBundle] pathForResource:@"ModulesRegister" ofType:@"plist"];
[[ModuleManager sharedInstance] loadModulesWithPlistFile:plistPath];
}
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
......
}
@end
這樣叉橱,各組件的開(kāi)發(fā)者在自己的組件中初始化自己,其他人需要使用時(shí)只需要加入ModulesRegister.plist
文件中即可者蠕。
二窃祝、組件間協(xié)作
簡(jiǎn)單來(lái)看,假設(shè)APP的每個(gè)頁(yè)面就是一個(gè)組件踱侣,假如我們的APP有AViewController锌杀、BViewController甩栈、CViewController、DViewController糕再、EViewController,各ViewController必然設(shè)置各種相互跳轉(zhuǎn)玉转。那么突想,我們APP的跳轉(zhuǎn)邏輯可能是下面這個(gè)樣子:
為了解決這種復(fù)雜的耦合關(guān)系,我們可以增加一個(gè)Router中間層去管理各ViewController之間的跳轉(zhuǎn)關(guān)系(也就是實(shí)際開(kāi)發(fā)中組件間相互調(diào)用的關(guān)系)究抓。
所以猾担,根據(jù)需要,我開(kāi)發(fā)并開(kāi)源了一個(gè)支持URL Rewrite的iOS路由庫(kù)— FFRouter刺下,通過(guò)FFRouter去管理各ViewController之間的跳轉(zhuǎn)關(guān)系:
這樣绑嘹,各ViewController之間的跳轉(zhuǎn)關(guān)系就變的清晰了許多。
FFRouter通過(guò)提前注冊(cè)對(duì)應(yīng)的URL橘茉,之后就直接通過(guò)打開(kāi)URL去控制各ViewController之間的跳轉(zhuǎn)(或各組件間的調(diào)用)工腋。
FFRouter支持組件間傳遞非常規(guī)對(duì)象,如UIImage等畅卓,并支持獲取組件返回值擅腰。
基本使用如下:
/**
注冊(cè) url
@param routeURL 要注冊(cè)的 URL
@param handlerBlock URL 被 Route 后的回調(diào)
*/
+ (void)registerRouteURL:(NSString *)routeURL handler:(FFRouterHandler)handlerBlock;
/**
注冊(cè) URL,通過(guò)該方式注冊(cè)的 URL 被 Route 后可返回一個(gè) Object
@param routeURL 要注冊(cè)的 URL
@param handlerBlock URL 被 Route 后的回調(diào),可在回調(diào)中返回一個(gè) Object
*/
+ (void)registerObjectRouteURL:(NSString *)routeURL handler:(FFObjectRouterHandler)handlerBlock;
/**
判斷 URL 是否可被 Route(是否已經(jīng)注冊(cè))
@param URL 要判斷的 URL
@return 是否可被 Route
*/
+ (BOOL)canRouteURL:(NSString *)URL;
/**
Route 一個(gè) URL
@param URL 要 Router 的 URL
*/
+ (void)routeURL:(NSString *)URL;
/**
Route 一個(gè) URL,并帶上額外參數(shù)
@param URL 要 Router 的 URL
@param parameters 額外參數(shù)
*/
+ (void)routeURL:(NSString *)URL withParameters:(NSDictionary<NSString *, id> *)parameters;
/**
Route 一個(gè) URL翁潘,可獲得返回的 Object
@param URL 要 Router 的 URL
@return 返回的 Object
*/
+ (id)routeObjectURL:(NSString *)URL;
/**
Route 一個(gè) URL趁冈,并帶上額外參數(shù),可獲得返回的 Object
@param URL 要 Router 的 URL
@param parameters 額外參數(shù)
@return 返回的 Object
*/
+ (id)routeObjectURL:(NSString *)URL withParameters:(NSDictionary<NSString *, id> *)parameters;
/**
Route 一個(gè)未注冊(cè) URL 時(shí)回調(diào)
@param handler 回調(diào)
*/
+ (void)routeUnregisterURLHandler:(FFRouterUnregisterURLHandler)handler;
/**
取消注冊(cè)某個(gè) URL
@param URL 要被取消注冊(cè)的 URL
*/
+ (void)unregisterRouteURL:(NSString *)URL;
/**
取消注冊(cè)所有 URL
*/
+ (void)unregisterAllRoutes;
/**
是否顯示 Log拜马,用于調(diào)試
@param enable YES or NO渗勘,默認(rèn)為 NO
*/
+ (void)setLogEnabled:(BOOL)enable;
而且參考天貓的方案增加了URL Rewrite功能:
可以使用正則
添加一條 Rewrite 規(guī)則,例如:
要實(shí)現(xiàn)打開(kāi) URL:https://www.taobao.com/search/原子彈
時(shí)俩莽,將其攔截旺坠,改用本地已注冊(cè)的 URL:protocol://page/routerDetails?product=原子彈
打開(kāi)。
首先添加一條 Rewrite 規(guī)則:
[FFRouterRewrite addRewriteMatchRule:@"(?:https://)?www.taobao.com/search/(.*)" targetRule:@"protocol://page/routerDetails?product=$1"];
之后在打開(kāi)URL:https://www.taobao.com/search/原子彈
時(shí)豹绪,將會(huì) Rewrite 到URL:protocol://page/routerDetails?product=原子彈
价淌。
[FFRouter routeURL:@"https://www.taobao.com/search/原子彈"];
可以通過(guò)以下方法同時(shí)增加多個(gè)規(guī)則:
+ (void)addRewriteRules:(NSArray<NSDictionary *> *)rules;
其中 rules 格式必須為以下格式:
@[@{@"matchRule":@"YourMatchRule1",@"targetRule":@"YourTargetRule1"},
@{@"matchRule":@"YourMatchRule2",@"targetRule":@"YourTargetRule2"},
@{@"matchRule":@"YourMatchRule3",@"targetRule":@"YourTargetRule3"},]
Rewrite 規(guī)則中的保留字:
- 通過(guò)
$scheme
、$host
瞒津、$port
蝉衣、$path
、$query
巷蚪、$fragment
獲取標(biāo)準(zhǔn) URL 中的相應(yīng)部分病毡。通過(guò)$url
獲取完整 URL - 通過(guò)
$1
、$2
屁柏、$3
...獲取matchRule
的正則中使用圓括號(hào)取出的參數(shù) -
$
:原變量的值啦膜、$$
:原變量URL Encode后的值有送、$#
:原變量URL Decode后的值
例如:
https://www.taobao.com/search/原子彈
對(duì)于Rewrite 規(guī)則(?:https://)?www.taobao.com/search/(.*)
$1=原子彈
$$1=%e5%8e%9f%e5%ad%90%e5%bc%b9
同樣,https://www.taobao.com/search/%e5%8e%9f%e5%ad%90%e5%bc%b9
對(duì)于Rewrite 規(guī)則(?:https://)?www.taobao.com/search/(.*)
$1=%e5%8e%9f%e5%ad%90%e5%bc%b9
$#1=原子彈
考慮到經(jīng)常用路由配置UIViewController
之間的跳轉(zhuǎn)僧家,所以增加了額外的工具FFRouterNavigation
來(lái)更方便地控制UIViewController
之間的跳轉(zhuǎn)雀摘。
三、其他組件化方案
目前這種組件化方案參考了蘑菇街八拱、天貓阵赠、京東的的實(shí)現(xiàn)方案。除這種方案外肌稻,Casa(查看文章)之前提出了解耦程度更高的方案清蚀,這種方案組件仍然使用中間件通信,但中間件通過(guò) runtime 接口解耦爹谭,然后使用 target-action 簡(jiǎn)化寫(xiě)法枷邪,通過(guò) category 分離組件接口代碼。
但是诺凡,這種方案雖然解耦程度更高东揣,但是也增加了組件化的成本,綜合考慮绑洛,直接使用中間件通信的方式更好一點(diǎn)救斑。具體哪種方案好,也就仁者見(jiàn)仁真屯、智者見(jiàn)智了~