開發與維運

Flutter在流式場景下的架構設計與應用

作者:閒魚技術——光酒

目前,閒魚的主要業務場景都是基於流式場景構建的。在閒魚的主要幾個業務場景下存在兩種類型的頁面:一種是複雜交互的頁面,如發佈頁面、商品詳情頁;另一種是輕交互、需要一定動態化能力滿足千人千面的運營配置及快速A/B實驗需求的頁面,如首頁、搜索頁面、我的等頁面。

在這些輕交互、動態化運營的頁面場景下,有很多共通的處理邏輯:頁面的佈局、數據的管理、事件邏輯驅動的數據變化以及數據驅動的視圖狀態更新;這些工作往往大部分都是重複的工作,重複的代碼邏輯。

在研發效能、交付效率方面,業務的變化往往依賴於版本發佈,動輒兩週的發版週期,對於需要快速投放和響應的業務來說,上線時間過長將難以接受。

能否設計一種流式頁面搭建能力,實現頁面的快速搭建,減少重複代碼,提升研發效能;提供業務動態化的能力,減少對發版發佈的依賴,提高上線的交付效率?

為了解決以上問題,在Flutter版本首頁改版的契機下,閒魚設計了一套流式場景下的頁面搭建架構設計。

流式頁面容器架構設計

在流式佈局的架構設計過程中,面對實際的業務場景,通過以下幾個方面解決端到端的流式頁面容器設計:

  1. 在搭建平臺側,實現頁面搭建、組件管理、協議編排等能力,與投放平臺、A/B實驗平臺和監控平臺打通;
  2. 在客戶端側,採用MVVM模型,設計通用的事件協議,抽象通用的頁面佈局、數據管理及事件處理的能力,減少重複的代碼開發,提升研發效率。在頁面佈局管理方面,與列表容器PowerScrollView深度結合,實現高效的頁面渲染、數據驅動的頁面刷新能力;
  3. 使用阿里巴巴集團 DinamicX作為DSL實現動態模板渲染,滿足投放以及運營需求;
  4. 在與服務端通信協議方面,閒魚一直在實踐Flutter+FaaS的雲端一體化開發,藉助FaaS的能力,定義一套雲端一體化的事件協議,解決業務邏輯動態化的問題,減少發版依賴,進而提升交付效率。

arch

在流式頁面容器架構設計中,重點包括以下幾個核心模塊:協議層、事件中心和數據中心。下面介紹這幾個模板的詳細設計。

協議的設計

在頁面容器協議的設計方面,在結合閒魚業務以及阿里巴巴集團的一些技術方案後,閒魚採用了三層協議的設計:Page、Section和Component。

  1. Page層協議主要包含整個頁面Sections信息,以及下拉刷新、上拉加載更多等配置信息;
  2. Section層協議包含當前Section的佈局信息、初始化Event、LoadMore Event及Components等信息;
  3. Component層協議與具體業務相關,對於容器來說是黑盒的,具體如何渲染會交給業務方處理;默認提供DX解析渲染Handler。

protocol

在通信協議的設計上,全部採用事件傳遞的方式,包括:客戶端與服務端、組件與組件、頁面與組件、頁面與App之間。這也是雲端一體化的設計,理論上開發者只需要考慮事件的發送與接收,具體事件的處理在客戶端還是在服務端,由對應的Handler決定。在雲端一體化的設計下,事件的處理更加靈活,可以更方便地將邏輯後移,當業務發生變更時,減少對發版的依賴。

接下來就讓我們來具體看一看事件中心的設計。

事件中心的設計

一切皆是Event;

在PowerContainer的設計中,一切皆是事件:不論是數據的更新、消息的傳遞、網絡請求、服務端返回的結果,還是自定義的本地處理邏輯。閒魚抽象定義了八種通用的事件類型,整個頁面容器通過事件的流動,完成頁面UI的渲染和刷新,以及業務邏輯的表達和執行。

event

以一次網絡請求為例,一次下拉刷新會獲取每個Section的initEvent事件,並添加到事件中心;事件中心根據事件類型找到對應的Handler來處理。

如果initEvent配置的是remote請求,則交給remoteHandler發送網絡請求,將事件傳送給FaaS端;在FaaS端收到Event後,在FaaS端的事件中心分發,找到對應的hsf服務並獲取數據,最後拼裝成Event的方式,下發給客戶端;客戶端接收到之後繼續讓Event在事件中心流動起來。

在處理完遠端下發的事件之後,EventCenter會發送事件結束的廣播,便於業務處理相關自定義事件。

event remote

通用事件抽象

下面我們來具體看一看通用事件的抽象:

  1. Restart事件:指定整個Page或者某個Section的刷新事件,對於需要刷新的Section,會將其initEvent事件加入事件中心。initEvent常見的一般為一個Remote事件,也可以是任意其他事件。
  2. LoadMore事件:LoadMore事件主要處理分頁加載更多數據的場景。
  3. Update事件:Update事件主要處理數據源的更新及UI的刷新。
  4. Context更新事件:每個Section都存在一個Context信息,代表了服務端與客戶端請求的上下文信息;每個Section的Rmote事件請求,都會默認將Context信息發送給服務端,相應的服務端可以下發Context事件更新指定Section的Context信息;具體使用場景例如分頁加載的page number等;
  5. Replace事件:Replace事件替換Section信息,在tab切換等場景使用會使用;
  6. Remote事件:遠端請求事件;
  7. Native事件:本地通用事件,如頁面跳轉、toast提示、數據埋點等;
  8. 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實現視圖的局部刷新能力。

render

關於PowerScrollView的詳細設計和介紹可以參考閒魚技術公眾號的文章。

動態模板渲染

DXFlutter使用阿里巴巴集團DinamicX作為DSL,在Flutter端實現了高效的動態模板渲染的能力。閒魚使用DXFlutter實現Component層協議的動態模板渲染。

在介紹協議設計時提到過,Component層協議對於頁面容器來說是黑盒,那麼DX卡片事件是如何與頁面容器PowerContainer打通的呢?黑盒的數據又是如何更新的呢?

event

在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的業務開發。

Leave a Reply

Your email address will not be published. Required fields are marked *