浅析瀑布流布局原理
前言
上一文讲到了图片, 这里我们就讲一个常用的图片场景: 瀑布流, 他的实现和优化
什么瀑布流
瀑布流,又称瀑布流式布局。是比较流行的一种网站页面布局,视觉表现为参差不齐的多栏布局,随着页面滚动条向下滚动,这种布局还会不断加载数据块并附加至当前尾部。最早采用此布局的网站是 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/JsDemo/waterfallLayout/css.html
(可通过缩放浏览器大小来查看他的效果)
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 的源码地址供大家参考: 点此查看
引用
- https://www.zhihu.com/question/20005422/answer/287568035
- https://developer.mozilla.org/zh-CN/docs/Web/CSS/columns
原文地址:https://www.cnblogs.com/Grewer/p/17077106.html
- 图片上传预览js
- Isolation Forest算法实现详解
- css继承样式怎么控制?用选择器
- wordpress站内搜索结果页URL伪静态如何操作
- 如何实现大图居中超过的部分两边自动隐藏
- Ubuntu16.04安装后开发环境配置和常用软件安装
- wordpress如何屏蔽wp-json(禁用REST API)
- 贝叶斯系列——贝叶斯与其他统计流派的区别和联系
- bootstrap tab切换如何让鼠标移动自动切换内容
- css自动换行如何设置?url太长会撑开页面
- Histogram of Oriented Gridients(HOG) 方向梯度直方图
- 动态规划系列之最长递增子序列问题解答
- Git SSH Key 生成步骤
- 如何将wordpress所有文章批量改为已发布状态
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- Flutter中Contrainer 组件的宽高限制分析
- 10张图带你深入理解Docker容器和镜像
- 手摸手教你撸一个微服务框架-关于服务端的处理
- 聊聊claudb的string command
- windows下安装nodejs
- 【Java面试总结】Java集合
- 《JavaScript 模式》读书笔记(8)— DOM和浏览器模式1
- 《JavaScript 模式》读书笔记(8)— DOM和浏览器模式2
- 5000字 | 24张图带你彻底理解21种并发锁
- JavaScript-变量
- Android应用安装卸载监控
- 细数这些年被困扰过的 TS 问题
- 将WordPress插件Elementor标签插入到WordPress模板文件以使用Elementor编辑
- WordPress自定义新建多区域widget小工具调用
- WordPress调用分类目录 及输出当前分类下的二级目录 和分类文章数量显示