淺析瀑布流佈局原理

Grewer發表於2023-01-31

前言

上一文講到了圖片, 這裡我們就講一個常用的圖片場景: 瀑布流, 他的實現和最佳化

什麼瀑布流

瀑布流,又稱瀑布流式佈局。是比較流行的一種網站頁面佈局,視覺表現為參差不齊的多欄佈局,隨著頁面捲軸向下滾動,這種佈局還會不斷載入資料塊並附加至當前尾部。最早採用此佈局的網站是 Pinterest,逐漸在國內流行開來。國內大多數清新站基本為這類風格。

更直觀的展示如下圖所示:

優缺點

優點:

  1. 外表美觀,更有藝術性。
  2. 使用者瀏覽時的觀賞和思維不容易被打斷,留存更容易。

缺點:

  1. 使用者無法瞭解內容總長度,對內容沒有宏觀掌控。
  2. 使用者無法瞭解現在所處的具體位置,不知道離終點還有多遠。
  3. 回溯時不容易定位到之前看到的內容。
  4. 容易造成頁面載入的負荷。
  5. 容易造成使用者瀏覽的疲勞,沒有短暫的休息時間。

實現方案

純 CSS 實現

這裡要介紹 CSS 屬性: column

現在常用的 column 屬性有這些

  • column-count
    將一個元素的內容分成指定數量的列。涉疫設定為具體數字, 或者auto
  • column-fill
    控制元素的內容在分成多列時如何平衡。有 autobalance 兩個值, 分別表示自動填充和水平分配
  • column-gap
    兩列間的距離, 可設定: px, 百分比, rem
  • column-rule
    列布局中, 分割線的設定, 可以設定風格、寬度、顏色(和 border 相同的引數)
  • column-span
    列布局中橫向的佔用元素, 具體作用可檢視此處
  • column-width
    每一列的最小寬度, 如果外部容易寬度特別小, 則會失效

另一個 CSS 屬性 columns

他是用來設定元素的列寬和列數屬性, 是由 column-widthcolumn-count 兩屬性合併而來的簡寫屬性

關於相容性, 目前來看相容性還可以, 可適應大多數情況

相關 CSS 屬性的詳情, 可點選此處

這裡再提供一個我使用 columns 的 demo, 預覽圖:

線上檢視地址: https://grewer.github.io/JsDe...
(_可透過縮放瀏覽器大小來檢視他的效果_)

js 實現

透過 js 我們也可以實現瀑布流, 核心思路是:

  1. 我們固定每張圖片的寬度, 如 200px, 這樣我們就能計算出當前螢幕中一行是 N 張圖片
  2. 透過步驟 1 中計算出來的數量, 我們建立高度陣列, 用來存放每一列的高度
  3. 遍歷圖片, 找到步驟 2 中高度陣列的最小值(預設為 0), 這樣我們在對應序號插入圖片, 同時高度陣列中, 也更新他的高度

具體實現

第 1,2 步中的初始化和高度陣列

    const width = 200; // 預設設定為 200px 的寬度

    const columns = Math.floor(window.innerWidth / 200) // 計算出當前頁面的列
    
    const columnsHeightArr = []
    
    // mock 圖片地址
    const urls = new Array(10).fill(0).map((it, index) => {
        return `https://grewer.github.io/JsDemo/waterfallLayout/imgs/img_${index}.png`
    })

在獲取圖片高度時, 我們需要注意的是, 圖片未載入完成時, 是無法獲取到圖片高度的

所以這裡我們準備先載入圖片, 在圖片全部載入完成後, 再進行高度的計算

    let flag = 0
    
    const getImg = (url) => {
        let img = new Image();
        img.src = url;
        const imgCallback = () => {
            flag++;
            if (flag === urls.length) {
                handler();
            }
        }
        
        // 是否快取
        if (img.complete) {
            imgCallback()
        } else {
            img.onload = imgCallback
        }
    }

在圖片全部載入完畢之後, 進入 handler 函式, 正式操作圖片:

    // 新增圖片到容器中, 透過 position: absolute 的方案來實現
    const appendImages = (url, position, top) => {
        const img = document.createElement('img');
        img.src = url;
        img.style.left = (position * width) + 'px';
        img.style.top = top + 'px';
        container.appendChild(img)
        return img
    }
    
    // 獲取高度陣列中的最小高度
    const getMin = (arr) => {
        let minHeight = arr[0];
        let index = 0
        for (let i = 1; i < arr.length; i++) {
            if (arr[i] < minHeight) {
                minHeight = arr[i]
                index = i
            }
        }
        return {index, minHeight}
    }
    const insertImages = () => {
        for (let i = 0; i < urls.length; i++) {
            // 判斷是否小於一行, 這樣我們就能直接加入了
            if (columnsHeightArr.length < columns) {
                const img = appendImages(urls[i], i, 0)
                columnsHeightArr[i] = img.offsetHeight;
            } else {
                const {index, minHeight} = getMin(columnsHeightArr)
                const img = appendImages(urls[i], index, minHeight);
                columnsHeightArr[index] = columnsHeightArr[index] + img.offsetHeight;
            }
        }
    }
    

