作者:光酒
目前,閒魚的主要業務場景都是基於流式場景構建的。在閒魚的主要幾個業務場景下存在兩種型別的頁面:一種是複雜互動的頁面,如釋出頁面、商品詳情頁;另一種是輕互動、需要一定動態化能力滿足千人千面的運營配置及快速A/B實驗需求的頁面,如首頁、搜尋頁面、我的等頁面。
在這些輕互動、動態化運營的頁面場景下,有很多共通的處理邏輯:頁面的佈局、資料的管理、事件邏輯驅動的資料變化以及資料驅動的檢視狀態更新;這些工作往往大部分都是重複的工作,重複的程式碼邏輯。
在研發效能、交付效率方面,業務的變化往往依賴於版本釋出,動輒兩週的發版週期,對於需要快速投放和響應的業務來說,上線時間過長將難以接受。為了解決以上問題,在Flutter版本首頁改版的契機下,閒魚設計了一套流式場景下的頁面搭建架構設計。
流式頁面容器架構設計
在流式佈局的架構設計過程中,面對實際的業務場景,通過以下幾個方面解決端到端的流式頁面容器設計:
1、在搭建平臺側,實現頁面搭建、元件管理、協議編排等能力,與投放平臺、A/B實驗平臺和監控平臺打通;
2、在客戶端側,採用MVVM模型,設計通用的事件協議,抽象通用的頁面佈局、資料管理及事件處理的能力,減少重複的程式碼開發,提升研發效率。在頁面佈局管理方面,與列表容器PowerScrollView深度結合,實現高效的頁面渲染、資料驅動的頁面重新整理能力;
3.使用阿里巴巴集團 DinamicX作為DSL實現動態模板渲染,滿足投放以及運營需求;
4.在與服務端通訊協議方面,閒魚一直在實踐Flutter+FaaS的雲端一體化開發,藉助FaaS的能力,定義一套雲端一體化的事件協議,解決業務邏輯動態化的問題,減少發版依賴,進而提升交付效率。
在流式頁面容器架構設計中,重點包括以下幾個核心模組:協議層、事件中心和資料中心。下面介紹這幾個模板的詳細設計。
協議的設計
在頁面容器協議的設計方面,在結合閒魚業務以及阿里巴巴集團的一些技術方案後,閒魚採用了三層協議的設計:Page、Section和Component。
- Page層協議主要包含整個頁面Sections資訊,以及下拉重新整理、上拉載入更多等配置資訊;
- Section層協議包含當前Section的佈局資訊、初始化Event、LoadMore Event及Components等資訊;
- Component層協議與具體業務相關,對於容器來說是黑盒的,具體如何渲染會交給業務方處理;預設提供DX解析渲染Handler。
在通訊協議的設計上,全部採用事件傳遞的方式,包括:客戶端與服務端、元件與元件、頁面與元件、頁面與App之間。這也是雲端一體化的設計,理論上開發者只需要考慮事件的傳送與接收,具體事件的處理在客戶端還是在服務端,由對應的Handler決定。在雲端一體化的設計下,事件的處理更加靈活,可以更方便地將邏輯後移,當業務發生變更時,減少對發版的依賴。
接下來就讓我們來具體看一看事件中心的設計。
事件中心的設計
一切皆是event;
在PowerContainer的設計中,一切皆是事件:不論是資料的更新、訊息的傳遞、網路請求、服務端返回的結果,還是自定義的本地處理邏輯。閒魚抽象定義了八種通用的事件型別,整個頁面容器通過事件的流動,完成頁面UI的渲染和重新整理,以及業務邏輯的表達和執行。
以一次網路請求為例,一次下拉重新整理會獲取每個Section的initEvent事件,並新增到事件中心;事件中心根據事件型別找到對應的Handler來處理。
如果initEvent配置的是remote請求,則交給remoteHandler傳送網路請求,將事件傳送給FaaS端;在FaaS端收到Event後,在FaaS端的事件中心分發,找到對應的hsf服務並獲取資料,最後拼裝成Event的方式,下發給客戶端;客戶端接收到之後繼續讓Event在事件中心流動起來。
在處理完遠端下發的事件之後,EventCenter會傳送事件結束的廣播,便於業務處理相關自定義事件。
通用事件抽象
下面我們來具體看一看通用事件的抽象:
- Restart事件:指定整個Page或者某個Section的重新整理事件,對於需要重新整理的Section,會將其initEvent事件加入事件中心。initEvent常見的一般為一個Remote事件,也可以是任意其他事件。
- LoadMore事件:LoadMore事件主要處理分頁載入更多資料的場景。
- Update事件:Update事件主要處理資料來源的更新及UI的重新整理。
- Context更新事件:每個Section都存在一個Context資訊,代表了服務端與客戶端請求的上下文資訊;每個Section的Rmote事件請求,都會預設將Context資訊傳送給服務端,相應的服務端可以下發Context事件更新指定Section的Context資訊;具體使用場景例如分頁載入的page number等;
- Replace事件:Replace事件替換Section資訊,在tab切換等場景使用會使用;
- Remote事件:遠端請求事件;
- Native事件:本地通用事件,如頁面跳轉、toast提示、資料埋點等;
- Custom事件:版本預埋的業務自定義事件。
資料中心的設計
在MVVM架構中,資料中心承擔著ViewModel的角色,處理Update事件,主要負責資料的更新及UI檢視的重新整理。對於資料的Update事件,閒魚根據自身業務場景抽象了幾種通用的資料更新型別:overload、patch、override和remove。在UI渲染方面,閒魚將列表容器PowerScrollView與動態模板渲染DXFlutter相結合,實現頁面渲染及資料更新後的頁面重新整理能力。
列表容器
PowerScrollView是閒魚實現的一套功能完善、高效能的列表佈局容器,滿足了頁面容器對於瀑布流、卡片曝光、錨點定位等能力的需求。在檢視渲染重新整理方面,PowerScrollView提供了列表的區域性重新整理能力,完美地解決了資料更新後檢視的重新整理問題。
在協議設計上,二級協議Section以及Footer、Header的設計與PowerScrollView的設計是一一對應的。二級協議Section定義了唯一標識Key,在UI渲染中,對應到PowerScrollView的SectionKey。在資料更新後,頁面容器會根據Section Key實現檢視的區域性重新整理能力。
動態模板渲染
DXFlutter使用阿里巴巴集團DinamicX作為DSL,在Flutter端實現了高效的動態模板渲染的能力。閒魚使用DXFlutter實現Component層協議的動態模板渲染。
在介紹協議設計時提到過,Component層協議對於頁面容器來說是黑盒,那麼DX卡片事件是如何與頁面容器PowerContainer打通的呢?黑盒的資料又是如何更新的呢?
在DSL中,閒魚自定義了頁面容器PowerContainer的事件powerEvent,通過它可以生成頁面容器的通用事件型別,將DinamicX卡片的事件與頁面容器事件中心打通。以上面程式碼為例,點選“刪除關注列表裡面的推薦卡片”的場景,只需要在onTap的事件中定義一個update型別的事件,subType為remove,即可實現資料的刪除及刪除後UI的渲染。
然而這裡沒有定義任何標識,且列表中可以存在多個相同的卡片,又是怎麼知道要操作的是哪一份資料呢?
這裡為每個Component生成一個唯一的ComponentKey,根據SectionKey+ComponentKey生成卡片的唯一標識。在每一個powerEvent事件中,會將Key傳入事件中心,這樣就定位到任意一個Component的資料model,根據事件型別更新資料model。同時,PowerScrollView也可以通過這個Key,操作UI的區域性重新整理。
Section狀態管理
頁面載入的過程中,往往需要展示一些載入狀態的處理,如載入中的Loading動畫、載入失敗狀態的重試按鈕、沒有更多內容狀態的提示資訊等。
在協議的設計方面,每個Section定義了state,在事件中心處理Remote請求事件和應答事件時,更新Section 的state。通過註冊render handler,針對Section的不同狀態返回載入狀態Widget。
void updateSectionState(String key, PowerSectionState state) {
final SectionData data = _dataCenter.findSectionByKey(key);
if (state == PowerSectionState.loading) {
// 從ViewCenter的config獲取loadingWidget
final Widget loadingWidget = _viewCenter?.config?.loadingWidgetBuilder(this, key, data);
// ViewCenter呼叫replace section方法更新UI
_viewCenter.replaceSectionOfIndex(loadingWidget);
// 標記需要重新整理Section
data.needRefreshWhenComplete = true;
} else if (state == PowerSectionState.error) {
...
} else if (state == PowerSectionState.noMore) {
...
} else if (state == PowerSectionState.complete) {
if (data.needRefreshWhenComplete ?? false) { // 判斷是否需要更新Section
final int index = _dataCenter.fineSectionIndexByKey(key);
if (index != null) {
final SectionData sectionData = _dataCenter.containerModel.sections[index];
final PowerSection section = _viewCenter.initSection(sectionData);
_viewCenter.replaceSectionOfIndex(index, section);
}
data.needRefreshWhenComplete = false;
}
}
}
Section狀態變化之後,通過PowerScrollView提供的replaceSection方法,重新整理UI檢視。
Tab容器支援
在閒魚首頁的場景,頁面容器需要支援Tab容器的佈局能力。PowerContainer又是如何支援Tab容器的支援呢?
閒魚在Section的協議中引入了插槽(Slot)的概念,當搭建頁面時,會指定Tab容器的Slot Section,預設不展示任何資訊的空插槽。每一次切換Tab容器,通過Replace事件修改頁面容器的Section資訊。
void replaceSections(List<dynamic> sections) {
if (sections == null || sections.isEmpty || _dataCenter?.containerModel?.sections == null) {
return;
}
for (int i = 0; i < sections.length; i++) {
SectionData replaceData = sections[i];
assert(replaceData.slot != null);
// 尋找Section list中與Slot匹配的index
int slotIndex = _findSlot(replaceData);
// 更新dataCenter
_dataCenter.replaceSectionData(slotIndex, replaceData);
// SectionData 轉換為PowerScrollView所需的PowerSection
final PowerSection sectionData = _viewCenter?.convertComponentDataToSection(replaceData);
// 更新viewCenter
_viewCenter?.replaceSectionOfIndex(slotIndex, sectionData);
//將替換Section的Restart事件傳送到event center
sendEventRestart(replaceData.key);
}
}
PowerScrollView同樣也提供了replaceSection方法,與上文提到的Section狀態管理相結合,完美地解決了tab容器的切換和載入狀態管理的問題。
總結和展望
本節主要介紹了在輕互動、動態化運營場景下,如何從頁面搭建、協議設計、端側容器的實現、動態模板渲染、雲端一體的事件互動等方面,設計並實現一套流式頁面搭建能力,實現頁面的快速搭建,提升研發效能。同時,提供業務動態化的能力,減少發版釋出的依賴,提高上線的交付效率。
目前,頁面容器PowerContainer在閒魚首頁Flutter版本重構中設計並落地。使用PowerContainer後,極大地降低了首頁三個tab頁面的重複程式碼,程式碼邏輯統一管理,降低了一半的人日工作量。在效能方面,有了PowerScrollView的區域性重新整理、Element複用、分幀渲染、差值器等方面的優化,在流暢度方面要優於原生的體驗。
沒有銀彈!這樣一套頁面容器的設計並不是為了適用所有的業務場景,更適合以展示為主、輕互動動態化運營的業務場景。雲端一體的事件協議,在服務端需要事件協議的封裝,這也使得與Serverless的場景更加適合。
閒魚未來還會在搭建平臺側做更多的嘗試,真正實現所見即所得的快速頁面搭建能力。在業務邏輯動態化方面,目前更多的是通過與FaaS的結合、邏輯後移的方式解決。但目前仍然無法解決本地自定義事件依賴發版的問題,未來在這方面也會有更多的嘗試,做到 less code甚至是no code的業務開發。
關注我們,每週 3 篇移動技術實踐&乾貨給你思考!