前言
上一文講到了圖片, 這裡我們就講一個常用的圖片場景: 瀑布流, 他的實現和最佳化
什麼瀑布流
瀑布流,又稱瀑布流式佈局。是比較流行的一種網站頁面佈局,視覺表現為參差不齊的多欄佈局,隨著頁面捲軸向下滾動,這種佈局還會不斷載入資料塊並附加至當前尾部。最早採用此佈局的網站是 Pinterest,逐漸在國內流行開來。國內大多數清新站基本為這類風格。
更直觀的展示如下圖所示:
優缺點
優點:
- 外表美觀,更有藝術性。
- 使用者瀏覽時的觀賞和思維不容易被打斷,留存更容易。
缺點:
- 使用者無法瞭解內容總長度,對內容沒有宏觀掌控。
- 使用者無法瞭解現在所處的具體位置,不知道離終點還有多遠。
- 回溯時不容易定位到之前看到的內容。
- 容易造成頁面載入的負荷。
- 容易造成使用者瀏覽的疲勞,沒有短暫的休息時間。
實現方案
純 CSS 實現
這裡要介紹 CSS 屬性: column
現在常用的 column
屬性有這些
- column-count
將一個元素的內容分成指定數量的列。涉疫設定為具體數字, 或者auto
- column-fill
控制元素的內容在分成多列時如何平衡。有auto
、balance
兩個值, 分別表示自動填充和水平分配 - column-gap
兩列間的距離, 可設定: px, 百分比, rem - column-rule
列布局中, 分割線的設定, 可以設定風格、寬度、顏色(和 border 相同的引數) - column-span
列布局中橫向的佔用元素, 具體作用可檢視此處 - column-width
每一列的最小寬度, 如果外部容易寬度特別小, 則會失效
另一個 CSS 屬性 columns
他是用來設定元素的列寬和列數屬性, 是由 column-width
和 column-count
兩屬性合併而來的簡寫屬性
關於相容性, 目前來看相容性還可以, 可適應大多數情況
相關 CSS 屬性的詳情, 可點選此處
這裡再提供一個我使用 columns
的 demo, 預覽圖:
線上檢視地址: https://grewer.github.io/JsDe...
(_可透過縮放瀏覽器大小來檢視他的效果_)
js 實現
透過 js 我們也可以實現瀑布流, 核心思路是:
- 我們固定每張圖片的寬度, 如 200px, 這樣我們就能計算出當前螢幕中一行是 N 張圖片
- 透過步驟 1 中計算出來的數量, 我們建立高度陣列, 用來存放每一列的高度
- 遍歷圖片, 找到步驟 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 的原始碼地址供大家參考: 點此檢視