小程式瀑布流元件
前段時間,接到一個需求,要在小程式中實現不定高度的瀑布流佈局。我首先去萬能的百度上搜索了一波,確實有很多方案,但都是固定高度的,這和需求不符。於是決定自己寫一個,考慮到後面也會有類似的需求,乾脆做成一個通用元件,方便使用。
瀑布流是比較流行的一種網站頁面佈局,尤其在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。 很明顯,在同一層級情況下,右邊高度也變成了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 - 需要更新的陣列下標 |
無 |