推荐 原创 视频 Java开发 iOS开发 前端开发 JavaScript开发 Android开发 PHP开发 数据库 开发工具 Python开发 Kotlin开发 Ruby开发 .NET开发 服务器运维 开放平台 架构师 大数据 云计算 人工智能 开发语言 其它开发
Lambda在线 > iOSTips > 驴妈妈客户端频道页模块化设计思路

驴妈妈客户端频道页模块化设计思路

iOSTips 2018-10-27

Hello ,iOSTips的读者朋友们大家好,我是来自驴妈妈的【傅说君】,喜欢一本正经的傅说八道。由于老峰已经放假出去浪了,今天的文章由我来分享,Trust me,傅说君分享的都是干货,嗯比老峰的干。

今天分享一下驴妈妈客户端频道页的模块化设计思路,国庆后会分享有关App架构系列的文章2~3篇,主要会覆盖App架构中的界面搭建、架构分层、以及模块组件化等等 ,内容着重于围绕职责讨论代码的角色划分以及设计模式对开发流程的影响(这次会附demo),我会尽量不同于过往的精品文章,深坑已经挖好,具体怎么填就看你们的热情度了,国庆后咱们周一见。

本文主要分享实现频道页模块化的大体思路, 并不涉及具体代码实现.由于各位看官老爷的"赞"真香,我最近又更新并追加了部分内容。

零、目录

全文字数: 4,156 | 预计阅读: 16分钟

  • 一、需求分析

  • 二、模块定义

  • 三、模块化设计原则

    • 3.1 面向接口

    • 3.2 数据驱动

    • 3.3 模块隔离

  • 四、模块化框架设计

    • 4.2.1 组件协议

    • 4.2.2 模块组件数据模型

    • 4.1.1 数据源协议

    • 4.1.2 模块组件管理

    • 4.1.3 数据流向

    • 4.1 数据源

    • 4.2 模块组件

    • 4.3 频道代理对象协议

    • 4.4 对象通信

    • 4.5 交互图

  • 五、小结

一、需求分析

为了满足运营同学动态配置频道页的内容排版, 以及产品同学一次开发, 各频道复用的需求, 要开发一个框架来满足以下两点:

  • 内容灵活排版: 某个频道页展示的内容及其顺序, 甚至说一个新的频道页, 皆可由运营同学在线上环境直接配置

  • 模块全局复用: 一个承载内容的 模块 开发完后, 可在全部频道页配置使用

二、模块定义

上文提及的 内容 , 就是我们各频道页看到的 模块. 不同的 模块 具有其独特的产品功能与运营目的. 

以驴妈妈首页频道为例, 如下图:

驴妈妈客户端频道页模块化设计思路

每个框所圈区域为一个独立模块. 比如:

  • banner模块(产品推荐、活动推广、广告投放等)

  • 频道入口、主题列表模块(用户分流导向)

  • 旅行头条模块(热门游记推荐)

  • ...

此外, 每个模块可以包含单个或多个不同的模块组件:

驴妈妈客户端频道页模块化设计思路

三、模块化设计原则

除了考虑SOLID(六大原则)外, 框架设计还会围绕以下三点.

3.1 面向接口

通过定义 接口(即协议) 抽象和规范框架所关心的类或事. 框架与模块间低耦合.

举个例子, 对于框架来说, 它并不关心配置数据是什么结构或如何获取, 它仅关心的是有多少个模块、每个模块在容器中所占大小以及位置等数据.

可为此定义一个数据源协议, 来规范充当框架数据源对象所必须遵循的行为. 至于数据源对象的具体类型是什么不重要, 只要遵循协议即可充当框架中的某个角色.

当然, 面向接口与面向对象并不冲突, 反而是相辅相成, 此处不展开讨论.

3.2 数据驱动

