美文网首页iOSiOS组件化
iOSApp组件化详解(从0到1实现一个完整的组件化项目)

iOSApp组件化详解(从0到1实现一个完整的组件化项目)

作者: StevenHu_Sir | 来源:发表于2021-03-22 18:40 被阅读0次

    何为组件化

    一种能够解决代码耦合的技术。项目经过组件化的拆分,不仅可以解决代码耦合的问题,还可以增强代码的复用性,工程的易管理性,减少编译时间等

    1.组件化分层架构图

    App组件化架构分层.png

    2.架构分层详解

    1.Lib层

    基础模块跟业务无关,只定义接口和基本配置,子类可以重写,便于扩展

    • LibBase
      • LibBaseController
      • LibBaseNavController
      • ...
    • LibCommon基础公共组件
    • LibFlexBox移动端FlexBox布局

    Widget层

    • 由Lib延伸而来,便于组件的扩展和复用

    2.Mediator层

    • 采用Bifrost框架,创建调度组件并定义交互协议,处理业务模块之间的数据传递及逻辑交互处理
    • Module层必须依赖该库

    3.Module层

    业务层,跟业务相关的一些组件,业务组件之间互不依赖,且依赖于ModuleCommon层

    ModuleCommon

    跟业务相关的公共组件部分,例如

    • CommonBaseController
    • CommonLodingController
    • CommonListController
    • 数据模型Models
    • Tools工具类
    • Macro常见宏等
    • QMUI常见配置/换肤配置
    • 第三方分享/登录/支付配置

    3.组件化要点罗列

    • 多Target分模块开发,代码解耦
    • 单独编译项目
    • 组件之间传值,通过调度组件Biforst
    • 组件间访问公共图片资源,解决命名冲突
    • 文件夹分层:子组件subspec
    • pod 引入依赖方式
      • 引入本地依赖
      • 引入远程依赖
      • 引入指定分支
    • 组件命名方式
    • App路由管理
    • podspec使用及格式校验
    • 组件间依赖管理
    • coapods私有化仓库搭建

    **

    3.1多Target分模块开发,代码解耦

    workspace 依赖多个功能文件开发

    # Uncomment the next line to define a global platform for your project
    # platform :ios, '9.0'
    def commonPods()
        #基础宏定义,类别
        pod 'HKMacros', :git => 'https://gitee.com/Steven_Hu/HKMacros.git'
        # 组件化基类-引用本地依赖
        pod 'HKBaseModule', :path => './PrivateRepo/HKBaseModule/HKBaseModule.podspec'
        #pod 'HKBaseModule', :git => 'https://gitee.com/Steven_Hu/HKBaseModule.git'
    end
    
    def mediatorPods()
        #pod 'Bifrost', :path => '../'
    end
    
      # Comment the next line if you don't want to use dynamic frameworks
      #use_frameworks!
      #workspace文件名
      workspace 'HKiOSTools.xcworkspace'
      #主工程路径
      project 'HKiOSTools/HKiOSTools.xcodeproj'
    
    target 'HKiOSTools' do
        project 'HKiOSTools/HKiOSTools.xcodeproj'
        commonPods()
      
        target "HKBaseModule_Example" do
            project 'PrivateRepo/HKBaseModule/Example/HKBaseModule.xcodeproj'
            commonPods()
        end
    
    end
    

    **

    3.2单独编译项目

    如何区分你编译的是主工程项目还是子工程项目

    • 最简单的方式,通过项目名称:ProjectName

    [[[NSBundle mainBundle] infoDictionary] objectForKey:(NSString *)kCFBundleExecutableKey]

    • 判断项目名称是否和子项目名称一致即可

    3.3组件之间传值Bifrost

    image.png

    注册路由(ViewController)

    + (void)load {
        [Bifrost bindURL:kRouteHomePage
               toHandler:^id _Nullable(NSDictionary * _Nullable parameters) {
            return [HKHomeViewController new];
        }];
    }
    

    获取路由(ViewController)

    UIViewController * vc = [Bifrost handleURL:kRouteHomePage];
    [self.navigationController pushViewController:vc animated:YES];
    

    页面传值

    • 以type为例
    //1.当前页面声明一个type属性
    /// 验证类型
    @property (nonatomic, assign) HKVerifyType  type ;
    // 2.bindURL
    [Bifrost bindURL:kRouteBindPhoneNumPage toHandler:^id _Nullable(NSDictionary * _Nullable parameters) {
      HKBindPhoneNumViewController *vc = [[self alloc] init];
      vc.type = [parameters[kRouteBindPhoneNumParamType] integerValue];
      return vc;
    }];
    // 3.传值type
    NSString *routeURL = BFStr(@"%@?%@=%@", kRouteBindPhoneNumPage, kRouteBindPhoneNumParamType, @(HKVerifyTypeForgetPassword));
    UIViewController * vc = [Bifrost handleURL:routeURL]
    [self.navigationController pushViewController:vc animated:YES];
    

    页面回调处理

    //1.当前页面声明一个type属性和Block
    /// 验证类型
    @property (nonatomic, assign) HKVerifyType  type ;
    /// 完成回调
    @property (nonatomic, strong) BifrostRouteCompletion complete ;
    // 2.bindURL
    [Bifrost bindURL:kRouteBindPhoneNumPage toHandler:^id _Nullable(NSDictionary * _Nullable parameters) {
        HKBindPhoneNumViewController *vc = [[self alloc] init];
        vc.type = [parameters[kRouteBindPhoneNumParamType] integerValue];
        vc.complete = parameters[kBifrostRouteCompletion];
        return vc;
    }];
    // 3.点击事件处理
    /// 获取验证
    - (void)getVerifyCodeEvent
    {
        [self.view endEditing:YES];
        BFComplete(@{kBifrostRouteCompletion:self.complete}, @(self.type));
    }
    // 4.传值type
    UIViewController *vc = [Bifrost handleURL:kRouteBindPhoneNumPage complexParams:@{kRouteBindPhoneNumParamType:@(HKVerifyTypeForgetPassword)} completion:^(id  _Nullable result) {
        HKVerifyType type = (YNVerifyType)result;
        [HKKeyWindow hk_showWithText:HKStr(@"你点击的类型是:%@",type)];
    }];
    [self.navigationController pushViewController:vc animated:YES];
    

    3.4组件间访问公共图片资源

    主工程有一个a.png的图片,而pod库里面也有一个a.png的图片,此时就产生命名冲突了

    思路:不同的组件都有自己独立的bundle,组件内部资源提供自身的Bundle来获取,以避免资源重复

    • 本地资源如图片等存放位置(Assets文件夹,否则无法访问)
    image.png
    • 指定Bundle名称
    # resource_bundles
    s.resource_bundles = {
       'HKLibCommon' => ['HKLibCommon/**/*.{xib,jpg,gif,png,xcassets}']
      }
    
    • 解决命名冲突:resource_bundles
    • resource_bundles会自动生成bundle把资源文件打包进去

    加载本地资源方法

    工具类抽取
    #import <Foundation/Foundation.h>
    #import <UIKit/UIKit.h>
    
    @interface ModuleBundle : NSObject
    
    /*
     * 根据bundle的名称获取bundle
     */
    + (NSBundle *)bundleWithName:(NSString *)bundleName;
    
    //获取bundle 每次只要重写这个方法就可以在指定的bundle中获取对应资源
    + (NSBundle *)bundle;
    
    //根据xib文件名称获取xib文件
    + (__kindof UIView *)viewWithXibFileName:(NSString *)fileName;
    
    //根据图片名称获取图片
    + (UIImage *)imageNamed:(NSString *)imageName;
    
    //根据sb文件名称获取对应sb文件
    + (UIStoryboard *)storyboardWithName:(NSString *)storyboardName;
    
    //获取nib文件
    + (UINib *)nibWithName:(NSString *)nibName;
    
    @end
    
    #import "ModuleBundle.h"
    
    @implementation ModuleBundle
    
    + (NSBundle *)bundleWithName:(NSString *)bundleName {
        if(bundleName.length == 0) {
            return nil;
        }
        NSString *path = [[NSBundle mainBundle] pathForResource:bundleName ofType:@"bundle"];
        NSAssert([NSBundle bundleWithPath:path], @"not found bundle");
        return  [NSBundle bundleWithPath:path];
    }
    
    + (NSBundle *)bundle {
    //    NSAssert([NSBundle mainBundle], @"not found bundle");
        return [NSBundle mainBundle];
    }
    
    + (UIView *)viewWithXibFileName:(NSString *)fileName {
        NSAssert([self viewWithXibFileName:fileName inBundle:[self.class bundle]], @"not found view");
        return [self viewWithXibFileName:fileName inBundle:[self.class bundle]];
    }
    
    + (UIImage *)imageNamed:(NSString *)imageName {
        NSAssert([self imageNamed:imageName inBundle:[self.class bundle]], @"not found image");
        return [self imageNamed:imageName inBundle:[self.class bundle]];
    }
    
    + (UIStoryboard *)storyboardWithName:(NSString *)storyboardName {
        NSAssert([self storyboardWithName:storyboardName inBundle:[self.class bundle]], @"not found storyboard");
        return [self storyboardWithName:storyboardName inBundle:[self.class bundle]];
    }
    
    + (UINib *)nibWithName:(NSString *)nibName {
        NSAssert([self nibWithNibName:nibName inBundle:[self.class bundle]], @"not found nib");
        return [self nibWithNibName:nibName inBundle:[self.class bundle]];
    }
    
    #pragma mark - private
    + (UIImage *)imageNamed:(NSString *)imageName inBundle:(NSBundle *)bundle {
        if(imageName.length == 0 || !bundle) {
            return nil;
        }
        return [UIImage imageNamed:imageName inBundle:bundle compatibleWithTraitCollection:nil];
    }
    
    + (UIImage *)imageNamed:(NSString *)imageName bundleName:(NSString *)bundleName {
        return [self imageNamed:imageName inBundle:[self bundleWithName:bundleName]];
    }
    
    + (UIView *)viewWithXibFileName:(NSString *)fileName inBundle:(NSBundle *)bundle {
        if(fileName.length == 0 || !bundle) {
            return nil;
        }
        //如果没有国际化,则直接去相应内容下的文件
        UIView *xibView = [[bundle loadNibNamed:fileName owner:nil options:nil] lastObject];
        if(!xibView) {
            //文件国际化之后,所有的bundle的文件资源都在base的目录下
            xibView = [[[NSBundle bundleWithPath:[bundle pathForResource:@"Base" ofType:@"lproj"]] loadNibNamed:fileName owner:nil options:nil] lastObject];
        }
        return xibView;
    }
    
    + (UIView *)viewWithXibFileName:(NSString *)fileName bundleName:(NSString *)bundleName {
        return [self viewWithXibFileName:fileName inBundle:[self bundleWithName:bundleName]];
    }
    
    + (UIStoryboard *)storyboardWithName:(NSString *)storyboardName inBundle:(NSBundle *)bundle {
        if(storyboardName.length == 0 || !bundle) {
            return nil;
        }
        return [UIStoryboard storyboardWithName:storyboardName bundle:bundle];
    }
    
    + (UIStoryboard *)storyboardWithName:(NSString *)storyboardName bundleName:(NSString *)bundleName {
        return [self storyboardWithName:storyboardName inBundle:[self bundleWithName:bundleName]];
    }
    
    + (UINib *)nibWithNibName:(NSString *)nibName inBundle:(NSBundle *)bundle {
        if(nibName.length == 0 || !bundle ) {
            return nil;
        }
        return [UINib nibWithNibName:nibName bundle:bundle];
    }
    
    @end
    
    Bundle获取

    创建一个类CommonBundle继承自ModuleBundle,并实现以下方法

    #import "CommonBundle.h"
    @implementation CommonBundle
    + (NSBundle *)bundle{
        //TODO:注意Bundle名字需跟模块名称一致,否则会找不到path,直接Crash
        return [self.class bundleWithName:@"HKLibCommon"];
    }
    @end
    
    使用

    替换系统加载图片的方式:[UIImage imageNamed:@"navigationbar_background"]

    [CommonBundle imageNamed:@"navigationbar_background"]
    
    多个包
    spec.resource_bundles = {
        'MapBox' => ['MapView/Map/Resources/*.png'],
        'OtherResources' => ['MapView/Map/OtherResources/*.png']
      }
    

    3.5文件夹分层子组件subspec

    我们在编写podspec文件时,sourcefiles只是告诉pods你需要哪些文件是这个项目中需要的,而没有包括文件的层级结构,那么就需要我们来实现这个层级结构:subspec


    image.png

    比如这里面的每一个文件夹,就是一个子pod,这样的好处是条理清晰,而且我们可以只用你需要的功能,在编写podfile时 就可以这样写
    pod 'HKModuleModels/User' 只使用其中的一个功能。

    主podspec

    主pod可以是一个头文件,也可以具有一定的功能,我写的组件sourcefiles只是一个import子组件的头文件, sourcebundle是项目中需要的一些图片


    image.png

    编写subspec

    • 让pods支持子subspec其实很简单,只要搞清楚三件事
    1. 文件夹结构 subspec sourcefiles的路径
    2. subspec 所依赖的系统库
    3. subspec 所依赖的第三方,和其它subspec的路径
    image.png

    3.6pod 引入依赖方式

    引入本地依赖

    pod 'ManageLocalCode', :path => '../ManageLocalCode'
    pod 'BioAuthAPI', :path => '../BioAuthAPI'
    pod 'HKTool', :path => '../'
    

    远程私有库依赖:

    # 基础宏定义byStevenHu
    pod 'HKMacros', :git => 'https://gitee.com/Steven_Hu/HKMacros.git'
    

    指定分支

    # 支付代码测试
      iap_pay:
        git:
          url:  https://gitee.com/Steven_Hu/iap_pay.git
          ref: master
    

    常规依赖

    pod "AFNetworking",  "~>0.2.0"
    

    3.7 组件命名方式

    组件性质 建议名称 示例
    基础组件拆分 项目前缀Lib组件名称 HKLibBase:基类模块
    业务组件拆分 项目前缀Module组件名称 HKModuleHome:首页模块
    调度组件拆分 项目前缀Mediator组件名称 HKMediatorDriver:司机端调度组件
    Wdiget组件拆分 项目前缀Widget组件名称 HKWidgetAddressPicker:地址选择器组件
    公共组件 项目前缀_组件名称_Common HKLibCommon,HKModuleCommon

    3.8 App端路由管理

    格式

    app内链

    举例yunquedriver://home/homepage/detail?name=steven&age=18
    appscheme://moduleName/pageName/secondPageName?key1=value1&key2=value2
    其中value中有中文等特殊字符时需要进行urlEncode。

    app外链外链

    http://xxx
    https://xxx

    格式说明

    • appscheme分解
      • 项目名称
        • yunque
      • 业务侧
        • driver
        • user
    • moduleName
      • 业务模块名称
    • pageName
      • 页面名称
    • secondPage
      • 二级页面

    scheme

    云雀司机: yunquedriver
    云雀用户:yunqueuser

    模块名

    lib:工具类,跟业务无关的

    页面

    image.png

    3.9 podspec使用及格式校验

    注意事项

    • podspec版本号和git仓库代码的tag版本必须一致,否则找不到
      • 比如:pod lib 默认生成的是0.1.0,我们根据业务需要改成0.0.1
    • 索引仓库和组件仓库关联
      • cd到组件仓库目录下
      • pod repo push #{本地关联到远程仓库的名字} #{第三方私有库的podspec文件} --verbose --allow-warnings 示例: pod repo push YNSpecs YNLibDemo.podspec --verbose --allow-warnings
    • 项目引入
    # 远程私有仓库
    source 'https://gitlab.com/xxxx/xxSpecs'
    
    # 引入使用
    pod 'xxLibNetwork'
    

    3.10 组件间依赖管理

    通过 git submodule add https://gitee.com/Steven_Hu/hk-module-components.git 添加submodule
    会在git上添加一个.gitmodules 文件,可以查看各种依赖

    [submodule "PrivateRepo/hk-lib-base"]
        path = PrivateRepo/hk-lib-base
        url = https://gitee.com/Steven_Hu/hk-lib-base.git
    [submodule "PrivateRepo/hk-lib-common"]
        path = PrivateRepo/hk-lib-common
        url = https://gitee.com/Steven_Hu/hk-lib-common.git
    [submodule "PrivateRepo/hk-lib-network"]
        path = PrivateRepo/hk-lib-network
        url = https://gitee.com/Steven_Hu/hk-lib-network.git
    [submodule "PrivateRepo/hk-mediator"]
        path = PrivateRepo/hk-mediator
        url = https://gitee.com/Steven_Hu/hk-mediator.git
    [submodule "PrivateRepo/hk-lib-keyboard"]
        path = PrivateRepo/hk-lib-keyboard
        url = https://gitee.com/Steven_Hu/hk-lib-keyboard.git
    [submodule "PrivateRepo/hk-lib-avoid-app-crash"]
        path = PrivateRepo/hk-lib-avoid-app-crash
        url = https://gitee.com/Steven_Hu/hk-lib-avoid-app-crash.git
    [submodule "PrivateRepo/hk-lib-load-picture"]
        path = PrivateRepo/hk-lib-load-picture
        url = https://gitee.com/Steven_Hu/hk-lib-load-picture.git
    [submodule "PrivateRepo/hkmodule-models"]
        path = PrivateRepo/hkmodule-models
        url = https://gitee.com/Steven_Hu/hkmodule-models.git
    [submodule "PrivateRepo/hk-module-uikit"]
        path = PrivateRepo/hk-module-uikit
        url = https://gitee.com/Steven_Hu/hk-module-uikit.git
    [submodule "PrivateRepo/hk-module-components"]
        path = PrivateRepo/hk-module-components
        url = https://gitee.com/Steven_Hu/hk-module-components.git
    [submodule "PrivateRepo/hk-module-main"]
        path = PrivateRepo/hk-module-main
        url = https://gitee.com/Steven_Hu/hk-module-main.git
    
    

    最终结果如下图所示,点击跳转新的submodule仓库地址


    image.png

    本地Clone方式

    git clone https://gitee.com/Steven_Hu/hk-iostools.git
    git submodule init && git submodule update
    
    #下面这一句的效果和上面三条命令的效果是一样的,多加了个参数  `--recursive`
    git clone https://gitee.com/Steven_Hu/hk-iostools.git --recursive
    
    

    常见问题

    error: Server does not allow request for unadvertised object b22e0a5a2a8dce1d0454bdead70353bf45f33f81

    Fetched in submodule path 'PrivateRepo/hk-module-main', but it did not contain b22e0a5a2a8dce1d0454bdead70353bf45f33f81. Direct fetching of that commit failed.

    原因分析:

    未拉取到该submodule的最新代码

    解决

    git submodule foreach git checkout master
    git submodule foreach git submodule update
    

    3.11 coapods私有化仓库搭建

    准备工作

    • 安装cocoapods
    • 准备gitlab/gitee/getlab(下文统一称为gitlab)等代码仓库账号

    创建组建索引Spec仓库(用于存放组件库的索引)

    pod repo add `specFileName(给spec仓库在本地的命名)` `spec(仓库的地址)`
    实例:
    pod repo add XXSpecs https://gitee.com/Steven_Hu/objective-c-specs
    

    ~/.cocoapods/repos目录中可以看到创建的文件夹

    image.png
    • 创建组件库
      • pod lib create #{库的名称}
      • 实例 pod lib create XXLibDemo
    • 修改podspec文件
    • 将组件push到gitlab
    • 将索引仓库和组件仓库关联
    • 项目引入
    # 远程私有仓库
    source 'https://gitlab.com/xxxx/xxSpecs'
    
    # 引入使用
    pod 'xxLibNetwork'
    

    4.常见问题补充

    4.1target has transitive dependencies that include statically linked binaries:

    解决办法

    s.static_framework = true
    

    4.2CocoaPods did not set the base configuration of your project because your project already...

    使用CocoaPods安装三方库后有两个警告,如下所示:


    image.png

    解决办法:
    将第三方库 的 PROJECT → Info → Configurations 下Debug和Release下的.debug和.release选项替换为None,如下图所示:


    image.png

    然后在pod install即可

    4.3清除 CocoaPods 本地缓存

    特殊情况下,由于网络或者别的原因,通过CocoaPods下载的文件可能会有问题。
    这时候您可以删除CocoaPods的缓存(~/Library/Caches/CocoaPods/Pods/Release目录),再次导入即可。

    4.4pod spec lint编译时报error: include of non-modular header inside framework module

    解决办法

    pod lib lint --allow-warnings --use-libraries --verbose
    

    有警告⚠️可以使用-allow-warnings忽略。

    4.5推送到本地LocalRepo

    • 使用了第三方库
    pod repo push driver_spec XXLibBase.podspec --allow-warnings --verbose --use-libraries
    
    • 未使用第三方库
    pod repo push driver_spec XXLibBase.podspec --allow-warnings --verbose
    

    4.6pod lib lint 可选参数

    pod lib lint SPEC_NAME.podspec
    可选参数:
    --verbose : 显示详细信息
    --allow-warnings: 是否允许警告,用到第三方框架时,用这个参数可以屏蔽讲稿
    --fail-fast: 在出现第一个错误时就停止
    --use-libraries:如果用到的第三方库需要使用库文件的话,会用到这个参数
    --sources:如果一个库的podspec包含除了cocoapods仓库以外的其他库的引用,则需要改参数指明,用逗号分隔。
    --subspec=Name:用来校验某个子模块的情况。
    

    注意:如果库用到了第三方的话要带上 --use-libraries,否则会报错,上传不上去。

    4.7--sources 使用

    1.私有pod的验证

    使用pod spec lint去验证私有库能否通过验证时应该,应该要添加--sources选项,不然会出现找不到repo的错误。

    pod spec lint --sources='私有仓库repo地址,https://github.com/CocoaPods/Specs'
    

    2.私有库引用私有库的问题

    在私有库引用了私有库的情况下,在验证和推送私有库的情况下都要加上所有的资源地址,不然pod会默认从官方repo查询。

    pod spec lint --sources='私有仓库repo地址,https://github.com/CocoaPods/Specs'
    pod repo push 本地repo名 podspec名 --sources='私有仓库repo地址,https://github.com/CocoaPods/Specs'
    

    4.组件化案例

    1.效果图

    组件化案例.gif

    2.代码地址

    https://gitee.com/Steven_Hu/hk-iostools

    搬砖不易,转载请注明出处,谢谢!

    相关文章

      网友评论

        本文标题:iOSApp组件化详解(从0到1实现一个完整的组件化项目)

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