组件化

组件化初探

组件化其实就是将模块单独抽离分层并指定模块间的通讯方式,从而实现解耦的一种方式,主要运用与团队开发。

为什么需要组件化?原因如下

  • 模块间解耦
  • 模块重用
  • 提高团队协作开发效率
  • 单元测试

当项目越来越大,如果此时的各个模块之间是互相调用,一旦我们需要对某一块代码进行修改,就会牵一发而动全身,导致项目难以维护。这个时候就需要进行组件化

需要组件化的项目主要体现在以下几个方面:

  • 修改某个功能,同时需要修改其他模块的代码,因为在其他模块中有该模块的引用。
  • 当模块需要重用到其他项目时,难以单独抽离
  • 模块间耦合的忌口导致接口和依赖关系混乱,无法进行单元测试

为了解决以上问题,我们需要采用更规范的方式来降低模块间的耦合度,这就是组件化,也可以理解为模块化

但是组件化也是需要一定成本的,需要花费时间设计接口分离代码等,而且组件化的代码量会更大一些,所以并不是所有的项目都需要组件化。如果你的项目有以下这些特征就不需要组件化:

  • 项目较小,模块间交互简单,耦合少
  • 模块没有被多个外部模块引用,只是一个单独的小模块
  • 模块不需要重用,代码也很少被修改
  • 团队规模很小
组件化分层

一个项目主要分为3层:业务层通用层以及基础层,在进行组件化时,有以下几点说明

image.png
image.png
  • 只能上层对下层依赖,不能下层对上层的依赖,因为下层是对上层的抽象
  • 项目公共代码资源下沉
  • 横向的依赖最好下沉至通用模块,或者基础模块

cocoapods铺垫

cocoapods是怎么找到一个三方库的?
cocoapods安装完成之后,里面会有一个本地资源库repos。三方库的地址版本号等信息都在本地资源库中,有了这些信息就能成功引入三方库

image.png
image.png

查看本地资源库repos中三方库的索引,文件搜索AFNetworking -> 选择Specs -> 右键在上层文件夹中显示

image.png

GitHubCocoaPods库里面维护有三方库的资源信息。

image.png
CocoaPods引用三方库流程图

组件化模块创建

模块创建
  • 进入一个空目录,执行$ pod lib create LGHomeModule命令,本质是以CocoaPods/pod-template为模版来创建模块
// 创建LGHomeModule模块
$ pod lib create LGHomeModule
......
选择类型如下
What platform do you want to use?? [ iOS / macOS ]
 > iOS
What language do you want to use?? [ Swift / ObjC ]
 > ObjC
Would you like to include a demo application with your library? [ Yes / No ]
 > Yes
Which testing frameworks will you use? [ Specta / Kiwi / None ]
 > None
Would you like to do view based testing? [ Yes / No ]
 > No
What is your class prefix? //添加前缀
 > LG

LGHomeModule模块成功创建

image.png
  • 模块的代码放入Pods -> LGHomeModule目录下,文件目录如下
image.png

代码文件放入Classes目录,资源文件放入Assets目录

  • 模块功能实现完成之后,在Example目录工程引用调试。LGHomeModule模块类似于AFNetworking被导入使用。

按照上面方式创建LGCommonUIModule模块,并编写好代码。编译LGCommonUIModule工程,有如下报错

image.png

报错原因:我们编写的代码中,使用到的三方库在工程中没有找到,那这些三方库要怎么引用呢?下面讨论......

组件化三方和本地组件依赖

按照以往的使用需要在Pods -> Podfile中引用Monsary库。但这里我们是要在自定义模块LGCommonUIModule中使用,那就要查找怎么在LGCommonUIModule模块中导入三方库?

  • 我们发现LGCommonUIModule目录下也有一个Pod目录,也就意味着三方库可以在Pod -> LGCommonUIModule.podspec中引用
引用三方库
  • 进入LGCommonUIModule -> Example目录,执行$ pod install导入三方库,并编译工程,发现还是有报错
