1. 程式人生 > >小程式瀑布流元件

小程式瀑布流元件

git倉庫地址:https://github.com/kapeter/mpMasonry

前段時間,接到一個需求,要在小程式中實現不定高度的瀑布流佈局。我首先去萬能的百度上搜索了一波,確實有很多方案,但都是固定高度的,這和需求不符。於是決定自己寫一個,考慮到後面也會有類似的需求,乾脆做成一個通用元件,方便使用。

瀑布流是比較流行的一種網站頁面佈局,尤其在mobile端經常被用來展示資訊流。前段時間,接到一個需求,要在小程式中實現不定高度的瀑布流佈局。我首先去萬能的百度上搜索了一波,確實有很多方案,但都是固定高度的,這和需求不符。於是決定自己寫一個,考慮到後面也會有類似的需求,乾脆做成一個通用元件,方便使用。

本套方案主要使用flex模型,結合小程式的特性(boundingClientRect、抽象節點),實現瀑布流佈局和元件化。

元件佈局

由於手機寬度的限制,一般移動端的瀑布流只有兩列,不需要考慮多列的情況,因此我們的佈局完全可以通過CSS3的flex模型完成。

<!-- masonry.wxml -->
<view class="masonry-list">
    <view class="masonry-list-left" style="{{ 'margin-right:' + intervalWidth }}">
        <view id="left-col-inner">
            <block wx:for="{{items}}" wx:key="{{item.id}}">
                <masonry-item wx:if="{{item.columnPosition === 'left'}}" item="{{item}}"></masonry-item>
            </block>
        </view>
    </view>
    <view class="masonry-list-right">
        <view id="right-col-inner">
            <block wx:for="{{items}}" wx:key="{{item.id}}">
                <masonry-item wx:if="{{item.columnPosition === 'right'}}" item="{{item}}"></masonry-item>
            </block>
        </view>
    </view>
</view>


// masonry.wxss
.masonry-list {
    width: 100%;
    display: flex;
    box-sizing: border-box;
}

.masonry-list-left, .masonry-list-right {
    flex: 1;
}

flex為1,給予左右兩列相同的寬度,通過設定properties中的intervalWidth來控制兩者間的間距。

有人可能會奇怪,為什麼要在class="masonry-list-left"的view之後再加一個層級?同一層級會怎麼樣呢?

來看兩個寫法的對比圖,我們統一給左邊加上高度150px,右邊高度設為auto。 flex的高度補全很明顯,在同一層級情況下,右邊高度也變成了150px,與左邊一致,這會導致我們後面獲取兩邊高度的時候拿到一樣的數值,就無法判斷該把元素放在哪邊。因此,我們要多增加一個層級。

出現這種情況的原因是:flex的column會進行高度補全,和父容器保持一致。

渲染函式

基本佈局已經完成,接下來就是要讓佈局“流”起來。

先來看一下傳統瀑布流的原理:先通過計算出一排能夠容納幾列元素,然後尋找各列之中所有元素高度之和的最小者,並將新的元素新增到該列上,如此迴圈下去,直至所有元素均能夠按要求排列為止。

根據上述原理,渲染流程如下:

渲染流程

/**
 * 渲染函式
 * 
 * @param {Array} items  - 正在渲染的陣列
 * @param {Number} i  - 當前渲染元素的下標
 * @param {Function} onComplete - 完成後的回撥函式
 */
_render (items, i, onComplete) {
    if (items.length > i && !this.data.stopMasonry) {
        this.columnNodes.boundingClientRect().exec(arr => {
            const item = items[i]
            const rects = arr[0]

            const leftColHeight = rects[0].height
            const rightColHeight = rects[1].height

            this.setData({
                items: [...this.data.items, {
                ...item,
                columnPosition: leftColHeight <= rightColHeight ? 'left' : 'right'
                }]
            }, () => {
                this._render(items, ++i, onComplete)
            })
        })
    } else {
        onComplete && onComplete()
    }
}

