组件化

作者: Mjs | 来源:发表于2020-12-03 11:03 被阅读0次

为什么需要组件化

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

什么情况不需要组件化

  • 项⽬较⼩,模块间交互简单,耦合少
  • 模块没有被多个外部模块引⽤,只是⼀个单独的⼩模块
  • 模块不需要重⽤,代码也很少被修改
  • 团队规模很⼩
  • 只能上层对下层依赖
  • 项⽬公共代码资源 下沉
  • 横向的依赖 最好下沉

CocoaPods

组件开发完成,会上传Spec到这里,包含着所有的版本信息,然后通过这么文件找到地址信息。我们在本地都有个隐藏文件,可以看到本地的Specs,例如afn的Spec信息/Users/xxx/.cocoapods/repos/trunk/Specs/a/7/5/AFNetworking


afn的spec.png CocoaPods流程.png

组件化分层

组件化分层.png

组件化之前我们需要继续解耦,模块分级.
首先创建基础模块,在项目目录下,终端

pod lib create JMacroAndCategoryModule
JMacroAndCategoryModule是你的模块名称

模板信息.png

通过这个我们可以创建一个基础模板https://github.com/CocoaPods/pod-template.git在本地。
生成的信息在 JMacroAndCategoryModule.podspec

#
# Be sure to run `pod lib lint JMacroAndCategoryModule.podspec' to ensure this is a
# valid spec before submitting.
#
# Any lines starting with a # are optional, but their use is encouraged
# To learn more about a Podspec see https://guides.cocoapods.org/syntax/podspec.html
#

Pod::Spec.new do |s|
  s.name             = 'JMacroAndCategoryModule'
  #发版版本号,每更新一次代码就改变一次版本号
  s.version          = '0.1.0'
  #一个简单的总结,随便写
  s.summary          = 'A short description of JMacroAndCategoryModule.'

# This description is used to generate tags and improve search results.
#   * Think: What does it do? Why did you write it? What is the focus?
#   * Try to keep it short, snappy and to the point.
#   * Write the description between the DESC delimiters below.
#   * Finally, don't worry about the indent, CocoaPods strips it!

#描述,随便写 但是要比 s.summary 长度长
  s.description      = <<-DESC
TODO: Add long description of the pod here.
                       DESC

  #你的 git 仓库首页的网页 url,注意并不是 https/ssh这种代码仓库地址
  s.homepage         = 'https://github.com/jscmkt/JMacroAndCategoryModule'
  # s.screenshots     = 'www.example.com/screenshots_1', 'www.example.com/screenshots_2'
  #直接写 MIT
  s.license          = { :type => 'MIT', :file => 'LICENSE' }
  #你是谁
  s.author           = { 'jscmkt' => 'xxx@qq.com' }
  #这里就是你 git 仓库的 https/ssh 地址了
  s.source           = { :git => 'https://github.com/jscmkt/JMacroAndCategoryModule.git', :tag => s.version.to_s }
  # s.social_media_url = 'https://twitter.com/<TWITTER_USERNAME>'

  s.ios.deployment_target = '8.0'
  
  #这里的文件夹下的内容就是这个 pods 被pod install 的时候会被下载下来的文件,不在这个文件夹,将不会被引用
  # Classes 目录和.podspec 目录是平级的。
  #你可以随便指定文件夹名称,只要这个文件夹是真实存在的
  #Classes/**/*.{h,m},表示 Classes 文件夹及其文件夹下的所有.h,.m 文件。
  s.source_files = 'JMacroAndCategoryModule/Classes/**/*'
  
  #资源文件地址,下面的所有.png资源都被打包成 s.name.bundle
  s.resource = ['Images/*.png','Sounds/*']
  
  #资源文件地址,和 resource 的区别是,这个属性可以指定 bundle 的名字,下面的所有.png文件都会被打包成 ABC_section.bundle
  # s.resource_bundles = {
  #   'JMacroAndCategoryModule' => ['JMacroAndCategoryModule/Assets/*.png']
  # }
  #指定公有头文件,如果没有写,那么所有 pod 中的头文件都默认公有,可以被 import。如果指定了某些头文件,那么只有这些被指定的头文件才可以被 import。
  # s.public_header_files = 'Pod/Classes/**/*.h'
  # s.frameworks = 'UIKit', 'MapKit'
  
  #这个 pods 还依赖于其他哪些 pods
  # s.dependency 'AFNetworking', '~> 2.3'
end

我们可以根据官网信息来进行配置

基础模块位置.png
头文件处理

我们把基础模块的代码放在Classes位置
进入Example终端执行pod install,更新依赖。
我们在通过pod lib create JCommonUIModule创建一个JCommonUIModule.
build一下,发现会有三方库的报错,我们在JCommonUIModule.podspec添加


  s.dependency 'AFNetworking'
  s.dependency 'Masonry'
  
  s.prefix_header_contents = '#import "Masonry.h"','#import "UIKit+AFNetworking.h"''
  

