内容简介:本文主要分享实现频道页模块化的大体思路, 不涉及具体代码实现.全文字数: 3,053 | 预计阅读: 12分钟为了满足运营同学动态配置
本文主要分享实现频道页模块化的大体思路, 不涉及具体代码实现.
零、目录
全文字数: 3,053 | 预计阅读: 12分钟
一、引言
为了满足运营同学动态配置 频道页的内容排版 , 以及产品同学 一次开发, 各频道复用 的需求, 要开发一个框架来满足以下两点:
模块
二、模块定义
引言提及的 业务内容
, 就是我们各频道页看到的每一个 模块
. 不同的 模块
具有其独特的产品功能与运营目的.
以驴妈妈首页频道为例, 如下图:
每个框所圈区域为一个独立模块. 比如:
- banner模块(产品推荐、活动推广、广告投放等)
- 频道入口、主题列表模块(用户分流导向)
- 旅行头条模块(热门游记推荐)
- ...
此外, 每个模块可以包含单个或多个不同的模块组件:
三、模块化设计原则
除了考虑SOLID(六大原则)外, 框架设计还会围绕以下三点.
3.1 面向接口
通过定义 接口(即协议)
抽象和规范框架所关心的类或事. 框架与模块间低耦合.
举个例子, 对于框架来说, 它并不关心配置数据是什么结构或如何获取, 它仅关心的是有多少个模块、每个模块在容器中所占大小以及位置等数据.
可为此定义一个数据源协议, 来规范充当框架数据源对象所必须遵循的行为. 至于数据源对象的具体类型是什么不重要, 只要遵循协议即可充当框架中的某个角色.
当然, 面向接口与面向对象并不冲突, 反而是相辅相成, 此处就不做过多讨论.
3.2 数据驱动
数据决定并驱动内容的展示与响应.
-
数据决定展示内容, 即数据与内容一一对应:
框架根据数据源提供的相关数据, 决定每个模块该创建的组件类型, 模块组件的展示大小及布局位置等.
-
着重点为数据的变化. 对于框架中模块发生的事件, 其结果只有两种:
- 事件导致相关数据有变化
- 事件没有导致相关数据变化
反过来说, 事件驱动中一个事件对应一个响应操作, 是1对1的关系. 而数据驱动可以是1对N的关系, 可能是多个事件修改同个数据.
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 */ - (NSString *)h5BackUrl; #pragma mark - 布局信息 /** 是否隐藏整个模块(Header、Cell、Inset统统隐藏). 比如异步请求数据前不展示该模块时, 返回YES */ - (BOOL)hidden; /** 元素数量 */ - (NSUInteger)numOfItems; /** 对应模块四周Inset */ - (UIEdgeInsets)sectionInset; /** 距离上一个模块的顶部距离 */ - (CGFloat)marginTop; /** 两个元素之间的水平间隔 */ - (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; @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 // ---------- 以下方法仅需要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 对象通信
模块之间, 模块与框架间存在相互通讯的需求. 比如在某些模块组件需要知道框架存在的生命周期事件, 以作出对应的操作.
对象间的常见通讯方式有:
- 命令模式或Target-Action
- 代理模式或回调Callback
- 观察者模式
考虑到模块间通讯可以1对多, 而前面两种皆为1对1通讯, 所以我们选择基于ReactiveCocoa或RxJava库, 遵循观察者模式来实现一个囊括所有跨模块事件的共享对象, 以进行集中式管理. 以下称之为 事件中心
.
具体来说, 就是把有通信需求模块的相关事件集, 以空方法的形式统统添加到事件中心的共享对象上暴露出来(方法实现为空, 但并非抽象类). 各模块则根据自己的需求, 选择性的订阅事件中心对象上的事件.
模块通讯方式则为直接调用共享事件中心上已添加好的事件方法, 如下:
在 4.2
模块组件一节中的两个协议里, 都可见定义了设置事件中心的方法以供框架赋值, 以供组件访问.
4.4 交互图
整个框架核心元素间的交互如下:
五、小结
以上便为驴妈妈频道页模块化的大致思路, 细节较多, 就不一一展开.
无论何种实现方案, 在灵活满足业务需求的前提下, 同时保证技术上的拓展性, 未来再不断"打怪升级", 都不失为一个较优解.
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- 驴妈妈客户端频道页模块化设计思路及实践
- Android模块化改造以及模块化通信框架
- Laravel 模块化开发模块 – Caffienate
- ASP.NET Core模块化前后端分离快速开发框架介绍之4、模块化实现思路
- 前端模块化架构设计与实现(二|模块接口设计)
- JavaScript模块化
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。