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

栏目: C · 发布时间: 6年前

内容简介:本文主要分享实现频道页模块化的大体思路, 不涉及具体代码实现.全文字数: 3,053 | 预计阅读: 12分钟为了满足运营同学动态配置

本文主要分享实现频道页模块化的大体思路, 不涉及具体代码实现.

零、目录

全文字数: 3,053 | 预计阅读: 12分钟

点击展开目录

一、引言

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

模块

二、模块定义

引言提及的 业务内容 , 就是我们各频道页看到的每一个 模块 . 不同的 模块 具有其独特的产品功能与运营目的.

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

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

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

  • banner模块(产品推荐、活动推广、广告投放等)
  • 频道入口、主题列表模块(用户分流导向)
  • 旅行头条模块(热门游记推荐)
  • ...

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

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

三、模块化设计原则

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

3.1 面向接口

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

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

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

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

3.2 数据驱动

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

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

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

  • 着重点为数据的变化. 对于框架中模块发生的事件, 其结果只有两种:

    1. 事件导致相关数据有变化
    2. 事件没有导致相关数据变化
    驴妈妈客户端频道页模块化设计思路

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

3.3 模块隔离

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

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

四、模块化框架设计

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

4.1 数据源

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

4.1.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
    复制代码
  2. 频道页数据源协议: 主要向框架提供整个频道拥有的模块总数, 以及各模块的局部数据源

    @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 组件协议

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

  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
    复制代码
  2. 不可复用的悬浮组件协议: 提供视图高度, 悬浮定位信息等

    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 对象通信

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

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

  1. 命令模式或Target-Action
  2. 代理模式或回调Callback
  3. 观察者模式

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

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

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

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

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

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

4.4 交互图

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

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

五、小结

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

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

原文地址: shawnfoo.github.io/2018/05/10/…


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

你不是个玩意儿

你不是个玩意儿

杰伦·拉尼尔 / 葛仲君 / 中信出版社 / 2011-8 / 35.00元

“你不是个玩意儿。” 这句话当然不是骂人,这是一个宣言。人当然不是玩意儿,不是机器,而是人。 在网络化程度越来越高的今天,我们每个人似乎都有足够的理由,无限欣喜地拥抱互联网。然而,你有没有想过互联网那些不完美的设计却是某种潜在的威胁…… 为什么如此多的暴民在社交网站上争吵不休,很多骂人的脏话我们在现实的人际交往中可能从来不会使用,但在匿名网络环境中却漫天飞舞? 互联网的本质......一起来看看 《你不是个玩意儿》 这本书的介绍吧!

XML、JSON 在线转换
XML、JSON 在线转换

在线XML、JSON转换工具

html转js在线工具
html转js在线工具

html转js在线工具

RGB HSV 转换
RGB HSV 转换

RGB HSV 互转工具