iOS开发进阶APP & program

iOS开发进阶:工程组件化实践

2022-01-08  本文已影响0人  __Null

当一个App聚合的业务较多时,或者团队开发成员较多的时候,实际开发中总会遇到一些问题:比如提交的代码冲突了,比如相同的功能写重复了,比如代码之间相互引用啊,那么工程组件化就很有必要。

那么组件化到底可以帮助我们解决什么问题呢?1.模块间解耦;2.模块重用;3.提高团队的协作开发效率;4.易于测试和排查问题。从计数角度来说最重要的就是模块之间解耦和代码的复用。

并不是所有的项目都需要组件化,如果你的项目较小,模块间交互简单,耦合少;模块没有被多个外部模块引用,只是一个单独的小模块;模块不需要重用,代码很少修改;团队规模很小。那么,项目是不需要组件化的。

一、组件的层级划分

基于如上所说的模块解耦和代码复用的目的,我们该怎么去设计一个组件呢?
首先组件分层的思路很重要,这里推荐将组件部分分为3层,加上主App,一共4层,如图所示:


注意事项:

二、组件的创建

如下实际操作部分,我们以一个包含图片编辑、视频编辑、用户三个主业务需求的App为基础,包含基础组件NXKit,公共组件NXUINXService,业务组件ComponentImageComponentVideoComponentOwner,这些组件同在一个名为Component-based-App的目录下,表示一个组件化的工程,完整目录如下:

组件的创建命令pod lib create 组件名称,然后按照提示选择组件的语言Swift/ObjC、是否需要样例等等可以根据提示和自己的需求来创建,创建完成后会自动用Xcode打开。创建组件可以参考如何制作 Cocoapods 库

1.创建基础组件
pod lib create NXKit
2.创建公共组件
pod lib create NXUI
pod lib create NXService
//在NXUI.podspec中添加依赖
s.dependency 'NXKit'
s.prefix_header_contents = '#import "NXKit.h"'
//在Podfile中配置路径
pod 'NXKit', :path => '../../NXKit'

如上pod install之后就可以在组件内部访问到下层的基础组件了。

3.创建业务组件
pod lib create ComponentImage
pod lib create ComponentVideo
pod lib create ComponentOwner
//在ComponentImage.podspec中添加依赖
s.dependency 'NXUI'
s.dependency 'NXService'
s.dependency 'NXKit'
s.prefix_header_contents = '#import "NXUI.h"','#import "NXService.h"','#import "NXKit.h"'
//Podfile中配置路径
pod 'NXUI', :path => '../../NXUI'
pod 'NXService', :path => '../../NXService'
pod 'NXKit', :path => '../../NXKit'

如上pod install之后就可以在组件内部访问到下层的基础组件、和公共组件了。

4.创建宿主App
pod 'ComponentVideo', :path => '../ComponentVideo'
pod 'ComponentImage', :path => '../ComponentImage'
pod 'ComponentOwner', :path => '../ComponentOwner'
  
pod 'NXUI', :path => '../NXUI'
pod 'NXService', :path => '../NXService'
pod 'NXKit', :path => '../NXKit'
@import ComponentVideo;
@import ComponentImage;
@import ComponentOwner;
@import NXUI;
@import NXService;
@import NXKit;
#import <UIKit/UIKit.h>
@interface EMMasterViewController : NXViewController
@end

#import "EMMasterViewController.h"

@implementation EMMasterViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.naviView.title = @"App/EMMasterViewController";
    
    NSArray *items = @[@"ComponentImage",@"ComponentImage",@"ComponentOwner"];
    for (NSInteger i = 0; i < items.count; i++) {
        UIButton *testView = [[UIButton alloc] initWithFrame:CGRectMake(0, 60*i, NX.width, 59)];
        testView.tag = I;
        testView.backgroundColor = [NX red:255 green:0 blue:0];
        [testView setTitle:items[i] forState:UIControlStateNormal];
        [testView addTarget:self action:@selector(testViewAction:) forControlEvents:UIControlEventTouchUpInside];
        [self.contentView addSubview:testView];
    }
}

