驢媽媽客戶端頻道頁模塊化設計思路及實踐

在此先感謝 文燒餅 同窗糾正文中一處描述錯誤的地方.git

要臉, 但贊不要停.github

零、目錄

全文字數: 3,718 | 預計閱讀: 14分鐘設計模式

點擊展開目錄
  • 1、引言
  • 2、模塊定義
  • 3、模塊化設計原則
    • 3.1 面向接口
    • 3.2 數據驅動
    • 3.3 模塊隔離
  • 4、模塊化框架設計
    • 4.1 數據源
      • 4.1.1 數據源協議
      • 4.1.2 模塊組件管理
      • 4.1.3 數據流向
    • 4.2 模塊組件
      • 4.2.1 組件協議
      • 4.2.2 模塊組件數據模型
    • 4.3 頻道代理對象協議
    • 4.4 對象通訊
    • 4.5 交互圖
    • 4.6 架構一覽
  • 5、小結

1、引言

爲了知足運營同窗動態配置頻道頁的內容排版, 以及產品同窗一次開發, 各頻道複用的需求, 要開發一個框架來知足如下兩點:緩存

  • 內容靈活排版: 某個頻道頁展現的內容及其順序, 甚至說一個新的頻道頁, 皆可由運營同窗在cms後臺直接配置
  • 模塊全局複用: 一個承載內容的 模塊 開發完後, 可在所有頻道頁配置使用

頁面模塊化的好處:bash

  1. 方便運營同窗在線上cms後臺直接建立新的界面或動態調整界面(導航欄、頁頭腳、內容元素等). 縮短內容上線週期架構

  2. 在咱們不一樣的業務代碼組件化後, 是相互隔離的. 不一樣的業務線開發好的業務組件難以複用(數據模型、命名、方法定義等都不統一). 並且並不是全部業務組件都能封裝成通用型基礎組件下沉到基礎組件庫中. 開發這個框架, 就能夠同時建立一個統一規範的業務組件庫app

2、模塊定義

上文說起的 內容 , 就是咱們各頻道頁看到的 模塊. 不一樣的 模塊 具備其獨特的產品功能與運營目的.框架

以驢媽媽首頁頻道爲例, 以下圖:ide

每一個框所圈區域爲一個獨立模塊. 好比:模塊化

  • banner模塊(產品推薦、活動推廣、廣告投放等)
  • 頻道入口、主題列表模塊(用戶分流導向)
  • 旅行頭條模塊(熱門遊記推薦)
  • ...

此外, 每一個模塊能夠包含單個或多個不一樣的模塊組件:

3、模塊化設計原則

除了考慮SOLID(六大原則)外, 框架設計還會圍繞如下三點.

3.1 面向接口

經過定義 接口(即協議) 抽象和規範框架所關心的類或事. 框架與模塊間低耦合.

舉個例子, 對於框架來講, 它並不關心配置數據是什麼結構或如何獲取, 它僅關心的是有多少個模塊、每一個模塊在容器中所佔大小以及位置等數據.

可爲此定義一個數據源協議, 來規範充當框架數據源對象所必須遵循的行爲. 至於數據源對象的具體類型是什麼不重要, 只要遵循協議便可充當框架中的某個角色.

接口 就比如一份 合同, 擬定好的 合同 就不能輕易修改, 若是貿然修改原有的條款, 那勢必波及到全部遵循 合同 的人. 因此, 合同 制定階段尤其重要, 不能好高騖遠也不能鼠目寸光, 定好了你們就按照 合同 來.

固然, 面向接口與面向對象並不衝突, 反而是相輔相成, 此處不展開討論.

3.2 數據驅動

數據決定並驅動內容的展現與響應.

  • 數據決定展現內容, 即數據與內容一一對應:

    框架根據數據源提供的相關數據, 決定每一個模塊該建立的組件類型, 模塊組件的展現大小及佈局位置等.

  • 數據驅動內容變化. 關注點爲數據, 而非事件.

    舉個例子, 對於框架中模塊發生的任意事件, 其結果也就兩種:

    1. 事件發生後, 數據有變化
    2. 事件發生後, 數據無變化

    也就說框架不會去管具體發生什麼事件, 只"盯着"它所關心的數據有沒有變

    另外, 事件驅動中一個事件每每對應一個響應操做, 是1對1的關係. 而數據驅動能夠是1對N的關係, 多是多個事件修改同個數據.