image.png

这一次报错原因是找不到我们声明的宏定义LGScreenWidth。因为我们把那些基础属性以及宏定义等等放在我们创建的基础模块LGMacroAndCategoryModule中,而基础模块并没有导入。

  • 引用LGMacroAndCategoryModule基础模块
LGMacroAndCategoryModule基础模块
基础模块引用
  • 执行$ pod install导入自定义基础模块,编译报错如下
image.png

自定义基础模块LGMacroAndCategoryModule在本地,并没有上传服务器,所以找不到。需要我们对其路径进行指定

  • 指定LGMacroAndCategoryModule基础模块路径
image.png
  • 再次执行$ pod install导入,编译成功

组件化资源文件加载

自定义组件的图片资源放在Assets目录,执行$ pod install命令进行导入。运行工程发现图片并没有展示出来。

image.png

原因是导入的图片在自定义模块中,而这里引用的图片是在LGModuleTest -> Images.xcassets,需要我们把图片的路径指定到自定义模块中

  • 指定图片资源路径
image.png
  • 打开资源文件引用权限
image.png
  • 执行$ pod install,运行工程成功加载图片
引用json文件与xib文件
引用json文件
引用xib文件

组件化CTMediator解耦通讯

当我们把项目拆分成各个模块之后,模块之间的通信就需要我们考虑解决了。

组件化通讯方案目前主流的有以下三种方式:

  • URL路由
  • target-action
  • protocol匹配
URL路由

URL路由方式主要是以蘑菇街为代表的的MGJRouter
实现思路:

  • App启动时实例化各组件模块,然后这些组件向ModuleManager注册Url,有些时候不需要实例化,使用class注册
  • 当组件A需要调用组件B时,向ModuleManager传递URL,参数跟随URL以GET方式传递,类似openURL。然后由ModuleManager负责调度组件B,最后完成任务。
// 1、注册某个URL
MGJRouter.registerURLPattern("app://home") { (info) in
    print("info: \(info)")
}

//2、调用路由
MGJRouter.openURL("app://home")
target-action

target-action基于OC的runtimecategory特性动态获取模块,例如通过NSClassFromString获取类并创建实例,通过performSelector + NSInvocation动态调用方法。

其主要的代表框架是CTMediator
实现思路:

  • 利用分类为路由添加新接口,在接口中通过字符串获取对应的类
  • 通过runtime创建实例,动态调用实例的方法
//******* 1、分类定义新接口
extension CTMediator{
    @objc func A_showHome()->UIViewController?{
        let params = [
            kCTMediatorParamsKeySwiftTargetModuleName: "Base_Example"
        ]
        if let vc = self.performTarget("A", action: "Extension_HomeViewController", params: params, shouldCacheTarget: false) as? UIViewController{
            return vc
        }
        return nil
    }
}

//******* 2、模块提供者提供target-action的调用方式(对外需要加上public关键字)
class Target_A: NSObject {
    @objc func Action_Extension_HomeViewController(_ params: [String: Any])->UIViewController{
        let home = HomeViewController()
        return home
    }
}

//******* 3、使用
if let vc = CTMediator.sharedInstance().A_showHome() {
    self.navigationController?.pushViewController(vc, animated: true)
}

模块间的引用关系如下图

image.png
CTMediator源码分析
  • 通过分类中调用的performTarget来到CTMediator中的具体实现,即performTarget:action:params:shouldCacheTarget:,主要是通过传入的name,找到对应的targetaction
