前言
瀑布流布局算是一種比較流行的布局,參差不齊的多列結構,不僅能節省空間,還能在視覺展示上錯落有緻不拘一格。在一次業務需求中,找了幾個開源的瀑布流元件,在使用的過程中總會有點小問題,便開發了此元件。
在開始之前你可能需要先了解一下IntersectionObserver[1],核心是這個API監聽指定的卡片是否在可視區域展示,當一個被監聽卡片出現在可視區域,就會觸發回調,執行列于列之間對比邏輯,并在高度較小的列添加資料。
基本使用
這款元件已經上傳npm[2]了,有興趣的小夥伴可以下載下傳使用一下。
1、安裝
npm i waterfall-vue2
2、使用方式
import { Waterfall } from "waterfall-vue2";
Vue.use(Waterfall);
<Waterfall
:pageData="pageData"
:columnCount="2"
:colStyle="{display:'flex',flexDirection:'column',alignItems:'center'}"
query-sign="#cardItem"
@wfLoad="onLoad"
@ObserveDataTotal="ObserveDataTotal"
>
<template #default="{ item, columnIndex, index}">
<!-- slot 内容 good-card:事例元件 -->
<good-card :item="item" id="cardItem" />
</template>
</Waterfall>
3、基本參數和事件
API
參數 | 說明 | 類型 | 預設值 | 版本 |
columnCount | 列數 | | 2 | - |
pageData | 目前 pageIndex 請求的資料(非多頁累加資料) | | [] | - |
resetSign | 重置資料(清空每列資料) | | | - |
immediateCheck | 立即檢查 | | | - |
offset | 觸發加載的距離門檻值,機關為px | | | - |
colStyle | 每列的樣式 | | | - |
querySign | 内容辨別(querySelectorAll選擇器) | | | - |
Event
事件名 | 說明 | 參數 |
wfLoad | 滾動條與底部距離小于 時觸發 | - |
ObserveDataTotal | 未渲染的資料總數 | length |
Slot
名稱 | 說明 |
default | 插槽内容 |
columnIndex | 目前内容所在的列 |
item | 單條資料 |
index | 目前資料所在列的下标 |
技術實作
難點
圖檔大多數會使用懶加載實作,一般情況下節點内容已加載完成,圖檔未出現在可視區域内未加載。在圖檔高度不确定的情況下,怎麼確定清單中列于列的落差小于一個卡片内容高度呢?
原理
利用IntersectionObserver監聽固定的節點資訊,每當監聽節點出現在可視區域中,就會觸發IntersectionObserver回調,在回調中執行資料插入,對比每列的内容的高度,在監聽資料池中取出一個資料放在最小列高度的資料清單中。每一個資料卡片的展示,就會觸發新資料卡片的加載,這就是「懶加載」瀑布流元件的核心思想。
如下圖,當卡片7剛剛展示在可視區域的時候,就會觸發IntersectionObserver回調,再回調邏輯中執行插入函數,插入函數中進行列于列之間的對比,此時對比發現B列高度較小,然後在監聽資料池中取出一個資料,放入B列的資料清單中,渲染出卡片8。