数据决定并驱动内容的展示与响应.

  • 数据决定展示内容, 即数据与内容一一对应:

    框架根据数据源提供的相关数据, 决定每个模块该创建的组件类型, 模块组件的展示大小及布局位置等.

  • 数据驱动内容变化. 关注点为数据, 而非事件. 

    举个例子, 对于框架中模块发生的任意事件, 其结果也就两种:

    驴妈妈客户端频道页模块化设计思路

    也就说框架不会去管具体发生什么事件, 只"盯着"它所关心的数据有没有变

    另外, 事件驱动中一个事件往往对应一个响应操作, 是1对1的关系. 而数据驱动可以是1对N的关系, 可能是多个事件修改同个数据.

    1. 事件发生后, 数据有变化

    2. 事件发生后, 数据无变化

3.3 模块隔离

模块间相互隔离, 模块独立自治, 其相关事务自行处理.

模块可单独开发, 注册到框架中. 模块内可自行使用MVX、VIPER等结构型设计模式(Structual Design Pattern).

此外, 模块联合开发中, 框架与模块隔离, 不产生依赖倒置. 模块依赖框架提供的功能进行开发, 而框架不能依赖模块内容, 框架仅知道我们抽象出来的接口, 接口细节由模块去实现.

通俗来说就是框架的功能开发与模块的开发是两条平行线, 模块开发过程中, 不需要向框架中引入模块的头文件. 

这一块的具体解耦会下一章节的中的 接口定义以及 模块管理 有所体现.

四、模块化框架设计

以iOS平台举例, 阐述对整个框架的具体设计. 抛开Android和iOS平台系统编码的风格习惯和具体实现上存在的不同, 整体思想大同小异.

4.1 数据源

一个频道页由若干个模块组成, 一个模块包含1个或多个不同的组件. 框架根据数据源提供的信息, 创建和安置模块组件. 

4.1.1 数据源协议

模块数据源协议: 主要向框架提供某个模块包含的组件信息、相关的布局信息、以及组件填充数据的内容等等

typedef NSObject<LVTSectionDataSource> LVTSectionData;

@protocol LVTSectionDataSource <NSObject>
#pragma mark - 合法性检测/** SectionData检测数据是否合法(比如tabData为空当做不合法). 返回NO, 则会剔除掉该模块. */

- (BOOL)isValid;

#pragma mark - 模块包含组件信息/** 悬浮Header类对象 */

- (nullable LVTFloatViewClass)floatViewClass;

/** 模块Section Header类对象 */

- (LVTemplateClass)headerClass;

/** 该模块中具体位置的Cell类对象, 可重写该方法以返回不同的Cell类型. */

- (LVTemplateClass)cellClassAtIndex:(NSUInteger)index;

/** H5组件Url. 样式频繁更新的内容未来可拓展使用RN */

- (NSString *)h5BackUrl;

#pragma mark - 布局信息

/** 是否隐藏整个模块(Header、Cell、Inset统统隐藏). 比如异步请求数据前不展示该模块时, 返回YES */
- (BOOL)hidden;

/** 元素数量 */
- (NSUInteger)numOfItems;

/** 对应模块对应h5的margin, 可为负数 */

- (UIEdgeInsets)sectionInset;

/** 两个元素之间的水平间隔 */

- (CGFloat)itemSpace;

/** 两个元素之间的垂直间隔. 行间隔 */

- (CGFloat)lineSpace;

/** 某个位置上的元素大小. 传参容器Size供计算参考 */

- (CGSize)itemSizeAtIndex:(NSUInteger)index withContainerSize:(CGSize)size;

/** SectionHeader的高度. (宽度一定会为容器宽度, 故只需要返回高度) */

- (CGFloat)headerHeight;

/** Section拥有的悬浮View高度 */

- (CGFloat)floatViewHeight;

/** 某个位置上的元素分隔样式, 只有分隔线支持具体元素是否展示. (默认分隔样式位于元素底部) */

- (LVTSeparatorType)separatorTypeAtIndex:(NSUInteger)index;

/** 某个位置上的元素的分隔线样式为线时, 水平边缘间距 */

- (LVTLineMargin)separatorLineMarginAtIndex:(NSUInteger)index;

#pragma mark - 模型获取/** 整个模块的model */

- (LVTemplateData *)templateData;

/** 模块中某个位置对应的模型. 不限死, 可创建不同的填充数据类型, 比如智能货架的为LVTabData */

