天天看點

【Web技術】1206- 如何設計一款支援懶加載的瀑布流元件?

前言

瀑布流布局算是一種比較流行的布局,參差不齊的多列結構,不僅能節省空間,還能在視覺展示上錯落有緻不拘一格。在一次業務需求中,找了幾個開源的瀑布流元件,在使用的過程中總會有點小問題,便開發了此元件。

在開始之前你可能需要先了解一下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 列數

​Number​

2 -
pageData 目前 pageIndex 請求的資料(非多頁累加資料)

​Array​

[] -
resetSign 重置資料(清空每列資料)

​Boolean​

​false​

-
immediateCheck 立即檢查

​Boolean​

​true​

-
offset 觸發加載的距離門檻值,機關為px

​String|Number​

​300​

-
colStyle 每列的樣式

​Object​

​{}​

-
querySign 内容辨別(querySelectorAll選擇器)

​String​

​必須項​

-

Event

事件名 說明 參數
wfLoad 滾動條與底部距離小于 ​

​offset​

​ 時觸發
-
ObserveDataTotal 未渲染的資料總數 length

Slot

名稱 說明
default 插槽内容
columnIndex 目前内容所在的列
item 單條資料
index 目前資料所在列的下标

技術實作

難點

圖檔大多數會使用懶加載實作,一般情況下節點内容已加載完成,圖檔未出現在可視區域内未加載。在圖檔高度不确定的情況下,怎麼確定清單中列于列的落差小于一個卡片内容高度呢?

原理

利用IntersectionObserver監聽固定的節點資訊,每當監聽節點出現在可視區域中,就會觸發IntersectionObserver回調,在回調中執行資料插入,對比每列的内容的高度,在監聽資料池中取出一個資料放在最小列高度的資料清單中。每一個資料卡片的展示,就會觸發新資料卡片的加載,這就是「懶加載」瀑布流元件的核心思想。

如下圖,當卡片7剛剛展示在可視區域的時候,就會觸發IntersectionObserver回調,再回調邏輯中執行插入函數,插入函數中進行列于列之間的對比,此時對比發現B列高度較小,然後在監聽資料池中取出一個資料,放入B列的資料清單中,渲染出卡片8。

【Web技術】1206- 如何設計一款支援懶加載的瀑布流元件?

設計規劃

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、流程圖設計

【Web技術】1206- 如何設計一款支援懶加載的瀑布流元件?

實踐

pageData實作

  1. 傳入資料,放入監視資料池
  2. 如重置辨別為true,清空監視資料、列資料,
  3. 每次新資料都會觸發資料插入
  4. 如果不相容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實作

  1. 每次資料池資料去空修改觸底辨別,隻要是防止滾動持續觸底,目前資料未渲染完
  2. 首次資料取空查找祖先滾動元素
  3. 每次資料變化,釋出事件,告知目前資料池剩餘資料
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​​