- (id)performTarget:(NSString *)targetName action:(NSString *)actionName params:(NSDictionary *)params shouldCacheTarget:(BOOL)shouldCacheTarget
{
    if (targetName == nil || actionName == nil) {
        return nil;
    }
    //在swift中使用时,需要传入对应项目的target名称,否则会找不到视图控制器
    NSString *swiftModuleName = params[kCTMediatorParamsKeySwiftTargetModuleName];
    
    // generate target 生成target
    NSString *targetClassString = nil;
    if (swiftModuleName.length > 0) {
        //swift中target文件名拼接
        targetClassString = [NSString stringWithFormat:@"%@.Target_%@", swiftModuleName, targetName];
    } else {
        //OC中target文件名拼接
        targetClassString = [NSString stringWithFormat:@"Target_%@", targetName];
    }
    //缓存中查找target
    NSObject *target = [self safeFetchCachedTarget:targetClassString];
    //缓存中没有target
    if (target == nil) {
        //通过字符串获取对应的类
        Class targetClass = NSClassFromString(targetClassString);
        //创建实例
        target = [[targetClass alloc] init];
    }

    // 去中心化
    // generate action 生成action方法名称
    NSString *actionString = [NSString stringWithFormat:@"Action_%@:", actionName];
    //通过方法名字符串获取对应的sel
    SEL action = NSSelectorFromString(actionString);
    
    if (target == nil) {
        // 这里是处理无响应请求的地方之一,这个demo做得比较简单,如果没有可以响应的target,就直接return了。实际开发过程中是可以事先给一个固定的target专门用于在这个时候顶上,然后处理这种请求的
        [self NoTargetActionResponseWithTargetString:targetClassString selectorString:actionString originParams:params];
        return nil;
    }
    //是否需要缓存
    if (shouldCacheTarget) {
        [self safeSetCachedTarget:target key:targetClassString];
    }
    //是否响应sel
    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];
            @synchronized (self) {
                [self.cachedTarget removeObjectForKey:targetClassString];
            }
            return nil;
        }
    }
}
  • 进入safePerformAction:target:params:实现,主要是通过invocation进行参数传递+消息转发
- (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];
    //void类型
    if (strcmp(retType, @encode(void)) == 0) {
        ...
    }
    //...省略其他类型的判断
}
protocol class

实现思路:

  • protocol对应的类进行字典匹配
  • 通过用protocol获取class动态创建实例

protocol比较典型的三方框架就是阿里的BeeHive

组件化Beehive解耦

BeeHive 核心思想
  • 各个模块间调用从直接调用对应模块,变成调用Service的形式,避免了直接依赖
  • App生命周期的分发,将耦合在AppDelegate中逻辑拆分,每个模块以微应用的形式独立存在

示例代码

//******** 1、注册
[[BeeHive shareInstance] registerService:@protocol(HomeServiceProtocol) service:[BHViewController class]];

//******** 2、使用
#import "BHService.h"

id< HomeServiceProtocol > homeVc = [[BeeHive shareInstance] createService:@protocol(HomeServiceProtocol)];
BeeHive 模块注册

BeeHive主要是通过BHModuleManager来管理各个模块的,BHModuleManager中只会管理已经被注册过的模块

BeeHive提供了三种不同的调用形式,静态plist动态注册annotationModuleService之间没有关联,每个业务模块可以单独实现Module或者Service的功能。

  1. Annotation方式注册
    这种方式主要是通过BeeHiveMod宏进行Annotation标记
//***** 使用
BeeHiveMod(ShopModule)

//***** BeeHiveMod的宏定义
#define BeeHiveMod(name) \
class BeeHive; char * k##name##_mod BeeHiveDATA(BeehiveMods) = ""#name"";

//***** BeeHiveDATA的宏定义 
#define BeeHiveDATA(sectname) __attribute((used, section("__DATA,"#sectname" ")))

//*****  全部转换出来后为下面的格式
char * kShopModule_mod __attribute((used, section("__DATA,""BeehiveMods"" "))) = """ShopModule""";

此时Module已经被存储到Mach-O文件的特殊段中,那么如何取呢?

