从头说起:
最近由于项目进行到了一定的阶段,项目中的代码到达了一定的体积与当量(主要是业务越来越多),这个时间分层使用MVC,MVVM等来分层已经力不从心,不是说MVC不行而是我认为MVC分层是一种粒度相对于较小的分层,相对于模块化要站在一个更大更宏观的角度来进行项目架构的调整所以这次一定要从项目全局宏观的角度出发。
什么是模块化,组件化,插件化
其实这个概念网上比较多,我可以简单的通俗的说一下这三个的概念。
插件化:
插件化开发是将一个项目app拆分成多个模块,这些模块包括宿主和插件。每个模块相当于一个apk,最终发布的时候将宿主apk和插件apk单独打包或者联合打包。
作用:每个组负责一个插件,彼此之间没有过多的依赖,可以单独调试打包,有时发版其实就相当于发插件,最重要的是可以动态下载插件更新插件。
模块化:
组件化开发是将一个项目app拆分成多个模块,模块化开发过程中相互依赖或单独调试,最终发布的时候是将这些模块合并统一成一个apk,另外模块化也是插件化的前提
组件化:是一种细粒度更小的结构方式多见于项目中一个组件化控件例如:自定义Button按钮,这种组件更多是为了在项目中的复用以及方便开发管理。
不知道我说的是否清楚,如果不清楚的可以在下面留言,下面接着说我在App模块化摸索的过程。
模块化主要需要解决以下几个问题:
1. 多个模块的跳转怎么解决
2. 模块化里面的传值问题怎么解决
3. 模块化里面的方法互相怎么调用
4. 模块化里面的响应式事件怎么处理
我们一个一个来解决,第一个问题和第二个问题可以归为一类就是模块跳转的问题,首先我先了解到了网路上开源的比较火的MGJRouter框架来解决这个问题
源码非常简单就不详细说了,大概就是用一个URL表示来表示一个模块,在这个模块初始化的时候在ViewController 里面的+ (void)load 方法里面进行模块URL的注册,注册里面有一个block保存了这个如果有人调用这个URL找到了这个模块要做的事情,里面所有的URL和对应的block保存在一个NSMutableDictionary里面,调用者利用URL来进行调用,这样就做到了模块直接的 直接引用从而解决了互相引用的问题,传参的问题很简单如果是简单的参数可以直接在URL后面加上?key=value 的形式,复杂的参数可以用一个NSDictionary来包装传递
image.png因为MGJRouter的Demo太简单了只是做了模块化项目Demo,项目结构没有模块化,我已经弄了个Demo把模块化项目里面集成了MGJRouter来实现模块
这个项目基于workspace 里面包含4个模块App模块只是一个壳,还有一个公共的模块MyUtil,一个业务模块MyCompont1,一个Pods模块,其他模块编译为.a库与Bundle包提供给App主模块调用,里面包括包括了一些简单的调用都在里面了
话说,当我准备再查找一个类似Android里面的EventBus来解决第3,4问题的时候(你别说我还真找到了一个开源的叫做IMXEventBus的框架实现原理也非常简单,不过他这个框架有些比较大的问题就是对同一个对象注册同一个级别的事件只能注册一次以后有机会再分享这个框架)我突然发现了一个链接,一个由有赞技术团队分享的他们的开源项目 Bifrost 的链接里面包括对于模块化架构不断的摸索以及项目调整,最终形成了一个轻量化的项目开源出来:
https://tech.youzan.com/you-zan-ioszu-jian-hua-jia-gou-she-ji-shi-jian/
里面有他们的一些沉淀以及思考,最重要的我们的4个问题他这里面都解决了,确切的说是解决前3个,最后一个是利用NSNotificationCenter来解决的,而且他这个Demo里面不是简配版的🤣,里面项目结构也模块化了
源码实现非常之简单其中Route功能和上面MGJRouter介绍的差不多,重点介绍下里面MGJRouter没有的功能:
就是关于上面提到的第3点,模块里面的服务怎么调用的实现,Bifrost里面是抽象出了一个中间层的模块叫做Mediator来实现,里面包含了每个独立的模块层对外需要提供的服务接口的头文件例如 GoodsModuleService里面的头文件:
#ifndef GoodsModuleService_h
#define GoodsModuleService_h
///<v1.0>
/**
service protocol头文件版本号基于Semantic Versioning
x(major).y(minor)
major - 公共API改动或者删减
minor - 新添加了公共API
小于1.0的版本(如0.6),视为未稳定版本,不做上述限制。
*/
#import "BifrostHeader.h"
NS_ASSUME_NONNULL_BEGIN
#pragma mark - Notifications
//static NSNotificationName kNotification*** = @"kNotification***";
#pragma mark - URL routers
static NSString *const kRouteAllGoodsList = @"//goods/all_goods_list";
static NSString *const kRouteGoodsDetail = @"//goods/detail";
static NSString *const kRouteGoodsDetailParamId = @"id";
#pragma mark - Model Protocols
@protocol GoodsProtocol <NSObject>
- (NSString*)goodsId;
- (NSString*)name;
- (CGFloat)price;
- (NSInteger)inventory;
@end
#pragma mark - Module Protocol
/**
The services provided by goods module to other modules
*/
@protocol GoodsModuleService <NSObject>
- (NSInteger)totalInventory;
- (NSArray<id<GoodsProtocol>>*)popularGoodsList; //热卖商品
- (NSArray<id<GoodsProtocol>>*)allGoodsList; //所有商品
- (id<GoodsProtocol>)goodsById:(nonnull NSString*)goodsId;
@end
NS_ASSUME_NONNULL_END
#endif /* GoodsModuleService_h */
第一行是通知的变量的定义(如果需要的话):
//static NSNotificationName kNotification*** = @"kNotification***";
接下来前面2个变量是URL routers 代表这个模块业务里面的暴露出去的调转页面的URL:
static NSString *const kRouteAllGoodsList = @"//goods/all_goods_list";
static NSString *const kRouteGoodsDetail = @"//goods/detail";
接下来是如果页面跳转可能会用到的传参的Key的名字:
static NSString *const kRouteGoodsDetailParamId = @"id";
例如:
NSString *routeURL = BFStr(@"%@?%@=%@", kRouteGoodsDetail, kRouteGoodsDetailParamId, goods.goodsId);
"//goods/detail?id=1" //相当于转换为这个URL字符串
后面是Model Protocol,业务模块比如GoodsModel要继承这个Protocol并且实现他,GoodsModel.h如下:
#import <Foundation/Foundation.h>
#import "GoodsModuleService.h"
@interface GoodsModel : NSObject<GoodsProtocol>
@property(nonatomic, strong) NSString *goodsId;
@property(nonatomic, strong) NSString *name;
@property(nonatomic, assign) CGFloat price;
@property(nonatomic, assign) NSInteger inventory;
@end
这里的实现很有意思,利用了 @property 属性来自动帮做这个事情
这个Protocols是用来作为Model用的(为什么Protocol里面不是申明所有属性而是get方法呢,可能是由于OC里面的Protocol一般来是用作方法申明的很少用作属性,我把get方法全部改为属性是完全可以编译运行的),然后下面是暴露的业务GoodsModuleService的protocol:
@protocol GoodsModuleService <NSObject>
- (NSInteger)totalInventory;
- (NSArray<id<GoodsProtocol>>*)popularGoodsList; //热卖商品
- (NSArray<id<GoodsProtocol>>*)allGoodsList; //所有商品
- (id<GoodsProtocol>)goodsById:(nonnull NSString*)goodsId;
@end
这里面是申明了要暴露给外部模块的方法名,GoodsModule业务模块必须实现这个GoodsModuleService的protocol来实现服务,GoodsModule.h文件如下:
#import <Foundation/Foundation.h>
#import "GoodsModuleService.h"
@interface GoodsModule : NSObject<BifrostModuleProtocol, GoodsModuleService>
@end
其他业务模块比如HomeMoule只要引用Mediator模块里面的GoodsModuleService就可以了,调用的时候 :
NSArray *list = [BFModule(GoodsModuleService) popularGoodsList];
这个方式来进行调用,BFModule(GoodsModuleService)会去找到运行期注册的
GoodsModuleService的对象,而GoodsModuleService的注册则在其服务的实现类GoodsModule一开始中注册好了
@implementation GoodsModule
+ (void)load {
BFRegister(GoodsModuleService);
}
然后调用它的popularGoodsList方法,访问Model属性也非常简单:
for (id<GoodsProtocol> goods in list) {
YZSTableViewCellModel *cellModel = [[YZSTableViewCellModel alloc] init];
[sectionModel.cellModelArray addObject:cellModel];
cellModel.height = 44;
cellModel.renderBlock = ^UITableViewCell * _Nonnull(NSIndexPath * _Nonnull indexPath, UITableView * _Nonnull tableView) {
YZStrong(self)
UITableViewCell *cell = [self reusableCellWithId:CellIdentifier inView:tableView];
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
NSString *text = BFStr(@"%@ : ¥%.2f", goods.name, goods.price);
cell.textLabel.text = text;
return cell;
};
}
直接把list转换为id<GoodsProtocol>,然后访问里面的属性这里面通过OC的点语法来进行访问刚好匹配到GoodsProtocol里面申明的方法
这样架构就会是这个样子的:GoodsModule模块单向依赖于Mediator模块,而其他业务模块也单向依赖于Mediator模块,但是各个业务模块之间没有依赖关系
最后面提一下的就是KNotification通知变量也定义在这个里面,如果需要监听注册的功能可以通过这个定义的变量去发送通知,这样的话所有GoodsModule模块对外业务相关的信息都定义在了Mediator模块里面的GoodsModuleService的.h头文件里面,整个项目比较工整
结尾要说的话:其实从2017年下半年网络上很多公司都开源了自己的模块化框架,拿过来用的同时一定要做到 “因地适宜” ,要根据自己项目的实际情况来进行选择和改进,另外我认为一个架构不可能从一开始就完全的想得面面俱到,而是通过业务不断调整来进行慢慢优化的,所以大家在开始转 “模块化” 架构的时候可以一开始把大的方向都确定然后着手进行修改,不需要一开始就局限于一些细节导致项目被动
PS:关于《Android App模块化》的文章大家出门转就可以看到了
😁最后面大家希望喜欢的话可以给我留言点赞···
网友评论