- (nullable LVTItemModel *)itemModelAtIndex:(NSUInteger)index;

#pragma mark - 自定义数据请求/** 当频道总接口请求响应后(即确定有哪些模块及顺序), 通过该方法提供机会给模块SectionData发起自定义数据请求 */
- (void)requestSectionCustomData;

#pragma mark -/** 当前频道模块组件缓存工具 */
- (void)setCacheUtil:(LVTCacheUtil *)cacheUtil;

/** 数据源代理对象, 默认为遵循频道页数据源协议的对象 */
- (void)setDelegate:(id<LVTSectionDataSourceDelegate>)delegate;

@end

频道页数据源协议: 主要向框架提供整个频道拥有的模块总数, 以及各模块的局部数据源

@protocol LVTPageDataSource <NSObject>

/** 模块的数量 */
- (NSUInteger)numberOfSections;

/** 对应SectionIndex的模块数据源对象 */
- (LVTSectionData *)sectionDataAt:(NSUInteger)section;

/** 获取频道页数据. 这个为举例用, 实际以业务需求为准. */
- (void)fetchPageDataWithCompletedBlock:(void (^)(NSError _Nullable *error))completedBlk;

@end

4.1.2 模块组件管理

对于模块内的任意组件, 都有对应一个标识ID. 我们通过一个配置文件来维护标识与组件的对应关系. 联合开发时, 每开发好一个新的组件, 就只用修改配置文件, 不动框架代码.

配置的JSON结构大致如下:

// 部分举例

{
   "header": {
       "header1": "LVTXXXHeader", // value为具体类名        "header2": "LVTXXXHeader",
       ...    },
   "cell": {
       "cell1": "LVTXXXCell",
       ...    },
 ...
}

通过ID我们可获得一个具体的类名, 再使用反射获得类对象以供框架创建组件实例. 

在iOS上我们通过一个ClassMapper来专门维护对应关系, 如下图.

驴妈妈客户端频道页模块化设计思路

上图类名仅为更好的表达Mapper的职责, 实际ClassMapper返回的类对象会使用泛型来进行解耦, ClassMapper中也不要引入任何组件的头文件.

4.1.3 数据流向

从原始数据到呈现到屏幕上的每个模块组件, 数据流向如下图所示:

驴妈妈客户端频道页模块化设计思路

上图各元素代表:
LVTPageDataSource 为遵循频道数据源协议的对象
LVTSectionData 为遵循模块数据源协议的对象
ClassMapper 为管理对应关系的对象
LVTCellXXX、LVTHeaderXXX为组件等

4.2 模块组件

组件是模块化框架中复用的基础元素. 

4.2.1 组件协议

模块组件分为可复用与不可复用两类, 分别对应以下协议:

复用组件协议: 提供组件用于复用队列的复用Id、用于布局的元素大小等

typedef Class<LVTReuseItemProtocol> LVTemplateClass;
typedef UICollectionViewCell<LVTReuseItemProtocol> LVTemplateCell;
typedef UICollectionReusableView<LVTReuseItemProtocol> LVTemplateReuseView;
typedef WKWebView<LVTReuseItemProtocol> LVTemplateWebView;

@protocol LVTReuseItemProtocol <NSObject>/** 可复用组件的ID. 默认实现为 className_ID */

+ (NSString *)tIdentifier;/**
根据Model计算复用组件的大小. 若高度固定, 则直接返回(容器宽度, 固定高度)即可 @param model id<LVTItemModelProtocol> 遵循该协议的模型对象 @param size 容器CollectionView的大小, 用于均分计算*/
+ (CGSize)itemSizeWithModel:(LVTItemModel *)model andContainerSize:(CGSize)size;
#pragma mark - 配置/** 根据传入Model配置组件内容. 会持有传入model. 子类实现需先调用super方法. 子类在该方法中进行数据填充, 以及通过事件中心进行 相关的事件注册*/
- (void)configItemWithModel:(LVTItemModel *)model;
/** 设置事件中心 */
- (void)setEventCenter:(id<LVTEventCenterProtocol>)center;
/** 当前频道模块组件缓存工具 */
- (void)setCacheUtil:(LVTCacheUtil *)util;