3.3 模塊隔離

模塊間相互隔離, 模塊獨立自治, 其相關事務自行處理.

模塊可單獨開發, 註冊到配置中. 模塊內可自行使用MVX、VIPER等結構型設計模式(Structual Design Pattern).

此外, 模塊聯合開發中, 框架與模塊也應該適當隔離. 遵循 依賴倒置原則 . 模塊(高層)依賴也不該該直接依賴框架(低層)進行開發, 框架僅知道咱們抽象出來模塊接口, 模塊也僅知道框架接口, 雙方都遵循接口進行實現.

通俗來講就是框架的功能開發與模塊的開發是兩條平行線, 除非修改接口, 不然雙方修改實現都不會影響到另外一方.

目前這塊實現是傳統型架構(4.6架構圖), 即模塊的開發是直接依賴了框架的實現(具體的基類), 框架沒有抽象出暴露給上層的接口. 在遵循 依賴倒置原則 後, 模塊(高層)在訪問低層(框架)時, 就只能接觸到遵循框架接口的某個對象(UIViewController<XxxProtocol> *), 而非具體某個XXXClass類.

仍是取捨的問題, 選擇性的開閉. 若徹底遵循 依賴倒置原則, 那高層也無法直接依賴低層實現進行繼承了, 包括哪些不容許修改, 哪些要使用公共實現, 哪些留給上層去拓展等等.

4、模塊化框架設計

以iOS平臺舉例, 闡述對整個框架的具體設計. 拋開Android和iOS平臺系統編碼的風格習慣和具體實現上存在的不一樣, 總體思想大同小異.

4.1 數據源

一個頻道頁由若干個模塊組成, 一個模塊包含1個或多個不一樣的組件. 框架根據數據源提供的信息, 建立和安置模塊組件.