設計規劃
1、一般瀑布流排列方式主要可以分為兩種,一是分欄布局,一是絕對定位布局。不管那種思想難點都在于解決圖檔動态高度的問題。本次采用分欄布局方式,這樣能夠減少因圖檔加載而進行大面積卡片位置的計算。
2、建立一個資料監聽池,作用一是将所有未渲染的資料儲存在其中。作用二是當資料池的資料取完之後可以減少很多不必要的執行操作。
3、參數規劃
// 列數
columnCount: {
type: Number,
default: 2,
},
// 每頁資料
pageData: {
type: Array,
default: () [],
},
// 重置
resetSign: {
type: Boolean,
default: false,
},
// 立即檢查
immediateCheck: {
type: Boolean,
default: true,
},
// 偏移
offset: {
type: [Number, String],
default: 300,
},
// 樣式
colStyle: {
type: Object,
default: () ({}),
},
// 查詢辨別
querySign: {
type: String,
require: true,
},
3、函數規劃
- getMinColSign 傳回最小列的辨別
- checkObserveDom 檢查目前dom是否有未監聽,将未監聽的節點放入監聽範圍内
- insetData 執行取資料并插入列資料中
- getScrollParentNode 擷取祖先滾動元素,并綁定滾動事件
- check 滾動檢查是否觸發加載閥值
4、流程圖設計
實踐
pageData實作
- 傳入資料,放入監視資料池
- 如重置辨別為true,清空監視資料、列資料,
- 每次新資料都會觸發資料插入
- 如果不相容IntersectionObserver,每列均分目前的資料
pageData(value = []) {
if (!value.length) return
if (IntersectionObserver) {
// 判斷目前是否需要重置
if (this.resetSign) {
// 重置斷開目前全部監控資料
this.intersectionObserve.disconnect()
Object.keys(this.colListData).forEach((key) => {
this.colListData[key] = []
})
this.observeData = [...value]
this.$nextTick(() {
this.insetData()
})
} else {
this.observeData = [...this.observeData, ...value]
// 插入資料
this.insetData()
}
} else {
// 當 IntersectionObserver 不支援,每列資料均勻配置設定
const val = (this.observeData = value)
while (Array.isArray(val) && val.length) {
let keys = null
// 盡量減小資料配置設定不均勻
if (this.averageSign) {
keys = Object.keys(this.colListData)
} else {
keys = Object.keys(this.colListData).reverse()
}
keys.forEach((key) => {
const item = val.shift()
item && this.colListData[key].push(item)
})
this.averageSign = !this.averageSign
}
}
}
insetData實作資料插入函數,確定控制資料的入口隻有一個,避免同一批處理周期内執行多次。
// 插入資料
insetData() {
const sign = this.getMinColSign()
const divData = this.observeData && this.observeData.shift()
if (!divData || !sign) {
return null
}
this.colListData[sign].push(divData)
this.checkObserveDom()
},
getMinColSign實作擷取目前所有列中高度最小的列,并傳回其辨別
// 擷取最小高度最小的辨別
getMinColSign() {
let minHeight = -1
let sign = null
Object.keys(this.colListData).forEach((key) => {
const div = this.$refs[key][0]
if (div) {
const height = div.offsetHeight
if (minHeight === -1 || minHeight > height) {
minHeight = height
sign = key
}
}
})
return sign
},
checkObserveDom實作将未加入監視的節點,加入監視
// 檢查dom是否全部被監控
checkObserveDom() {
const divs = document.querySelectorAll(this.querySign)
if (!divs || divs.length === 0) {
// 防止資料插入dom未渲染,監聽函數無資料
setTimeout(() {
// 每次新資料的首個資料無法監控,需要延遲觸發
this.insetData()
}, 100)
}
divs.forEach((div) => {
if (!div.getAttribute('data-intersectionobserve')) {
// 避免重複監聽
this.intersectionObserve.observe(div)
div.setAttribute('data-intersectionobserve', true)
}
})
}
observeData實作
- 每次資料池資料去空修改觸底辨別,隻要是防止滾動持續觸底,目前資料未渲染完
- 首次資料取空查找祖先滾動元素
- 每次資料變化,釋出事件,告知目前資料池剩餘資料
observeData(val) {
if(!val) return
if (val.length === 0) {
if (this.onceSign) {
// 監視數組資料分發完了,在進行首次的祖先滾動元素的查找
this.onceSign = false
this.scrollTarget = this.getScrollParentNode(this.$el)
this.scrollTarget.addEventListener('scroll', this.check)
}
// 資料更新,修改觸發觸底辨別
this.emitSign = true
}
this.$emit('ObserveDataTotal', val.length)
}
getScrollParentNode實作在内容未加載的時候,無法準确的通過overflow屬性查找到滾動祖先元素,為了能夠更準确的擷取祖先滾動元素,在首次内容全部加載之後才進行祖先滾動元素的查找
// 擷取滾動的父級元素
getScrollParentNode(el) {
let node = el
while (node.nodeName !== 'HTML' && node.nodeName !== 'BODY' && node.nodeType === 1) {
const parentNode = node.parentNode
const { overflowY } = window.getComputedStyle(parentNode)
if (
(overflowY === 'scroll' || overflowY === 'auto') &&
parentNode.clientHeight != parentNode.scrollHeight
) {
return parentNode
}
node = parentNode
}
return window
},
check實作檢查是否觸發load
// 滾動檢查
check() {
this.intersectionObserve && this.checkObserveDom()
// 觸底辨別為false直接跳過
if (!this.emitSign) {
return
}
const { scrollTarget } = this
let bounding = {
top: 0,
bottom: scrollTarget.innerHeight || 0,
}
if (this.$refs.bottom.getBoundingClientRect) {
bounding = this.$refs.bottom.getBoundingClientRect()
}
// 元素所在視口容器的高度
let height = bounding.bottom - bounding.top
if (!height) {
return
}
const container = scrollTarget.innerHeight || scrollTarget.clientHeight
const distance = bounding.bottom - container - this._offset
if (distance < 0) {
// 釋出事件
this.$emit('wfLoad')
// 釋出事件觸發修改觸底辨別
this.emitSign = false
}
},
最後
上面便是整個「懶加載」瀑布流元件的産生過程,感興趣的小夥伴可以下載下傳使用,體驗一下。或者您有更好的想法,互相探讨,共同進步。
源碼位址
github:https://github.com/zengxiangfu/vue2-waterfall
參考資料
[1]
IntersectionObserver: https://developer.mozilla.org/zh-CN/docs/Web/API/IntersectionObserver
[2]
npm: https://www.npmjs.com/package/waterfall-vue2