為了滿足item高度是動態的場景,需要將渲染函式設定為遞迴函式。以下是渲染函式的執行流程:

  • 判斷下標,如果遞迴結束,呼叫完成回撥函式(onComplete),函式結束,反之執行下面流程;
  • 通過boundingClientRect()調取兩邊的高度;
  • 比較兩邊高度,將結果賦給columnPosition欄位;
  • 呼叫setData()將新的元素渲染到dom上,新的元素位置基於columnPosition欄位的值;
  • 渲染完成後,在setData()的回撥函式中執行下一層遞迴,確保下一次boundingClientRect()能獲取到最新的高度。
  • setData()為非同步渲染,詳細說明請見小程式指南-雙執行緒下的介面渲染
  • 由於每渲染一個元素,需要執行一次boundingClientRect()setData(),渲染時間較長。
  • exec()返回的是按請求次序構成的結果陣列,即使只執行了一次請求,結果也位於res[0]而不是res。
  • boundingClientRect的詳細用法可檢視小程式文件-WXML節點資訊API

重新整理函式

有了核心的渲染函式,我們還要進行一些處理。

/**
 * 重新整理瀑布流
 * 
 * @param {Array} items - 參與渲染的元素陣列 
 */
_refresh(items) {
  const query = wx.createSelectorQuery().in(this)
  this.columnNodes = query.selectAll('#left-col-inner, #right-col-inner')

  return new Promise((resolve, reject) => {
    this._render(items, 0, () => {
      resolve()
    })
  })
}

_refresh函式包括兩部分:

  • 獲取左右兩列的WXML節點(這一步放在渲染函式中,會重複獲取,影響效能)
  • 返回一個Promise物件,將_render函式包起來,並在_render的完成回撥函式中觸發resolve(),這樣就能在渲染結束後執行其他操作。

使用抽象節點剝離業務邏輯

當前存在一個問題,masonry-item元件是用來承載元素的業務邏輯,如果專案存在多處需要瀑布流,並且業務邏輯不一樣,那就需要修改masonry元件,新增判斷條件,這就產生了耦合,不符合通用元件的規範。因此,我們需要進行解耦。

這裡需要用到“抽象節點”。以下是定義:自定義元件模版中的一些節點,其對應的自定義元件不是由自定義元件本身確定的,而是自定義元件的呼叫者確定的。這時可以把這個節點宣告為“抽象節點”。

簡單來說,就是在masonry元件內部定義抽象節點masonry-item,這個節點可以代表任何元件,只有當頁面呼叫masonry元件時,這個元件才被確定,這樣就能將業務邏輯元件剝離出來了。

具體實現很簡單,在masonry元件宣告抽象節點。

// masonry.json
"componentGenerics": {
    "masonry-item": true
}

在頁面呼叫時,指定該抽象節點為哪個元件。

<!-- index.wxml -->
<!-- 指定抽象節點為img-box元件 -->
<masonry generic:masonry-item="img-box""></masonry>

注意點:節點的 generic 引用 generic:xxx="yyy" 中,值 yyy 只能是靜態值,不能包含資料繫結。因而抽象節點特性並不適用於動態決定節點名的場景。

如何在頁面中使用元件

1、將components目錄下中masonry資料夾複製到自己專案中。

2、新增業務元件,並在業務元件中新增property,用於承載資料

// property名必須為item
properties: {
    item: { 
        type: Object
    }
}

3、引入masonry元件和所需的業務元件

// index.json
"usingComponents": {
    "masonry": "../../components/masonry/masonry",
    "img-box": "../../components/img-box/img-box"
}

4、在wxml加入masonry節點

<!-- index.wxml -->
<masonry generic:masonry-item="img-box" id="masonry" interval-width="20rpx"></masonry>

generic:masonry-item用於指定業務元件,interval-width為左右兩列空隙寬度。

5、呼叫函式,渲染瀑布流

_doStartMasonry(items) {
    // 通過ID,獲取元件例項
    this.masonryListComponent = this.selectComponent('#masonry');
    // 呼叫元件的start函式,渲染瀑布流
    this.masonryListComponent.start(items).then(() => {
        console.log('render completed')
    })
}

為保證頁面顯示效果,建議一次渲染不超過100個元素。

方法列表

函式名 函式功能 引數說明 返回值
append 批量新增元素 {Array} items - 新增的元素陣列 Promise
delete 批量刪除瀑布流中的元素 {Number} start - 開始下標
{Number} end - 結束下標
deleteItem 刪除瀑布流中的某個元素 {Number} index - 陣列下標
start 啟動元件,開始渲染瀑布流 {Array} items - 參與渲染的元素陣列 Promise
stop 停止渲染瀑布流,清空資料
updateItem 更新渲染陣列中的某個元素 {Object} newItem - 修改後的元素
{Number} index - 需要更新的陣列下標

實踐案例

京東種草

原文https://www.kapeter.com/post/64