@optional

/** 不同于系统的prepareForReuse(在复用队列dequeue时才调用), 该方法会在入队时调用 */

- (void)itemPrepareForReuse;
// ---------- 以下方法仅需要BaseCell实现 ----------/** 设置组件在Section中的Index序号 */

- (void)setIndex:(NSUInteger)index;/** 设置分割线是否隐藏 */

- (void)setSeparatorHidden:(BOOL)hidden;/** 设置分割线水平边距 */

- (void)setSeparatorLineMargin:(LVTLineMargin)margin;
@end

不可复用的悬浮组件协议: 提供视图高度, 悬浮定位信息等

typedef Class<LVTFloatViewProtocol> LVTFloatViewClass;

@protocol LVTFloatViewProtocol <NSObject>
/** 与所处Section的顶部间距. 默认为0. 未来看需求可开放左右间隔等 */

+ (CGFloat)topInSection;

/** 视图高度. */

+ (CGFloat)viewHeight;

/** 根据传入Model配置组件内容. 通过事件中心注册感兴趣的事件等. */

- (void)configItemWithModel:(LVTItemModel *)model;

/** 设置事件中心 */

- (void)setEventCenter:(id<LVTEventCenterProtocol>)center;

/** 当前页面公共缓存对象 */

- (void)setCacheUtil:(LVTCacheUtil *)util;

@end

数据填充等公共方法可抽象到另一个协议中, 再进行继承

4.2.2 模块组件数据模型

用于填充模块组件的数据模型类型不一, 框架也不与具体模型产生瓜葛. 通过协议规范数据模型得有的属性即可.

数据模型协议:

typedef NSObject<LVTItemModelProtocol> LVTItemModel;
@protocol LVTItemModelProtocol <NSObject>

/** Cell内容是否折叠 */
@property (nonatomic, assign) BOOL isFolded;

/** Cell内容完全展示时的大小 */
@property (nonatomic, assign) CGSize itemSize;

/** Cell内容折叠时的大小 */
@property (nonatomic, assign) CGSize foldedItemSize;

/** SectionHeader高度 */
@property (nonatomic, assign) float headerHeight;

/** 悬浮View高度 */
@property (nonatomic, assign) float floatViewHeight;

@end

我们在前边协议中看到的LVTItemModel即代表了遵循该协议的数据模型

4.3 频道代理对象协议

以上我们说的那些模块在框架中的位置都是可调整的. 对于频道中位置固定的内容, 比如导航栏, 容器页头, 页脚等元素, 会交给一个频道的 代理对象 来处理. 

驴妈妈客户端频道页模块化设计思路

除了固定内容的管理, 还有一些与框架无关联的业务功能, 比如点位获取、站点切换等功能, 也会放到代理对象里边实现, 但不在协议里边体现.

具体代理协议如下:

@protocol LVTPageDelegate <NSObject>

#pragma mark - 服务类对象/** 通过事件中心注册相关事件, 以进行一些操作. 比如, 导航栏颜色变化、透明度变化等 */
@property (nonatomic, weak) LVTEventCenter *eventCenter;

/** 布局信息查询器 */
@property (nonatomic, weak) LVTLayoutQuery *layoutQuery;

#pragma mark - 频道表现控制/** 容器顶部间隔, 默认返回statusBar+导航栏高度 */
@property (nonatomic, readonly) CGFloat containerViewTopInset;

/** 是否隐藏TabBar. 默认为YES. */
@property (nonatomic, readonly) BOOL hidesBottomBarWhenPushed;

/** 首屏加载或者下拉刷新是否显示 小驴加载动画. */
@property (nonatomic, readonly) BOOL showsLoadingIndicator;

#pragma mark - 由代理负责创建的对象/** 子类重写getter方法返回nil则没有header. 基类实现默认会创建MJRefreshNormalHeader实例 */
@property (nonatomic, strong) MJRefreshHeader *header;

/** 子类重写getter方法返回nil则没有footer. 基类实现默认会创建MJRefreshAutoNormalFooter实例 */
@property (nonatomic, strong) MJRefreshFooter *footer;