NSArray* BHReadConfiguration(char *sectionName,const struct mach_header *mhp)
{
    
    NSMutableArray *configs = [NSMutableArray array];
    unsigned long size = 0;
#ifndef __LP64__
    // 找到之前存储的数据段(Module找BeehiveMods段 和 Service找BeehiveServices段)的一片内存
    uintptr_t *memory = (uintptr_t*)getsectiondata(mhp, SEG_DATA, sectionName, &size);
#else
    const struct mach_header_64 *mhp64 = (const struct mach_header_64 *)mhp;
    uintptr_t *memory = (uintptr_t*)getsectiondata(mhp64, SEG_DATA, sectionName, &size);
#endif
    
    unsigned long counter = size/sizeof(void*);
    // 把特殊段里面的数据都转换成字符串存入数组中
    for(int idx = 0; idx < counter; ++idx){
        char *string = (char*)memory[idx];
        NSString *str = [NSString stringWithUTF8String:string];
        if(!str)continue;
        
        BHLog(@"config = %@", str);
        if(str) [configs addObject:str];
    }
    
    return configs; 
}

进入BHReadConfiguration方法,主要是通过Mach-O找到存储的数据段,取出放入数组中

  1. 读取本地Pilst文件
    首先设置好路径
[BHContext shareInstance].moduleConfigName = @"BeeHive.bundle/BeeHive";//可选,默认为BeeHive.bundle/BeeHive.plist

创建plist文件,Plist文件的格式也是数组中包含多个字典。字典里面有两个Key,一个是@"moduleLevel",另一个是@"moduleClass"。注意根的数组的名字叫@“moduleClasses”

image.png

进入loadLocalModules方法,主要是从Plist里面取出数组,然后把数组加入到BHModuleInfos数组里面。

  1. load方法注册
    在Load方法里面注册Module的类
+ (void)load
{
    [BeeHive registerDynamicModule:[self class]];
}
BeeHive 模块事件

BeeHive会给每个模块提供生命周期事件,用于与BeeHive宿主环境进行必要信息交互,感知模块生命周期的变化。

BeeHive各个模块会收到一些事件,在BHModuleManager中,所有的事件被定义成了BHModuleEventType枚举。如下所示,其中有2个事件很特殊,一个是BHMInitEvent,一个是BHMTearDownEvent

typedef NS_ENUM(NSInteger, BHModuleEventType)
{
    //设置Module模块
    BHMSetupEvent = 0,
    //用于初始化Module模块,例如环境判断,根据不同环境进行不同初始化
    BHMInitEvent,
    //用于拆除Module模块
    BHMTearDownEvent,
    BHMSplashEvent,
    BHMQuickActionEvent,
    BHMWillResignActiveEvent,
    BHMDidEnterBackgroundEvent,
    BHMWillEnterForegroundEvent,
    BHMDidBecomeActiveEvent,
    BHMWillTerminateEvent,
    BHMUnmountEvent,
    BHMOpenURLEvent,
    BHMDidReceiveMemoryWarningEvent,
    BHMDidFailToRegisterForRemoteNotificationsEvent,
    BHMDidRegisterForRemoteNotificationsEvent,
    BHMDidReceiveRemoteNotificationEvent,
    BHMDidReceiveLocalNotificationEvent,
    BHMWillPresentNotificationEvent,
    BHMDidReceiveNotificationResponseEvent,
    BHMWillContinueUserActivityEvent,
    BHMContinueUserActivityEvent,
    BHMDidFailToContinueUserActivityEvent,
    BHMDidUpdateUserActivityEvent,
    BHMHandleWatchKitExtensionRequestEvent,
    BHMDidCustomEvent = 1000
};

主要分为三种

  1. 系统事件:主要是指Application生命周期事件
image.png

一般的做法是AppDelegate改为继承自BHAppDelegate

@interface TestAppDelegate : BHAppDelegate 
  1. 应用事件:官方给出的流程图,其中modSetupmodInit等,可以用于编码实现各插件模块的设置与初始化
  2. 自定义事件

以上所有的事件都可以通过调用BHModuleManagertriggerEvent:来处理

- (void)triggerEvent:(NSInteger)eventType
{
    [self triggerEvent:eventType withCustomParam:nil];
    
}

