在一个 App 开发过程中, 如果项目较小且团队人数较少, 使用最基本的 MVC,MVVM 开发就已经足够了, 因为维护成本比较低.
但是当一个项目开发团队人数较多时, 因为每个人都会负责相应组件的开发, 常规开发模式耦合会越来越严重, 而且导致大量代码冲突, 会使后期维护和升级过程中代码 "牵一发而动全身", 额外带来很大的工作量, 并且会导致一些潜在的 BUG.
在这时, 组件化开发就派上很大用场了, 所谓的组件化开发, 就是把 App 根据业务拆分为各独立的组件, 各个组件相互写作, 组成完整的 App.
一, 各组件的引入
关于组件的拆分, 就根据具体项目进行拆分, 假如 App 被拆分了 AModule,BModule,CModule, 那么, 应该如何引入这些组件呢? 你可能会想到 App 的入口 AppDelegate. 在平时开发中, AppDelegate 中往往初始化了好多组件, 比如推送, 统计等组件, 这样就会导致 AppDelegate 的臃肿.
所以, 我们可以增加一个 ModuleManager, 专门用来初始化各组件.
首先增加一个 ModuleProtocol:
- #import <Foundation/Foundation.h>
- @import UIKit;
- @import UserNotifications;
- @protocol ModuleProtocol <UIApplicationDelegate, UNUserNotificationCenterDelegate>
- @end
我们在 ModuleManager 中 hook 住 UIApplicationDelegate 和 UNUserNotificationCenterDelegate 中的方法, 使相应的组件中实现了对应方法, 在相应时机就会调用组建里的对应方法:
- #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
之后我们通过一个 ModulesRegister.plist 文件管理需要引入的组件:
如上图, 假如我们要引入 AModule,BModule,CModule, 那么这三个 Module 只需要实现协议 ModuleProtocol, 然后实现 AppDelegate 中对应的方法, 在对应方法中初始化自身即可:
- 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 方法中通过 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 文件中即可.
二, 组件间协作
简单来看, 假设 App 的每个页面就是一个组件, 假如我们的 App 有 AViewController,BViewController,CViewController,DViewController,EViewController, 各 ViewController 必然设置各种相互跳转. 那么, 我们 App 的跳转逻辑可能是下面这个样子:
为了解决这种复杂的耦合关系, 我们可以增加一个 Router 中间层去管理各 ViewController 之间的跳转关系 (也就是实际开发中组件间相互调用的关系).
所以, 根据需要, 某开源作者开发并开源了一个支持 URL Rewrite 的 iOS 路由库 - https://github.com/imlifengfeng/FFRouter , 通过 https://github.com/imlifengfeng/FFRouter 去管理各 ViewController 之间的跳转关系:
这样, 各 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=原子弹
同样, https://www.taobao.com/search/原子弹 对于 Rewrite 规则 (?:https://)?www.taobao.com/search/(.*)
- $1=%e5%8e%9f%e5%ad%90%e5%bc%b9
- $#1 = 原子弹
考虑到经常用路由配置 UIViewController 之间的跳转, 所以增加了额外的工具 FFRouterNavigation 来更方便地控制 UIViewController 之间的跳转.
三, 其他组件化方案
目前这种组件化方案参考了蘑菇街, 天猫, 京东的的实现方案. 除这种方案外, Casa(查看文章 https://casatwy.com/iOS-Modulization.html ) 之前提出了解耦程度更高的方案, 这种方案组件仍然使用中间件通信, 但中间件通过 runtime 接口解耦, 然后使用 target-action 简化写法, 通过 category 分离组件接口代码.
但是, 这种方案虽然解耦程度更高, 但是也增加了组件化的成本, 综合考虑, 直接使用中间件通信的方式更好一点. 具体哪种方案好, 也就仁者见仁, 智者见智了~
来源: https://www.cnblogs.com/lifengfneg/p/9798639.html