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

ShawnFoo發表於2018-09-28

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

要臉, 但贊不要停.

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

零、目錄

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

點選展開目錄
  • 一、引言
  • 二、模組定義
  • 三、模組化設計原則
    • 3.1 面向介面
    • 3.2 資料驅動
    • 3.3 模組隔離
  • 四、模組化框架設計
    • 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 架構一覽
  • 五、小結

一、引言

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

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

頁面模組化的好處:

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

  2. 在我們不同的業務程式碼元件化後, 是相互隔離的. 不同的業務線開發好的業務元件難以複用(資料模型、命名、方法定義等都不統一). 而且並非所有業務元件都能封裝成通用型基礎元件下沉到基礎元件庫中. 開發這個框架, 就可以同時建立一個統一規範的業務元件庫

二、模組定義

上文提及的 內容 , 就是我們各頻道頁看到的 模組. 不同的 模組 具有其獨特的產品功能與運營目的.

以驢媽媽首頁頻道為例, 如下圖:

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

每個框所圈區域為一個獨立模組. 比如:

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

此外, 每個模組可以包含單個或多個不同的模組元件:

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

三、模組化設計原則

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

3.1 面向介面

通過定義 介面(即協議) 抽象和規範框架所關心的類或事. 框架與模組間低耦合.

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

可為此定義一個資料來源協議, 來規範充當框架資料來源物件所必須遵循的行為. 至於資料來源物件的具體型別是什麼不重要, 只要遵循協議即可充當框架中的某個角色.

介面 就好比一份 合同, 擬定好的 合同 就不能輕易修改, 如果貿然修改原有的條款, 那勢必波及到所有遵循 合同 的人. 所以, 合同 制定階段尤為重要, 不能好高騖遠也不能鼠目寸光, 定好了大家就按照 合同 來.

當然, 面向介面與物件導向並不衝突, 反而是相輔相成, 此處不展開討論.

3.2 資料驅動

資料決定並驅動內容的展示與響應.

  • 資料決定展示內容, 即資料與內容一一對應:

    框架根據資料來源提供的相關資料, 決定每個模組該建立的元件型別, 模組元件的展示大小及佈局位置等.

  • 資料驅動內容變化. 關注點為資料, 而非事件.

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

    1. 事件發生後, 資料有變化
    2. 事件發生後, 資料無變化

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

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

    另外, 事件驅動中一個事件往往對應一個響應操作, 是1對1的關係. 而資料驅動可以是1對N的關係, 可能是多個事件修改同個資料.

3.3 模組隔離

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

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

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

通俗來說就是框架的功能開發與模組的開發是兩條平行線, 除非修改介面, 否則雙方修改實現都不會影響到另一方.

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

還是取捨的問題, 選擇性的開閉. 若完全遵循 依賴倒置原則, 那高層也沒法直接依賴低層實現進行繼承了, 包括哪些不允許修改, 哪些要使用公共實現, 哪些留給上層去擴充等等.

四、模組化框架設計

以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 基礎功能層(水晶) 與業務無關的一層, 換個專案也能用, 具有開源性 通用基礎元件等

五、小結

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

無論何種實現方案, 在靈活滿足業務需求的前提下, 同時保證技術上的擴充性, 未來再不斷"打怪升級", 都不失為一個較優解.


原文作者: 傅翔

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

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

相關文章