- (void)triggerEvent:(NSInteger)eventType
     withCustomParam:(NSDictionary *)customParam {
    [self handleModuleEvent:eventType forTarget:nil withCustomParam:customParam];
}

#pragma mark - module protocol
- (void)handleModuleEvent:(NSInteger)eventType
                forTarget:(id)target
          withCustomParam:(NSDictionary *)customParam
{
    switch (eventType) {
            //初始化事件
        case BHMInitEvent:
            //special
            [self handleModulesInitEventForTarget:nil withCustomParam :customParam];
            break;
            //析构事件
        case BHMTearDownEvent:
            //special
            [self handleModulesTearDownEventForTarget:nil withCustomParam:customParam];
            break;
            //其他3类事件
        default: {
            NSString *selectorStr = [self.BHSelectorByEvent objectForKey:@(eventType)];
            [self handleModuleEvent:eventType forTarget:nil withSeletorStr:selectorStr andCustomParam:customParam];
        }
            break;
    }
}
BeeHive模块调用

BeeHive中是通过BHServiceManager来管理各个Protocol的,BHServiceManager中只会管理已经被注册过的Protocol

注册Protocol的方式总共有三种,和注册Module是一样一一对应的

  1. Annotation方式注册
//****** 1、通过BeeHiveService宏进行Annotation标记
BeeHiveService(HomeServiceProtocol,BHViewController)

//****** 2、宏定义
#define BeeHiveService(servicename,impl) \
class BeeHive; char * k##servicename##_service BeeHiveDATA(BeehiveServices) = "{ \""#servicename"\" : \""#impl"\"}";

//****** 3、转换后的格式,也是将其存储到特殊的段
char * kHomeServiceProtocol_service __attribute((used, section("__DATA,""BeehiveServices"" "))) = "{ \"""HomeServiceProtocol""\" : \"""BHViewController""\"}";
  1. 读取本地plist文件
    首先同Module一样,先设置好路径
[BHContext shareInstance].serviceConfigName = @"BeeHive.bundle/BHService";

设置plist文件

截屏2021-11-07 下午3.31.15.png

同样也是在setContext时注册services

//加载services
-(void)loadStaticServices
{
    [BHServiceManager sharedManager].enableException = self.enableException;
    
    [[BHServiceManager sharedManager] registerLocalServices];
    
}

- (void)registerLocalServices
{
    NSString *serviceConfigName = [BHContext shareInstance].serviceConfigName;
    //获取plist文件路径
    NSString *plistPath = [[NSBundle mainBundle] pathForResource:serviceConfigName ofType:@"plist"];
    if (!plistPath) {
        return;
    }
    
    NSArray *serviceList = [[NSArray alloc] initWithContentsOfFile:plistPath];
    
    [self.lock lock];
    //遍历并存储到allServicesDict中
    for (NSDictionary *dict in serviceList) {
        NSString *protocolKey = [dict objectForKey:@"service"];
        NSString *protocolImplClass = [dict objectForKey:@"impl"];
        if (protocolKey.length > 0 && protocolImplClass.length > 0) {
            [self.allServicesDict addEntriesFromDictionary:@{protocolKey:protocolImplClass}];
        }
    }
    [self.lock unlock];
}
  1. load方法注册
    Load方法里面注册Protocol协议,主要是调用BeeHive里面的registerService:service:完成protocol的注册
+ (void)load
{
   [[BeeHive shareInstance] registerService:@protocol(UserTrackServiceProtocol) service:[BHUserTrackViewController class]];
}

- (void)registerService:(Protocol *)proto service:(Class) serviceClass
{
    [[BHServiceManager sharedManager] registerService:proto implClass:serviceClass];
}
Module & Protocol

这里简单总结下:

  • 对于Module:数组存储
  • 对于Protocol:通过字典将protocol与类进行绑定,key为protocol,value为 serviceImp即类名

你可能感兴趣的:(组件化)