iOS 的組件化開發

在一個APP開發過程當中,若是項目較小且團隊人數較少,使用最基本的MVC、MVVM開發就已經足夠了,由於維護成本比較低。html

可是當一個項目開發團隊人數較多時,由於每一個人都會負責相應組件的開發,常規開發模式耦合會愈來愈嚴重,並且致使大量代碼衝突,會使後期維護和升級過程當中代碼「牽一髮而動全身」,額外帶來很大的工做量,而且會致使一些潛在的BUG。git

在這時,組件化開發就派上很大用場了,所謂的組件化開發,就是把APP根據業務拆分爲各獨立的組件,各個組件相互寫做,組成完整的APP。github

1、各組件的引入

關於組件的拆分,就根據具體項目進行拆分,假如APP被拆分了AModule、BModule、CModule,那麼,應該如何引入這些組件呢?你可能會想到APP的入口AppDelegate。在平時開發中,AppDelegate中每每初始化了好多組件,好比推送、統計等組件,這樣就會致使AppDelegate的臃腫。app

因此,咱們能夠增長一個ModuleManager,專門用來初始化各組件。
首先增長一個 ModuleProtocol工具

#import <Foundation/Foundation.h>
@import UIKit;
@import UserNotifications;

@protocol ModuleProtocol <UIApplicationDelegate, UNUserNotificationCenterDelegate>

@end

咱們在ModuleManager中hook住UIApplicationDelegateUNUserNotificationCenterDelegate中的方法,使相應的組件中實現了對應方法,在相應時機就會調用組建裏的對應方法:組件化

#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:atom

#import <Foundation/Foundation.h>
#import "ModuleProtocol.h"

@interface ModuleManager : NSObject

+ (instancetype)sharedInstance;

- (void)loadModulesWithPlistFile:(NSString *)plistFile;

- (NSArray<id<ModuleProtocol>> *)allModules;

@end

以後咱們經過一個 ModulesRegister.plist文件管理須要引入的組件:url

ModulesRegister.plist

如上圖,假如咱們要引入AModule、BModule、CModule,那麼這三個Module只須要實現協議ModuleProtocol,而後實現AppDelegate中對應的方法,在對應方法中初始化自身便可:
AModule.h:spa

#import <Foundation/Foundation.h>
#import "ModuleProtocol.h"

@interface AModule : NSObject<ModuleProtocol>

@end

AModule.m:3d

#import "AModule.h"

@implementation AModule

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions{
    //初始化AModule
    return YES;
}

@end

以後在AppDelegateload方法中經過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

這樣,各組件的開發者在本身的組件中初始化本身,其餘人須要使用時只須要加入ModulesRegister.plist文件中便可。

2、組件間協做

簡單來看,假設APP的每一個頁面就是一個組件,假如咱們的APP有AViewController、BViewController、CViewController、DViewController、EViewController,各ViewController必然設置各類相互跳轉。那麼,咱們APP的跳轉邏輯多是下面這個樣子:
跳轉邏輯

爲了解決這種複雜的耦合關係,咱們能夠增長一個Router中間層去管理各ViewController之間的跳轉關係(也就是實際開發中組件間相互調用的關係)。

因此,根據須要,某開源做者開發並開源了一個支持URL Rewrite的iOS路由庫— FFRouter,經過FFRouter去管理各ViewController之間的跳轉關係:
FFRouter

這樣,各ViewController之間的跳轉關係就變的清晰了許多。

FFRouter經過提早註冊對應的URL,以後就直接經過打開URL去控制各ViewController之間的跳轉(或各組件間的調用)。
FFRouter支持組件間傳遞很是規對象,如UIImage等,並支持獲取組件返回值。
基本使用以下:

/**
 註冊 url

 @param routeURL 要註冊的 URL
 @param handlerBlock URL 被 Route 後的回調
 */
+ (void)registerRouteURL:(NSString *)routeURL handler:(FFRouterHandler)handlerBlock;

/**
 註冊 URL,經過該方式註冊的 URL 被 Route 後可返回一個 Object

 @param routeURL 要註冊的 URL
 @param handlerBlock URL 被 Route 後的回調,可在回調中返回一個 Object
 */
+ (void)registerObjectRouteURL:(NSString *)routeURL handler:(FFObjectRouterHandler)handlerBlock;