- (void)testViewAction:(UIButton *)sender{
    if(sender.tag == 0){
        CIMasterViewController *vc = [[CIMasterViewController alloc] init];
        vc.title = @"App/CIMasterViewController";
        [self.navigationController pushViewController:vc animated:YES];
    }
    else if(sender.tag == 1){
        CVMasterViewController *vc = [[CVMasterViewController alloc] init];
        vc.title = @"App/CVMasterViewController";
        [self.navigationController pushViewController:vc animated:YES];
    }
    else if(sender.tag == 2){
        COMasterViewController *vc = [[COMasterViewController alloc] init];
        vc.title = @"App/COMasterViewController";
        [self.navigationController pushViewController:vc animated:YES];
    }
}
@end

到此一个组件化的工程已经搭建起来了,我们可以在App层调用各个组件层的代码了,能进行页面的Push和Pop了,更多细节问题还需要进一步完善。

三、组件中资源的引用

这部分我们以NXUI设置自定义导航栏左上角返回按钮为例。

1.podspec配置

将图片拷贝到NXUI/Assets目录下,打开NXUI.podspec中的s.resource_bundles = { 'NXUI' => ['NXUI/Assets/*.png']},再执行pod install,可以看到图片出现在了Pod组件中。

2.设置NXNaviView的backBar的图片
//这样设置未生效
[self.backBar setImage:[UIImage imageNamed:@"navi-back.png"] forState:UIControlStateNormal];
3.查看图片所在的位置

点击组件的Products目录,看到


4.动态获取图片路径

点开NXUI.bundle后发现图片在这里,两个Bundle中的内容相同。
NSBundle有根据一个类确定这个类所在的Bundle,这里通过NXUI这个类确定当前NXUI.framework所在的路径,图片的完整路径在这个路径后面拼接上/NXUI.bundle/${NAME}.png即可

@interface NXUI : NSObject
@property (nonatomic, class, readonly) NSString *path;
@end

@implementation NXUI
+ (NSString *)path {
    return [NSBundle bundleForClass:NXUI.self].resourcePath;
}
@end
//然后图片的路径只需要再拼接上NXUI.bundle/navi-back.png即可
NSString *name = [NSString stringWithFormat:@"%@/NXUI.bundle/navi-back.png", NXUI.path];
[self.backBar addTarget:self action:@selector(backBarAction) forControlEvents:UIControlEventTouchUpInside];

如上操作图片就被加载出来。(这里试了下访问NXUI.framework同级的NXUI.bundle没有生效,可能是不支持../)

5.封装该组件图片访问方法
@implementation NXUI
+ (nullable UIImage *) image:(NSString *)name {
    NSString *path = [NSString stringWithFormat:@"%@/NXUI.bundle/%@", NXUI.path, name];
    return [UIImage imageNamed:path];
}
@end

外部则可以通过[NXUI image:@"navi-back.png"]访问该组件内的图片。

6.json等文件的访问方式

注意需要将NXUI.podspec中设置为s.resource_bundles = { 'NXUI' => ['NXUI/Assets/*']},然后执行pod install
封装访问文件的方法:

@implementation NXUI
+ (nullable NSData *)file:(NSString *)name{
    NSString *path = [NSString stringWithFormat:@"%@/NXUI.bundle/%@", NXUI.path, name];
    return [NSData dataWithContentsOfFile:path];
}
@end
7.nib文件的访问
@implementation NXUI
+ (nullable UINib *)nib:(NSString *)name {
    NSString *path = [NSString stringWithFormat:@"%@/NXUI.bundle", NXUI.path];
    return [UINib nibWithNibName:name bundle:[NSBundle bundleWithPath:path]];
}
@end

四、组件之间的通信

在上面第二部分,我们详细阐述了基础组件、公共组件、以及业务组件之间在纵向上遵从上层依赖下层的的原则。那么对于复杂的项目业务组件与业余组件之间的横向依赖关系也是存在的,那么对于这部分该如何解决呢?

