驢媽媽客戶端頻道頁模組化設計思路
本文主要分享實現頻道頁模組化的大體思路, 不涉及具體程式碼實現.
零、目錄
全文字數: 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 互動圖
整個框架核心元素間的互動如下:

五、小結
以上便為驢媽媽頻道頁模組化的大致思路, 細節較多, 就不一一展開.
無論何種實現方案, 在靈活滿足業務需求的前提下, 同時保證技術上的拓展性, 未來再不斷"打怪升級", 都不失為一個較優解.
原文地址: ofollow,noindex">shawnfoo.github.io/2018/05/10/…