纯css和javascript、vue实现瀑布流布局的几种方式
因为专题制作需要用到瀑布流,所以这阵子总结了几种实现瀑布流的方式。纯css实现瀑布流主要有3种方式:1. 多列布局multi-columns2.Flexbox布局3. grid布局multi-columns这是columns的语法:http://www.webhek.com/post/css3-multi-columns.htmlhtml代码...
因为专题制作需要用到瀑布流,所以这阵子总结了几种实现瀑布流的方式。
纯css实现瀑布流主要有3种方式:
1. 多列布局multi-columns
2.Flexbox布局
3. grid布局
multi-columns
这是columns的语法:http://www.webhek.com/post/css3-multi-columns.html
html代码
<div id="box">
<div class="item">
<!--填充内容-->
</div>
<div class="item">
<!--填充内容-->
</div>
<!--更多的列-->
</div>
html结构非常简单,id:box元素是瀑布流的容器,我们的目的是让它的子元素呈现瀑布流排列。这里面可以放置n个项。
css代码
#box {
width: 400px;
column-width: 50%; /*设定列宽*/
column-count: 2; /*列数*/
column-gap: 0; /*列间距*/
}
.item {
break-inside: avoid; /*避免在元素内部断行并产生新列*/
}
你只要在父级元素中设定列数column-count,列间距column-gap,列宽column-width,子元素就会呈现瀑布流排列。
然后在子元素中设定break-inside,这是为了避免子元素内部的文本块分解成单独的列。
我比较喜欢子容器刚好占满父容器,然后通过padding和margin来设置间距。
接下来就可以在item中填充图片和文字。
multi-columns实现瀑布流非常简单,能兼容IE10及以上。
但是只能够一列一列的排列。
demo:https://caizhichen.github.io/waterfall/column.html
flexbox
和multi-columns相比,flexbox可以说是广为人知了。
html代码
<div id="box">
<div class="column">
<div>item1</div>
<div>item2</div>
<div>item3</div>
<!--更多的项-->
</div>
<div class="column">
<div>item1</div>
<div>item2</div>
<div>item3</div>
<!--更多的项-->
</div>
<div class="column">
<div>item1</div>
<div>item2</div>
<div>item3</div>
<!--更多的项-->
</div>
<div class="column">
<div>item1</div>
<div>item2</div>
<div>item3</div>
<!--更多的项-->
</div>
</div>
flexbox瀑布流需要2层包裹item。
css代码
#box {
width: 600px;
display: flex;
background: red;
}
.column {
width: 25%;
display: flex;
flex-direction: column;
}
.column div {
width: 100%;
}
通过设置2层容器,分别设定为行flex-direction:row(默认)和列flex-direction:column。
横向flex布局嵌套多列纵向flex布局,
然后在往子容器中添加内容模块。
这种方式比起multi-columns来说,更麻烦,并且无法自动堆叠。
grid
grid布局语法:https://www.html.cn/archives/8510
grid正在得到众多浏览器的支持,并且比flexbox更强大,我觉得很有学习的必要。
相比前面2个只能通过列排列的瀑布流来说,grid布局则可以实现横向排列的瀑布流。
html代码
<div id="box">
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
</div>
css代码
#box {
display: grid;
grid-gap: 40px; /*复合属性,行和列间距*/
grid-template-columns: repeat(3, 1fr);/*设定3列,每个列高度相同*/
grid-auto-rows: minmax(50px, auto);/*为网格中的行设置默认大小*/
}
.item:nth-of-type(1) {
grid-row: 1 / 4; /* 复合属性,高度起始行 / 高度结束行 */
grid-column: 1; /* 该列中的第1行 */
}
.item:nth-of-type(2) {
grid-row: 1 / 3;
grid-column: 2;
}
.item:nth-of-type(3) {
grid-row: 1 / 4;
grid-column: 3;
}
/* 其他项,逐个添加 */
我们需要通过grid-row
和grid-column
来指定每个子元素所在的区域。
demo:https://caizhichen.github.io/waterfall/grid.html
通过总结css3的三种瀑布流实现方式,我发现,他们虽然能够实现瀑布流布局,但却不符合我的项目需求。
需求:一次加载10张,滑动到图片底部,再加载10张。说白了就是移动端横向瀑布流配合无限滚动。
这是最常见的瀑布流使用方式了。
可惜是multi-columns通过列排列的,它会造成图片的乱序(因为我是下滑加载)。
所以,配合javascript的瀑布流还是不可或缺。
javascript
原理:
1.将一个容器分为n列。
2.判断这些列高度最低的一列,往这个列添加列表项
3.通过循环,重复添加,直到数据添加完毕。
html代码
<div id="waterfall"> <!--瀑布流最外层容器-->
<div class="column"><!--将最外层划分为2个子元素容器,用来存放内容列表-->
<div class="item1"><!--添加这一层容器是为了获取各自列的内容高度,通过循环,在高度最小列添加列表项-->
<!-- <div>
<p>
<img src=" alt="">
</p>
<p>这是文字描述</p>
</div> -->
<!--列表项-->
</div>
</div>
<div class="column"><!--第二列-->
<div class="item2">
</div>
</div>
</div>
html主要分成3层,id:waterfall为瀑布流总容器,class:column是它的子元素,将总容器划分为2列。
接下来可以直接在class:column中放置列表项了。
不过因为要计算每一列的内容高度(我采用flexbox,所以class:column元素高度充满父容器),
我在class:column里面又添加了一层容器,用来放置列表项。这是为了计算列表项的总高度。
css代码
#waterfall {
width: 400px; /*设定总容器宽度*/
display: flex;
}
.column {
flex: 1; /*每个子元素各占一份*/
}
.column img {
width: 200px;
display: block;/*避免图片下面会有留白*/
}
.column p:nth-of-type(2) {
text-align: center;
background-color: antiquewhite;
}
css代码非常简单,设置容器的宽度,高度通过内容来撑开。
接下来是js,任务是动态的往高度最低的列添加列表项。
js代码
// 获取列表项外层容器,用来获取该列总高度和添加子项。
var item1 = document.querySelector('.item1');
var item2 = document.querySelector('.item2');
setTimeout(()=>{ // 运用定时器模拟ajax获取图片数据
var arr = [ // 模拟数据
{
text: '这是图片1',
src: 'https://ss0.bdstatic.com/70cFvHSh_Q1YnxGkpoWK1HF6hhy/it/u=2490556141,282475895&fm=26&gp=0.jpg'
},
{
src: 'https://ss0.bdstatic.com/70cFuHSh_Q1YnxGkpoWK1HF6hhy/it/u=2992241689,499733493&fm=26&gp=0.jpg',
text: '这是图片2'
},
{
text: '这是图片3',
src: 'https://ss3.bdstatic.com/70cFv8Sh_Q1YnxGkpoWK1HF6hhy/it/u=2969780621,3804924728&fm=200&gp=0.jpg'
},
{
src: 'https://ss1.bdstatic.com/70cFuXSh_Q1YnxGkpoWK1HF6hhy/it/u=2006592942,2480446407&fm=200&gp=0.jpg',
text: '这是图片4'
},
{
text: '这是图片5',
src: 'https://ss3.bdstatic.com/70cFv8Sh_Q1YnxGkpoWK1HF6hhy/it/u=2744966649,1683455002&fm=26&gp=0.jpg'
},
{
src: 'https://ss1.bdstatic.com/70cFuXSh_Q1YnxGkpoWK1HF6hhy/it/u=3189653015,2178768034&fm=26&gp=0.jpg',
text: '这是图片6'
},
{
text: '这是图片7',
src: 'https://ss1.bdstatic.com/70cFvXSh_Q1YnxGkpoWK1HF6hhy/it/u=1867339380,1890495727&fm=200&gp=0.jpg'
},
{
src: 'https://ss3.bdstatic.com/70cFv8Sh_Q1YnxGkpoWK1HF6hhy/it/u=615663329,2871716128&fm=200&gp=0.jpg',
text: '这是图片8'
}
];
arr.forEach((val)=>{ // 通过循环,动态添加数据
new Promise((res,rej)=>{
var img = new Image();
img.src = val.src;
img.onload = function () { // 当图片加载完毕,再执行then后面的代码
res(img);
}
})
.then((img)=>{
if (item1.offsetHeight <= item2.offsetHeight) { // 判断高度最低一列,添加内容
// 往第一列添加列表项
item1.innerHTML +=
`<div>
<p><img src="${img.src}" alt=""></p>
<p>${val.text}</p>
</div>`;
} else {
// 往第二列添加列表项
item2.innerHTML +=
`<div>
<p><img src="${img.src}" alt=""></p>
<p>${val.text}</p>
</div>`;
}
})
.catch((error)=>{
console.log(error);
});
});
},100);
js代码的目的:
1.判断高度最低的列
2.往这一列添加列表项
但在实际过程中,因为是通过图片高度来撑开父级,也就是class:item1和class:item2。
而图片是异步加载的,所以要获取最低高度,我们必须在图片加载完毕后再进行高度获取。
所以我采用的方法是结合promise(promise可以看阮一峰的《es6入门标准》)和
图片onload事件(onload 事件在图片加载完成后立即执行,是异步回调)。
1.创建img图片对象,并添加src属性。
2.img.onload事件函数内执行res(),这可以保证在图片加载完毕后才执行then回调函数中的内容。
3.在then回调函数中,判断内容高度,往高度最低的一列中添加列表项。
4.通过循环载入全部数据。
这样就可以实现js瀑布流了。
但是实际上这还存在2个小问题。
1.图片加载并不是一张加载完毕后再加载另一张的。
如果前面的图片很大,也许后面的图片会先加载完毕。
这就会造成图片的乱序,(只会造成同一批加载的图片位置乱序)。
虽然造成的影响不是很大,但还是不够完美。
这可以采用图片预加载来完美解决。
第二个问题是不够完善,如果图片加载失败显示默认图片。
js代码
var defaultImg = new Image();
defaultImg.src = 'https://ss0.bdstatic.com/70cFuHSh_Q1YnxGkpoWK1HF6hhy/it/u=51715546,2816916450&fm=26&gp=0.jpg';
// 获取列表项外层容器,用来获取该列总高度和添加子项。
var item1 = document.querySelector('.item1');
var item2 = document.querySelector('.item2');
setTimeout(()=>{ // 模拟ajax获取图片数据
var arr = [ // 模拟数据
{
text: '这是图片1',
src: 'https://ss0.bdstatic.com/70cFvHSh_Q1YnxGkpoWK1HF6hhy/i'
},
{
src: 'https://ss0.bdstatic.com/70cFuHSh_Q1YnxGkpoWK1HF6hhy/it/u=2992241689,499733493&fm=26&gp=0.jpg',
text: '这是图片2'
},
{
text: '这是图片3',
src: 'https://ss3.bdstatic.com/70cFv8Sh_Q1YnxGkpoWK1HF6hhy/it/u=2969780621,3804924728&fm=200&gp=0.jpg'
},
{
src: 'https://ss1.bdstatic.com/70cFuXSh_Q1YnxGkpoWK1HF6hhy/it/u=2006592942,2480446407&fm=200&gp=0.jpg',
text: '这是图片4'
},
{
text: '这是图片5',
src: 'https://ss3.bdstatic.com/70cFv8Sh_Q1YnxGkpoWK1HF6hhy/it/u=2744966649,1683455002&fm=26&gp=0.jpg'
},
{
src: 'https://ss1.bdstatic.com/70cFuXSh_Q1YnxGkpoWK1HF6hhy/it/u=3189653015,2178768034&fm=26&gp=0.jpg',
text: '这是图片6'
},
{
text: '这是图片7',
src: 'https://ss1.bdstatic.com/70cFvXSh_Q1YnxGkpoWK1HF6hhy/it/u=1867339380,1890495727&fm=200&gp=0.jpg'
},
{
src: 'https://ss3.bdstatic.com/70cFv8Sh_Q1YnxGkpoWK1HF6hhy/it/u=615663329,2871716128&fm=200&gp=0.jpg',
text: '这是图片8'
}
];
var promiseAll = [],
img = [];
arr.forEach((val,i)=>{ // 循环加载图片
promiseAll[i] = new Promise((res,rej)=>{
img[i] = new Image();
img[i].src = val.src;
img[i].title = val.text;
img[i].onload = function () { // 图片加载完毕后该promise状态为fulfilled
res(this);
};
img[i].onerror = function () { //图片加载失败,显示默认图片
this.src = defaultImg.src;
this.onload = function () {
res(this);
};
}
});
});
const p = Promise.all(promiseAll).then((img)=>{ //合并promise,这意味着所有promise状态为fulfilled,p的状态才为fulfilled
img.forEach((val)=>{ // 循环添加
if (item1.offsetHeight <= item2.offsetHeight) { // 判断高度最低一列,添加内容
// 往第一列添加列表项
item1.innerHTML +=
`<div>
<p><img src="${val.src}" alt=""></p>
<p>${val.title}</p>
</div>`;
} else {
// 往第二列添加列表项
item2.innerHTML +=
`<div>
<p><img src="${val.src}" alt=""></p>
<p>${val.title}</p>
</div>`;
}
});
});
},100);
图片预加载有多种方式,我是通过Promise.all来做的。
Promise.all可以将多个promise实例包装成一个新的promise,并且当所有promise状态完成fulfilled,这个新的promise状态也为完成。
promise的状态为fulfilled才会执行then回调函数。
所以我们只要在那些promise中加载图片,当图片加载完毕后再修改状态fulfilled。
然后在新的promise中处理数据,就能实现图片预加载。
同时,我们可以监听onerror事件,当图片加载失败,显示为默认图片,再修改promise状态。
这样就可以实现一个还算ok的js瀑布流了。
还有一种方案,图片的懒加载,需要后台发送图片的宽高。
下面是效果,人的那张图片是加载失败的默认图片。
demo:https://caizhichen.github.io/waterfall/javascript.html
vue.js
接下来是vue怎么实现瀑布流。
因为我的专题是用vue做的,其实和js原理都是一样的,就是判断最低高度,添加列表项。
但在实际做的时候,有一个需要注意的地方。
html代码
<template>
<div id="box">
<div class="wrap-box">
<div class="item1" ref="left">
<div v-for="item of arr1">
<img :src="item.src" alt="">
{{item.gid}}
</div>
</div>
</div>
<div class="wrap-box">
<div class="item2" ref="right">
<div v-for="item of arr2">
<img :src="item.src" alt="">
{{item.gid}}
</div>
</div>
</div>
</div>
</template>
html代码和js的一样,只是绑定上数据。
css代码
#box {
width: 600px;
border: 10px solid purple;
display: flex;
margin: 40px auto;
}
#box .wrap-box {
height: 100%;
background:sandybrown;
font-size: 32px;
text-align: center;
}
#box img {
width: 300px;
}
js代码
export default {
data () {
return {
arr1: [ // 左边一列
],
arr2: [ //右边一列
]
}
},
methods: {
// 通过递归,循环获取高度,插入数据
updataImg (arr) {
if (arr == false) { // 控制递归终止条件
return;
}
var leftHeight = this.$refs.left.offsetHeight;//获取左边一列高度
var rightHeight = this.$refs.right.offsetHeight;
// console.log(leftHeight,rightHeight);
if (leftHeight <= rightHeight) {
this.arr1.push(arr.shift()); // 删除数组的第一项并将删除后的数组添加到arr1中
} else {
this.arr2.push(arr.shift());
}
this.$nextTick(function () {
this.updataImg(arr);
})
},
// 预加载
preloading (arr) {
var defaultImg = new Image();
defaultImg.src = 'https://ss3.bdstatic.com/70cFv8Sh_Q1YnxGkpoWK1HF6hhy/it/u=490096197,3796491108&fm=15&gp=0.jpg';
var promiseAll = [],
img = [];
arr.forEach((val,i)=>{
promiseAll[i] = new Promise((res,rej)=>{
img[i] = new Image();
img[i].src = val.src;
img[i].onload = function () {
res();
};
img[i].onerror = function () {// 图片加载失败
this.src = defaultImg.src;
this.onload = function () {
res();
};
}
});
});
const p = Promise.all(promiseAll).then(()=>{ // 加载完毕
this.updataImg(arr);
});
}
},
created: function () {
this.$ajax({
method: 'POST',
url: 'http://localhost/php/goods.php',
})
.then( (response)=>{ //数据格式[{'src':''},{'src':''}]
var data = response.data;
console.log(response.data);
this.preloading(data);
} )
.catch( (error)=>{
console.log(error);
} );
},
mounted: function () {
}
}
vue和js的原理都是一致的,我们只需要控制2个数组的值。
唯一需要注意的是,vue.$nextTick函数,在官方文档中的解释:在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。
因为我们需要获取列表项的高度,调用这个函数可以让我们获取到DOM更新后的高度。
但是不能直接循环获取,因为我们需要每添加一个列表项,就马上获取高度进行比较,然后再添加另一个列表项。
如果直接在vue.$nextTick函数中循环数据,那得到的高度都是一致的。
所以我用了递归的方式,每添加一个列表项,就调用vue.$nextTick函数来获取高度和添加列表项。
感觉瀑布流布局还是离不开js,因为往往要添加无限滚动功能,这些是运用css的瀑布流无法解决的。
更多推荐
所有评论(0)