这里我们就解决了三方依赖。现在我们需要解决模块间的依赖。
我们需要给他一个查询路径,在JCommonUIModulePodfile中添加

  pod 'JMacroAndCategoryModule', :path => '../../JMacroAndCategoryModule'

资源文件处理

我们通过self.imageView.image = [UIImage imageNamed:@"share_wechat"发现无法加载资源文件。

资源文件位置.png

首先我们需要在将资源文件放在Assets文件夹中,然后在podspec中添加资源目录。

   s.resource_bundles = {
     'LGModuleTest' => ['LGModuleTest/Assets/*']
   }

执行pod install
在项目中通过资源包加载。

    NSString *bundlePath = [[NSBundle bundleForClass:[self class]].resourcePath stringByAppendingPathComponent:@"/JModuleTest.bundle"];
    NSBundle *resoure_bundle = [NSBundle bundleWithPath:bundlePath];
    
    self.imageView.image = [UIImage imageNamed:@"share_wechat" inBundle:resoure_bundle compatibleWithTraitCollection:nil];

xib和json也是这样处理

*****************xib********************
  NSString *bundlePath = [NSBundle bundleForClass:[self class]].resourcePath;
    
  [self.tableView registerNib:[UINib nibWithNibName:className bundle:[NSBundle bundleWithPath:bundlePath]] forCellReuseIdentifier:className];

*****************json********************
    NSString *bundlePath = [[NSBundle bundleForClass:[self class]].resourcePath stringByAppendingPathComponent:@"/JHomeModule.bundle"];
    NSString *path = [[NSBundle bundleWithPath:bundlePath] pathForResource:[NSString stringWithFormat:@"Home_TableView_Response_%@", channelId] ofType:@"json"];
    

中间层依赖

为了解决模块和模块间通讯,我们使用Mediator
VC1 想PUSH VC2 需要有响应+参数+回调
VC2需要有响应方法
通过runtime我们知道方法都有消息接收者和消息主体。
这个时候就需要暴露接口:target-action,VC2需要来维护他。
这个时候我们就需要MediatorMediator来帮助找target-action。

- (id)performTarget:(NSString *)targetName action:(NSString *)actionName params:(NSDictionary *)params shouldCacheTarget:(BOOL)shouldCacheTarget
{
    if (targetName == nil || actionName == nil) {
        return nil;
    }
    
    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 safeFetchCachedTarget: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 safeSetCachedTarget:target key:targetClassString];
    }

    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;
        }
    }
}

这就是Mediator主要方法。


- (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:&params atIndex:2];
        [invocation setSelector:action];
        [invocation setTarget:target];
        [invocation invoke];
        return nil;
    }
.......

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
    return [target performSelector:action withObject:params];
#pragma clang diagnostic pop
}

简单的返回值类型直接处理,其他的交给target performSelector处理.


- (UIViewController *)Action_nativeFetchDetailViewController:(NSDictionary *)params
{
    // 因为action是从属于ModuleA的,所以action直接可以使用ModuleA里的所有声明
    DemoModuleADetailViewController *viewController = [[DemoModuleADetailViewController alloc] init];
    viewController.valueLabel.text = params[@"key"];
    return viewController;
}

最后直接返回需要跳转的页面。
使用方法

//需要调用的地方
   UIViewController *vc = [[CTMediator sharedInstance] CTMediator_naviagetPlayerVC:((HomeTemplateData *)data).videoId title:((HomeTemplateData *)data).title];
    [[UIViewController currentViewController].navigationController pushViewController:vc animated:YES];

新建CTMediator分类

///处理跳转方法

#import "CTMediator+LGPlayerModuleAction.h"

NSString * const kCTMediatorTargetLGPlayerModule = @"LGPlayerModule";
NSString * const kCTMediatorActionNativeLGPlayerViewController = @"nativeLGPlayerViewController";

@implementation CTMediator (LGPlayerModuleAction)

- (UIViewController *)CTMediator_naviagetPlayerVC:(NSString *)videoID title:(NSString *)videoTitle{
    UIViewController *viewController = [self performTarget:kCTMediatorTargetLGPlayerModule
                                                    action:kCTMediatorActionNativeLGPlayerViewController
                                                    params:@{@"videoID":videoID,
                                                             @"videoTitle":videoTitle
                                                    }
                                         shouldCacheTarget:NO
                                        ];
    
    return viewController;
}

@end

整合项目

新建个工程,导入写好的模块

    pod 'JHomeModuleCategory', :path => '../JHomeModuleCategory'
    pod 'JMacroAndCategoryModule', :path => '../JMacroAndCategoryModule'
    pod 'JCommonUIModule', :path => '../JCommonUIModule'
    pod 'JPlayerModuleCategory', :path => '../JPlayerModuleCategory'
    pod 'JHomeModule', :path => '../JHomeModule'
    pod 'JPlayerModule', :path => '../JPlayerModule'
    pod 'JDLNAModule', :path => '../JDLNAModule'

调试一下就可以了。

组件化demo

相关文章

网友评论

      本文标题:组件化

      本文链接:https://www.haomeiwen.com/subject/dzeywktx.html