/**
 判斷 URL 是否可被 Route(是否已經註冊)

 @param URL 要判斷的 URL
 @return 是否可被 Route
 */
+ (BOOL)canRouteURL:(NSString *)URL;



/**
 Route 一個 URL

 @param URL 要 Router 的 URL
 */
+ (void)routeURL:(NSString *)URL;

/**
 Route 一個 URL,並帶上額外參數

 @param URL 要 Router 的 URL
 @param parameters 額外參數
 */
+ (void)routeURL:(NSString *)URL withParameters:(NSDictionary<NSString *, id> *)parameters;

/**
 Route 一個 URL,可得到返回的 Object

 @param URL 要 Router 的 URL
 @return 返回的 Object
 */
+ (id)routeObjectURL:(NSString *)URL;

/**
 Route 一個 URL,並帶上額外參數,可得到返回的 Object

 @param URL 要 Router 的 URL
 @param parameters 額外參數
 @return 返回的 Object
 */
+ (id)routeObjectURL:(NSString *)URL withParameters:(NSDictionary<NSString *, id> *)parameters;



/**
 Route 一個未註冊 URL 時回調

 @param handler 回調
 */
+ (void)routeUnregisterURLHandler:(FFRouterUnregisterURLHandler)handler;



/**
 取消註冊某個 URL

 @param URL 要被取消註冊的 URL
 */
+ (void)unregisterRouteURL:(NSString *)URL;

/**
 取消註冊全部 URL
 */
+ (void)unregisterAllRoutes;


/**
 是否顯示 Log,用於調試

 @param enable YES or NO,默認爲 NO
 */
+ (void)setLogEnabled:(BOOL)enable;

並且參考天貓的方案增長了URL Rewrite功能:

可使用正則添加一條 Rewrite 規則,例如:
要實現打開 URL:https://www.taobao.com/search/原子彈時,將其攔截,改用本地已註冊的 URL:protocol://page/routerDetails?product=原子彈打開。
首先添加一條 Rewrite 規則:

[FFRouterRewrite addRewriteMatchRule:@"(?:https://)?www.taobao.com/search/(.*)" targetRule:@"protocol://page/routerDetails?product=$1"];

以後在打開URL:https://www.taobao.com/search/原子彈時,將會 Rewrite 到URL:protocol://page/routerDetails?product=原子彈

[FFRouter routeURL:@"https://www.taobao.com/search/原子彈"];


能夠經過如下方法同時增長多個規則:

+ (void)addRewriteRules:(NSArray<NSDictionary *> *)rules;

其中 rules 格式必須爲如下格式:

@[@{@"matchRule":@"YourMatchRule1",@"targetRule":@"YourTargetRule1"},
  @{@"matchRule":@"YourMatchRule2",@"targetRule":@"YourTargetRule2"},
  @{@"matchRule":@"YourMatchRule3",@"targetRule":@"YourTargetRule3"},]


Rewrite 規則中的保留字:

  • 經過 $scheme$host$port$path$query$fragment 獲取標準 URL 中的相應部分。經過$url獲取完整 URL
  • 經過 $1$2$3...獲取matchRule的正則中使用圓括號取出的參數
  • $:原變量的值、$$:原變量URL Encode後的值、$#:原變量URL Decode後的值


例如:
https://www.taobao.com/search/原子彈對於Rewrite 規則(?: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對於Rewrite 規則(?:https://)?www.taobao.com/search/(.*)

$1=%e5%8e%9f%e5%ad%90%e5%bc%b9
$#1=原子彈

考慮到常常用路由配置UIViewController之間的跳轉,因此增長了額外的工具FFRouterNavigation來更方便地控制UIViewController之間的跳轉。

3、其餘組件化方案

目前這種組件化方案參考了蘑菇街、天貓、京東的的實現方案。除這種方案外,Casa(查看文章)以前提出瞭解耦程度更高的方案,這種方案組件仍然使用中間件通訊,但中間件經過 runtime 接口解耦,而後使用 target-action 簡化寫法,經過 category 分離組件接口代碼。 可是,這種方案雖然解耦程度更高,可是也增長了組件化的成本,綜合考慮,直接使用中間件通訊的方式更好一點。具體哪一種方案好,也就仁者見仁、智者見智了~

相關文章
相關標籤/搜索