#pragma mark - 托管对象/** 托管给代理的导航栏, 以便修改颜色变化、透明度变化等 */
@property(nonatomic, weak) UINavigationBar *navigationBar;

/** 托管给代理的便捷配置的导航栏item */
@property (nonatomic, weak) UINavigationItem *navigationItem;

/** 托管给代理的主View, 用于添加自定义悬浮View等. 对应在prepareForDispose进行移除. */
@property (nonatomic, weak) UIView *view;

#pragma mark - 自定义配置/** 在该方法进行 导航栏元素创建、Header、Footer创建以及事件注册等 */
- (void)setupPageUI;

/** 根据接口下发数据动态配置样式. 比如搜索框默认文字、默认站点等 */
- (void)configPageWithModel:(id)model;

/** 替换代理对象时, 销毁上一个代理对象, 可在该方法内移除由该代理添加的一些视图等操作 */
- (void)prepareForDispose;
@end

该代理的管理, 与模块管理一致, 共用配置文件, 代理类对象的获取也是通过ClassMapper.

另外, 严格意义上讲, 把这个delegate命名为strategy会更加合适, 它的使用体现是 策略模式. 不同的代理有着不同的实现, 某个频道运行时, 也可能会动态的切换代理对象. 

比如, 某次下拉刷新后, 下发的代理ID变了, 即对应的类对象变了, 就会创建新的 策略对象 来替换, 从而产生了不一样的UI或行为表现.

4.4 对象通信

模块之间, 模块与框架间存在相互通讯的需求. 比如在某些模块组件需要知道框架存在的生命周期事件, 以作出对应的操作. 

对象间的常见通讯方式有:

  1. 命令模式或Target-Action

  2. 代理模式或回调Callback

  3. 观察者模式

考虑到模块间通讯可以1对多, 而前面两种皆为1对1通讯, 所以我们选择基于ReactiveCocoa或RxJava库, 遵循观察者模式来实现一个囊括所有跨模块事件的共享对象, 以进行集中式管理. 以下称之为 事件中心.

具体来说, 就是把有通信需求模块的相关事件集, 以空方法的形式统统添加到事件中心的共享对象上暴露出来(方法实现为空, 但并非抽象类). 各模块则根据自己的需求, 选择性的订阅共享对象上的事件. 

驴妈妈客户端频道页模块化设计思路

模块通讯方式则为直接调用共享事件中心上已添加好的事件方法, 如下:

驴妈妈客户端频道页模块化设计思路

 4.2 模块组件一节中的两个协议里, 都可见定义了设置事件中心的方法以供框架赋值, 以供组件访问.

4.5 交互图

整个框架核心元素间的交互如下:

驴妈妈客户端频道页模块化设计思路

上图没有包括具体的交互细节, 补上两张时序图:

  • 某频道页首屏展示的时序图(忽略本地缓存等各种情况):

    驴妈妈客户端频道页模块化设计思路

    注: 模块的数据源(SectionData)会向ClassMapper获取具体的类对象, 具体可见数据源协议

  • 某个框架事件(比如切换界面、滑动等)通过事件中心传递给订阅者的时序图:

    事件传递为同步操作, 哪个线程调用哪个线程触发, 订阅者接收事件的顺序由订阅时的先后顺序决定

五、小结

以上便为驴妈妈频道页模块化的大致思路, 思路不复杂, 主要细节繁多, 就不一一展开. 

无论何种实现方案, 在灵活满足业务需求的前提下, 同时保证技术上的拓展性, 未来再不断"打怪升级", 都不失为一个较优解.


最后,小长假即将到来,傅说君预祝各位玩的开心,外出请注意安全,enjoy。


版权声明:本站内容全部来自于腾讯微信公众号,属第三方自助推荐收录。《驴妈妈客户端频道页模块化设计思路》的版权归原作者「iOSTips」所有,文章言论观点不代表Lambda在线的观点, Lambda在线不承担任何法律责任。如需删除可联系QQ:516101458

文章来源: 阅读原文

相关阅读