「中高级前端」干货!深度解析瀑布流布局

34,891

你知道的越多,你不知道的越多
点赞再看,手留余香,与有荣焉

前言

2019年终岁尾,最近对布局相关的内容比较有兴趣,在此整理一下和瀑布流相关的使用场景以及多种实现方案。

瀑布流布局

瀑布流又称瀑布流式布局,是一种比较流行的页面布局方式,专业的英文名称为[Masonry Layouts]。与传统的分页显示不同,视觉表现为参差不齐的多栏布局,最早是由Pinterest首先运用。

无图无真相:

如图所示,网页上呈现参差不齐的多栏布局,图片等宽不等高,根据图片原比例缩放直至宽度达到固定的要求,每行排满后,后面的元素依次添加到其后,视觉上显得错落有致不拘一格。

瀑布流的优点

优点如下:

  • 节省空间,外表美观,更有艺术性。
  • 对于触屏设备非常友好,通过向上滑动浏览
  • 用户浏览时的观赏和思维不容易被打断,留存更容易。

从体验的心理讲,女性是一种逛街数小时都不需要停歇的生物,一眼望不到头的瀑布流契合了这种心理。瀑布流的图片就像商品,就像逛街、就像扫货。女性只要不断往下拉伸页面,就像置身在一条没有尽头的购物街,没有层高限制的商场中一样。传统布局中的下一页就是打断,好比男朋友轻声在耳边说了句:休息一下吧,我累了……结果不言而喻~

瀑布流的缺点

缺点如下:

  • 用户无法了解内容总长度,对内容没有宏观掌控。
  • 用户无法了解现在所处的具体位置,不知道离终点还有多远。
  • 回溯时不容易定位到之前看到的内容。
  • 容易造成页面加载的负荷。
  • 容易造成用户浏览的疲劳,没有短暂的休息时间。

瀑布流的适用场景

根据瀑布流的优缺点,我们不难得出在什么情况下选择瀑布流是合理的选择:

  • 内容以图片为主的时候,瀑布流是更好的选择。图片占用空间比较大,并且大脑理解的速度相比理解文字要快,短时间内可以扫过的内容很多,所以如果用分页显示的话用户务必会频繁的翻页,影响沉浸式的体验,而瀑布流可以解决这个问题。

  • 信息与信息之间相对独立时,瀑布流是更好的选择。如果信息关联性强,用户务必会进行大量的回溯操作去查看之前或者之后的信息,相反,如果信息相对独立的话,可以使用瀑布流,让用户同时接受来自不同地方的信息。

  • 信息与搜索匹配比较模糊时,瀑布流是更好的选择。瀑布流给人的直观印象,就是同时显示的信息与用户搜索的匹配度大致一样,而分页显示的直观印象则是越靠上的信息被认为与用户的搜索越匹配。因此,当信息与搜索匹配度没有明显区分度时,可以采用瀑布流。

  • 用户目的性不强的时候,瀑布流是更好的选择。如果用户有特定需要查找的信息,分页查找定位更方便,而当目的性较弱的时候,瀑布流可以增加用户停留的时间和意想不到的收获。

multi-column 多栏布局实现瀑布流

通常Multi-column用于文本的分列:

.container {
  column-count: 3;
}

multi-column布局中子元素的排列顺序是先从上往下从左至右

根据这个特性,我们就可以用来实现瀑布流

multi-column实现瀑布流主要依赖以下几个属性:

  • column-count: 设置共有几列
  • column-width: 设置每列宽度,列数由总宽度每列宽度计算得出
  • column-gap: 设置列与列之间的间距

column-countcolumn-width都可以用来定义分栏的数目,而且并没有明确的优先级之分。优先级的计算取决与具体的场景。

计算方式为:计算column-countcolumn-width转换后具体的列数,哪个小就用哪个。

一个图片&文字的例子:

<div class="masonry">
    <div class="item">
        <img src="..."/>
        <span class="title">...</span>
    </div>
    <div class="item">
        <img src="..."/>
        <span class="title">...</span>
    </div>
    <!-- more items-->
</div>
.masonry{
    column-count: 3;
    column-gap: 10px;
}
.masonry .item{
    border:1px solid #999;
    margin-bottom: 10px;
}
.masonry .item img{
    width: 100%;
}

点击查看在线DEMO及完整代码

效果如下:

我们可以看到,虽然实现了瀑布流的效果,但奇怪的是例子中前两列的最后一个元素的文本内容自动断开,一部分在当前列尾,一部分在下一列的列头。