对于解决横向依赖的关系,业界用的较多的有CTMediator、BeeHive等通信方案。

1. CTMediator

CTMediator如上图所示,接下来我用文字阐述他们之间的关系:
2. BeeHive

BeeHive也是一个使用很好的模块化解耦的框架,他维护了BHContextBHModuleManager类和BHServiceManager三个重要的类。

BHContext
BHModuleManager
BHServiceManager

针对如上几个类我觉得BHModuleManager管理的模块类做事件分发的思路是非常值得借鉴的,他是一种去中心化的订阅机制,平等的发送/接收消息内容。而BHServiceManager管理的服务使用的时候需要根据协议类BHServiceProtocol去创建协议实现的类的实例,不太直观,对于BHServiceProtocol子类的引用,还是存在一定程度的耦合。

3.MGJRouter

MGJRouter是一种基于URL的路由器,简单来说:

mgj://category/travel       ==> [mgj, ~, category, travel]
mgj://category                 ==> [mgj, ~, category]
mgj://service/library        ==> [mgj, ~, service, library]
{
    "mgj" : {
        "~" : {
            "category":{
                "_":block2,
                "travel": {
                    "_": block1,
                },
            },
            "service":{
               "library":{
                  "_":block3
              }
           }
        }
    }
}

2.匹配的过程中,通过extractParametersFromURL找到匹配的节点,同样的从根节点开始一级往下匹配。找到后会继续向下找。原则上匹配到之后就会拿到{ "_": block,}对象。上层在通过这里的block回调即可实现通信。
3.项目中使用的时候可以在各个业务模块实现MGJRouter(X),实现 [registerURLPattern:toHandler:]handler
4.整个路由的解支持中英文,支持/:query模式的参数匹配,是一种非常优秀的方案。
5.匹配灵活高效。

因为维护的路楼全部都是字符串所以在排查错误的时候可能就没有那么容易,如果项目较大,团队成员较多的话需要有比较规范的技术文档做参考。根据原作者的调研还有几款与之类似的路有解决方案JLRoutes, HHRouter 可以参考。

模块化带来的是业务代码的解耦,这就引发了模块之前的通信问题,能把这两个问题一起解决好的项目才是真正组件化的项目。

补充:MVC、MVP、MVVM、VIPER架构模式

iOS开发中,常见的需求主要是由一个个页面UIViewController承载的,如何组织好一个页面的各个部分的代码,也是很有讲究的。MVC、MVP、MVVM以及VIPER是iOS开发中常用的架构单个页面的方式。

MVC架构

在各种架构中,最常见的是MVC,它把代码分成三部分:

在iOS中为了简化开发流程,提高开发效率,iOS 中的 UIViewController控制层已经包含了根视图self.view,所以很多初级开发人员就直接在控制层将视图层的初始化、数据显示的逻辑全部放在这里,这是不对的,这也是很多人吐槽控制层是MassiveViewController的原因。接下来我以一个具体的案例说明这三部的代码该如何组织:

案例说明:页面展示一组地址列表和一组公司列表。点击地址条目和公司条目上的删除按钮,可以删除对应的条目;点击地址条目和公司条目跳转到对应的详情页。

iOS中的MVC

很多人吐槽MVC生产MassiveViewController,实际上只要把模型层和视图层的责任定义清楚,控制层的代码数量是可控的,MVC已经可以应付绝大多数的产品需求了。

MVP架构

对于非常复杂的页面,也就是交互多、状态多、网络请求多的情况,实际上在单一的UIViewController中,代码量还是可能上去的,MVP架构就是对MVC应对复杂页面的优化,它将原有控制层逻辑处理单独抽象出来一个展示层,弱化UIViewController的概念。展示层持有模型层和视图层,它本身被UIViewController持有。

iOS中的MVP
其他各层的职责参考MVC章节中,如果页面足够大一个控制器对应多个展示层也是可能的。

以上所有代码可以参考项目代码

上一篇 下一篇

猜你喜欢

热点阅读