最後的實現效果:

因為圖片是一口氣載入完, 再 append 到頁面中的, 所以頁面不會顯示有圖片的載入流程

線上 demo : 點選檢視

最佳化圖片載入

在上一方案中, 我們是先載入完所有圖片, 再將圖片放到容器中
這樣做的缺點就是: 頁面載入緩慢, 並且圖片也沒有載入流程, 頁面出現的有點突兀

假設:
如果我們一開始不載入所有的圖片,而是載入 N 張, 分批次載入, 這樣會不會好很多呢?

嘗試解決:


    const insertImages = () => {
        if (!urls || urls.length <= 0) {
            console.log('done 全部載入完畢')
            return
        }
        
        // 一行有 N 張圖片的話, 我們就以 N 作為一個批次載入的圖片數量
        const arr = urls.splice(0, columns)
        
        let flag = arr.length;
        arr.forEach(async (item, i) => {
            if (columnsHeightArr.length < columns) {
                const img = await loadImage(item);
                appendImages(img, i, 0)
                columnsHeightArr[i] = img.offsetHeight;
            } else {
                const img = await loadImage(item);
                const {index, minHeight} = getMin(columnsHeightArr)
                appendImages(img, index, minHeight);
                columnsHeightArr[index] = columnsHeightArr[index] + img.offsetHeight;
            }
            // 做出檢查, 所有圖片是否都已經載入完畢
            flag--;
            
            if (flag <= 0) {
                // 呼叫自身, 直到圖片全部載入完畢
                insertImages()
            }
        })
        
    }

效果展示:

線上 demo : 點選檢視

從目前的效果來看, 雖然載入看上去好一點點, 但還是不盡如人意

從根本原因上看, 是因為我們要獲取圖片的長寬來計算佈局, 所以要等待載入

那麼我們提出設想, 在獲取到圖片路徑的同時, 能獲取尺寸比例, 那麼載入速度問題也就解決了

圖片源頭最佳化

假設我們的初始資料是這樣:

[
    {
        "url": "https://grewer.github.io/JsDemo/waterfallLayout/imgs/img_0.png",
        "width": 1776,
        "height": 1184
    },
    //...省略
]

或者是這樣:

[
    "https://grewer.github.io/JsDemo/waterfallLayout/imgs/img_0.png?w=1776&h=1184",
    // ... 省略
]

如果是長寬比例也是沒問題的:


[
    "https://grewer.github.io/JsDemo/waterfallLayout/imgs/img_0.png?scale=1.5",
    // scale = height/width
    // ... 省略
]

這樣的資料, 需要一定的支援, 比如在上傳時, 前端將圖片的尺寸一起上傳, 或者後端計算尺寸存入資料庫等等

我們載入這樣的資料, 就只要關心高度即可, 不用再擔心圖片是什麼時候載入完成的

    const insertImages = () => {
        data.forEach( (item, i) => {
            const img = document.createElement('img');
            img.src = item.url;
            const height = ((item.height/item.width) * (200-gap)) + gap;
            if (columnsHeightArr.length < columns) {
                appendImages(img, i, 0, height)
                columnsHeightArr[i] = height;
            } else {
                const {index, minHeight} = getMin(columnsHeightArr)
                appendImages(img, index, minHeight, height);
                columnsHeightArr[index] = columnsHeightArr[index] + height;
            }
        })
    }
    

效果展示:

線上 demo : 點選檢視

從這裡我們擺脫了圖片的載入速度限制, 可以說是最佳化成功了(當然這種方案是需要一定支援)

長列表的最佳化

瀑布流和正常的列表一樣, 也會存在一個列表過長時的渲染問題

之前我在一篇文章裡介紹了長列表的最佳化(點選此處檢視), 裡面介紹了幾種方案

虛擬列表是一種很好的最佳化方案, 但是他和瀑布流佈局並不好相容, 因為虛擬列表需要的是每一行的高度(即使高度不一樣)

但是瀑布流是按照列來佈局的, 沒有完整行的概念, 這就造成了不相容的衝突

目前來看 CSS 屬性 content-visibility 可支援瀑布流的最佳化, 但是具體效果還需要在業務中實戰出來

總結

本文介紹了瀑布流的概念, 以及兩種不同的實現方案, 並對 JS 方案作出了一定的最佳化解決
CSS 方案比較方便, 但是相容性略差
JS 方案是一種普適性較強的方案, 針對圖片的載入效能和佈局來說, 需要對圖片的比例作出提前的計算

這裡留下本文所有 demo 的原始碼地址供大家參考: 點此檢視

引用

相關文章