我的理解是multi-column布局会将其内的元素自动进行流动和平衡,尽可能保证每列的高度趋于相同,所以会将其内的文本阶段分布在两列内。

而这种展示方式无疑是我们不希望看到的,我们希望的是每个元素都是独立的,前后不断开,此时我们需要使用break-inside来实现。

break-inside: auto | avoid

  • auto: 元素可以中断
  • avoid: 元素不能中断

修改一下之前的例子:

.masonry .item{
    break-inside: avoid;
}

点击查看在线DEMO及完整代码

效果如下:

效果实现了,但由于multi-column布局中子元素的排列顺序是先从上往下从左至右,所以这种方式仅适用于数据固定不变的情况,对于滚动加载更多等可动态添加数据的情况就并不适用了。

关于column更多的用法,参见MDN
关于column的兼容性,参见caniuse

grid 布局实现瀑布流

Grid布局是最强大的 CSS 布局方案。

它将网页划分成一个个网格,可以任意组合不同的网格,做出各种各样的布局。以前,只能通过复杂的 CSS 框架达到的效果,现在浏览器内置了。

上图这样的布局,就是 Grid 布局的拿手好戏,因此,我们就可以用Grid来实现瀑布流

为实现瀑布流先介绍以下几个属性:

  • display:设置为grid指明当前容器为Grid布局
  • grid-template-columns: 定义每一列的列宽
  • grid-template-rows: 定义每一行的行高
  • column-gap:用于设置列间距

grid-template-columnsgrid-template-rows,可以使用绝对单位,也可以使用百分比。并且为了表示比例关系,Grid布局提供了fr关键字,如果设置1fr2fr,表示后者是前者的两倍。

根据以上几个属性,先写一个例子出来,看看效果:

<div class="masonry">
    <div class="item"></div>
    <!-- more items-->
</div>
.masonry{
    display: grid;
    grid-template-rows: 1fr 1fr 1fr; // 分为3行
    grid-template-columns: 1fr 1fr 1fr; // 分为3列
    column-gap:5px; // 列间距5px
}

点击查看在线DEMO及完整代码

效果如下:

我们看到高度不同的div块分布在每一个单元格内,但还没有实现瀑布流的效果。

为实现瀑布流再介绍几个属性:

  • grid-row-start:上边框所在的水平网格线
  • grid-row-end:下边框所在的水平网格线
  • grid-column-start:左边框所在的垂直网格线
  • grid-column-end:右边框所在的垂直网格线

那么什么是网格线呢?

划分网格的线,称为网格线。水平网格线划分出行,垂直网格线划分出列。

正常情况下,n行n + 1根水平网格线,m列m + 1根垂直网格线,比如三行就有四根水平网格线。

上图是一个 4 x 4 的网格,共有5根水平网格线和5根垂直网格线。

这4个属性可接收如下属性:

  • auto:表示自动放置
  • 自定义名称:可以给予网格线一个名称,并在此处引用(本文并不涉及)
  • 网格线索引: 代表第几条网格线(从1开始)
  • span + 数字 : 表示上下边框或左右边框跨越多少网格

来看看这个网格线有什么用处

为方便查看,我们让例子中的每个div块高度修改为100%,并将样式代码修改为:

.item{
    height:100%;
}
.item:first-child{
    grid-row-start:1;
    grid-row-end:span 2;
}

点击查看在线DEMO及完整代码

我们对Grid布局中的第一项添加了grid-row-start:1grid-row-end:span 2,令其上边框位于1水平网格线,下边框距上边框跨越2个水平网格线。从效果上看来,是不是有点像瀑布流了呢!

在之前的例子中,我们分别指定了grid-template-columnsgrid-template-rows用于定义几行几列,由于行列数的确定,其内的每个单元格的宽高也被确定了,而实际的瀑布流布局中,宽度是固定的,而高度是动态的,并且具体的行数也是无法在开始时确定的,所以我们需要在Grid布局中不指定行高(grid-template-rows)。

介绍另一个属性:

  • grid-auto-rows:用来设置多余网格的行高

结合刚才说的Grid实现的瀑布流布局中,不设置行高(grid-template-rows),此时设置grid-auto-rows后,所有单元格的高度均为grid-auto-rows指定的值。

由于grid-row-startgrid-row-end可以指定单元格的上边距和下边距位置,也就是说可以将单元格的高度拉伸,而原有高度由grid-auto-rows决定,我们仅需将grid-auto-rows设置一个很小的值,比如10px,然后对其进行拉伸将其高度指定为真实高度,每一个单元格都做如下操作,那么瀑布流就实现了~