4.1.1 數據源協議

  1. 模塊數據源協議: 主要向框架提供某個模塊包含的組件信息、相關的佈局信息、以及組件填充數據的內容等等

    typedef NSObject<LVTSectionDataSource> LVTSectionData;
    
    @protocol LVTSectionDataSource <NSObject>
    
    - (LVTemplateClass)headerClass;
    - (LVTemplateClass)cellClassAtIndex:(NSUInteger)index;
    - ...
    
    - (BOOL)hidden;
    - (NSUInteger)numOfItems;
    - (UIEdgeInsets)sectionInset;
    - (CGFloat)itemSpace;
    - (CGFloat)lineSpace;
    - (CGSize)itemSizeAtIndex:(NSUInteger)index withContainerSize:(CGSize)size;
    - ...
    
    - (nullable LVTItemModel *)itemModelAtIndex:(NSUInteger)index;
    
    - (void)requestSectionCustomData;
    
    @end
    複製代碼
  2. 頻道頁數據源協議: 主要向框架提供整個頻道擁有的模塊總數, 以及各模塊的局部數據源

    @protocol LVTPageDataSource <NSObject>
     
    - (NSUInteger)numberOfSections;
    - (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 組件協議

模塊組件分爲可複用與不可複用兩類, 分別對應如下協議:

  1. 複用組件協議: 提供組件用於複用隊列的複用Id、用於佈局的元素大小等

    typedef Class<LVTReuseItemProtocol> LVTemplateClass;
    typedef UICollectionViewCell<LVTReuseItemProtocol> LVTemplateCell;
    typedef UICollectionReusableView<LVTReuseItemProtocol> LVTemplateReuseView;
    typedef WKWebView<LVTReuseItemProtocol> LVTemplateWebView;
    
    @protocol LVTReuseItemProtocol <NSObject>
    
    + (NSString *)tIdentifier;
    + (CGSize)itemSizeWithModel:(LVTItemModel *)model andContainerSize:(CGSize)size;
    
    - (void)configItemWithModel:(LVTItemModel *)model;
    - (void)setEventCenter:(id<LVTEventCenterProtocol>)center;
    - (void)setCacheUtil:(LVTCacheUtil *)util;
    
    - (void)itemPrepareForReuse;
    
    @end
    複製代碼
  2. 不可複用的懸浮組件協議: 提供視圖高度, 懸浮定位信息等

    typedef Class<LVTFloatViewProtocol> LVTFloatViewClass;
    
     @protocol LVTFloatViewProtocol <NSObject>
    
     + (CGFloat)topInSection;
     + (CGFloat)viewHeight;
    
     - (void)configItemWithModel:(LVTItemModel *)model;
     - ...
    
     @end
    複製代碼

數據填充等公共方法可抽象到另外一個協議中, 再進行繼承

4.2.2 模塊組件數據模型

用於填充模塊組件的數據模型類型不一, 框架也不與具體模型產生瓜葛. 經過協議規範數據模型得有的屬性便可.

數據模型協議:

typedef NSObject<LVTItemModelProtocol> LVTItemModel;

@protocol LVTItemModelProtocol <NSObject>

@property (nonatomic, assign) BOOL isFolded;
@property (nonatomic, assign) CGSize itemSize;
@property (nonatomic, assign) CGSize foldedItemSize;
...

@end
複製代碼

咱們在前邊協議中看到的LVTItemModel即表明了遵循該協議的數據模型

4.3 頻道代理對象協議

以上咱們說的那些模塊在框架中的位置都是可調整的. 對於頻道中位置固定的內容, 好比導航欄, 容器頁頭, 頁腳等元素, 會交給一個頻道的 代理對象 來處理.

除了固定內容的管理, 還有一些與框架無關聯的業務功能, 好比點位獲取、站點切換等功能, 也會放到代理對象裏邊實現, 但不在協議裏邊體現.

具體代理協議以下:

@protocol LVTPageDelegate <NSObject>

@property (nonatomic, weak) LVTEventCenter *eventCenter;
@property (nonatomic, weak) LVTLayoutQuery *layoutQuery;

@property (nonatomic, readonly) CGFloat containerViewTopInset;
@property (nonatomic, readonly) BOOL hidesBottomBarWhenPushed;
@property (nonatomic, readonly) BOOL showsLoadingIndicator;

@property (nonatomic, strong) MJRefreshHeader *header;
@property (nonatomic, strong) MJRefreshFooter *footer;
...

- (void)setupPageUI;
- (void)configPageWithModel:(id)model;
- ...

@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獲取具體的類對象, 具體可見數據源協議

  • 某個框架事件(好比切換界面、滑動等)經過事件中心傳遞給訂閱者的時序圖:

    事件傳遞爲同步操做, 哪一個線程調用哪一個線程觸發, 訂閱者接收事件的順序由訂閱時的前後順序決定

4.6 架構一覽

上張簡版架構圖以示頻道頁模塊化後上述提到的元素分別位於哪一層.

層次 "個性"命名 說明 包含模塊化元素
4 塔頂(召喚師峽谷) 對外是某個"產品". 對內是某個"載體".
3 純業務層(外塔) 與具體業務密切相關的一層, 好比具體某個界面 模塊化通用的控制器VC、遵循PageDataSource的數據源類
2 模塊化"組件"層(中塔) 虛擬出來的一層, 只爲更直觀. 實際也屬於上邊的純業務層. 包括遵循PageDelegate協議、模塊組件協議、模塊DataSource協議的全部類. 是統一規範的大合集
1 業務功能層(內塔) 依舊與業務相關的一層. 但屬於公共的業務功能 模塊化定義的接口, 遵循接口的VC基類、"組件"基類, ClassMapper, EventCenter, 其餘輔助組件開發的類等
0 基礎功能層(水晶) 與業務無關的一層, 換個項目也能用, 具備開源性 通用基礎組件等

5、小結

以上便爲驢媽媽頻道頁模塊化的大體思路, 思路不復雜, 主要細節繁多, 就不一一展開.

不管何種實現方案, 在靈活知足業務需求的前提下, 同時保證技術上的拓展性, 將來再不斷"打怪升級", 都不失爲一個較優解.


原文做者: 傅翔

原文地址: mp.weixin.qq.com/s/J5YhTk5gy…

非商業轉載, 請註明做者及上述原文地址.

相關文章
相關標籤/搜索