因为专题制作需要用到瀑布流,所以这阵子总结了几种实现瀑布流的方式。

纯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,这是为了避免子元素内部的文本块分解成单独的列。

我比较喜欢子容器刚好占满父容器,然后通过paddingmargin来设置间距。

接下来就可以在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-rowgrid-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:item1class: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的瀑布流无法解决的。

 

 

 

 

 

 

Logo

前往低代码交流专区

更多推荐