假设第一个单元格内容真实高度为100px,由于grid-auto-rows:10px,那么我们可以这样设置:

.item1{
    grid-row-start:'auto';
    grid-row-end:span 10;
}

假设第二个单元格内容真实高度为150px,由于grid-auto-rows:10px,那么我们可以这样设置:

.item2{
    grid-row-start:'auto';
    grid-row-end:span 15;
}

当然了,在实际情况中,瀑布流更多的是为图片的展示而服务的,并且由于图片是异步请求加载,只有在加载完成后才能获取图片的真实宽高,所以不得不使用JS来动态将单元格高度进行拉伸。

伪代码如下:

    //image-dom
    let img = document.getElementsByTagName('img')[0];
    //image-dom 当前宽度
    let width = img.width;
    
    let image = new Image();
    image.src = 'xxxx.img';
    image.onload = function(){
        //图片原宽
        let w = image.width;
        //图片原高
        let h = image.height;
        //image-dom的真实高度(依据当前宽度及图片真实宽高)
        let height = Math.round(h * width / w)
        //设置当前跨越几个网格(每个网格10px)
        img.style.gridRowEnd = `span ${~~(height/10)}`
    }

点击查看在线DEMO及完整代码

效果如下:

关于column更多的用法,参见MDN
关于column的兼容性,参见caniuse

Flexbox 实现瀑布流

Flexbox布局到今天已经是使用非常广泛的,也算是很成熟的一个特性。在此就不再介绍Flexbox布局的相关内容,如果还有不是很了解的朋友,可参见阮一峰的《Flex 布局教程:语法篇》

那接下来我们就看Flexbox怎么实现瀑布流布局。

此时,我们需要将html结构设计成如下结构:

<div class="masonry">
    <!-- 第一列 -->
    <div class="column">
        <div class="item"></div>
        <!-- more items-->
    </div>
    <!-- 第二列 -->
    <div class="column">
        <div class="item"></div>
        <!-- more items-->
    </div>
    <!-- 第三列 -->
    <div class="column">
        <div class="item"></div>
        <!-- more items-->
    </div>
</div>

上面代码中div.masonry代表当前瀑布流容器,div.column代表每一列的容器,div.item代表每一列中的每一项。

我们需要将div.masonrydiv.column都通过display:flex将其设置为Flex容器。

不同的是瀑布流容器主轴方向设置为水平方向flex-direction:row,列容器主轴方向设置为垂直方向flex-direction:column

.masonry {
    display: flex; // 设置为Flex容器
    flex-direction: row; // 主轴方向设置为水平方向
}

.column {
    display: flex; // 设置为Flex容器
    flex-direction: column; // 主轴方向设置为垂直方向
}

由于当前的html结构分为了瀑布流容器列容器,并且常见的需求图片均是从左至右从上到下来进行排列,所以需要通过Javascript来区分每一列的具体数据:

假设分为三列,伪代码如下:

let data1 = [], //第一列
    data2 = [], //第二列
    data3 = [], //第三列
    i = 0;
while (i < data.length) {
    data1.push(data[i++]);
    if (i < data.length) {
        data2.push(data[i++]);
    }
    if (i < data.length) {
        data3.push(data[i++]);
    }
}
return {
    //第一列
    data1,
    //第二列
    data2,
    //第三列
    data3
};

点击查看在线DEMO及完整代码

效果如下:

总结

做瀑布流需要考虑几方面大因素,图片质量,图片大小,加载速度,如果这些不能同时满足,会大大降低用户体验。个人觉得瀑布流对于触屏终端体验会更好一些。

本文总结了multi-columngridFlexbox三种方式实现瀑布流,实现方案各有不同,从兼容性及易用性综合考虑,还是推荐使用Flexbox的布局实现方案。

本文介绍的瀑布流布局方案,本质上可称为竖向瀑布流,篇幅有限,并未涉及横向瀑布流的内容,关于横向瀑布流的内容,会在接下来的文章中继续总结,敬请期待。

参考

写在最后

  • 文中如有错误,欢迎在评论区指正,如果这篇文章帮到了你,欢迎点赞关注
  • 本文同步首发与github,可在github中找到更多精品文章,欢迎Watch & Star ★
  • 后续文章参见:计划

欢迎关注微信公众号【前端小黑屋】,每周1-3篇精品优质文章推送,助你走上进阶之旅

同时欢迎加我好友,回复加群,拉你入群,和我一起学前端~