CSS Grid中的auto-fill和auto-fit

发布于 大漠

Web布局相关的话题一直以来都是一个永恒的话题,不管发展到什么时候,都不会缺这样的话题。一直以来,我都非常推崇CSS Grid的布局模块,在我的认知中,CSS Grid布局是一个强大的布局,它将灵活的解决我们以往在Web中碰到的大多数问题。正因为CSS Grid布局很强大,因此它的体系也很庞大。比如@Juan Martín García就用CSS Grid向大家展示了如何通过一行CSS代码实现特定环境下的响应式布局效果。

grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));

在不依赖任何媒体查询的条件之下就可以实现上图所示的效果。

实现该效果运用到了CSS Grid中的几个特性:repeat()minmax()frauto-fit(或 auto-fill)。换句话说,构建上图这样的响应式布局效果,使用CSS Grid来实现的话离不开这几个属性,但在这篇文章中主要想和大家一起探讨 auto-fit 和 **auto-fill**之间的差异。希望对大家有所帮助。

简单地回忆repeat()minmax()

auto-fitauto-fill主要被运用于CSS Grid中的repeat()函数中。

由于auto-fitauto-fillrepeat()函数紧密绑定在一起使用,因此,在介绍auto-fitauto-fill之前,我们很有必要的了解一下CSS Grid中的repeat()函数。

在Web布局中时常会碰到平铺的场景,在CSS Grid布局实现这样的布局效果非常的容易。那是因为CSS Grid中有一个强大的函数repeat()。比如说,有一个卡片平铺的布局,卡片的宽度都是300px,在CSS Grid中可以这样来处理:

.card__container {
    display: grid;
    grid-template-columns: 300px 300px 300px 300px;
}

上面的代码告诉Web浏览器,card__container容器通过grid-teplate-columns将容器分成了四列,每列的宽度都是300px。对于这样的场景(每列的宽度相等),那么可以使用repeat()函数,让布局变得更简单:

.card__container {
    display: grid;
    grid-template-columns: repeat(4, 300px)
}

我们用一张图来描述,会更易于理解:

repeat()同样可以运用于grid-template-rows属性中。

另外,repeat()函数的第二个参数还可以是一个复合值,比如:

.card__container {
    grid-template-columns: repeat(3, 40vmin 30vmin)
}

意思是40vmin 30vmin被重复3次,这样一来就构建了一个6列的网格:

同样用图来解释:

事实上,在repeat()函数的第二个参数中还可以结合minmax()函数一起使用。其中minmax()函数接受两个参数,即 minmax(minVal, maxVal)。如果maxVal小于minVal,则minmax(minVal, maxVal)会忽略maxVal,将minmax(minVal, maxVal)视为minVal

比如,我们把上面的代码稍作调整:

.card__container {
    grid-template-columns: repeat(4, minmax(30vmin, 1fr))
}

效果如下:

你可以在上面的示例中尝试着拖动card__container来改变其大小,可以发现每个.card的宽度(列宽)会在30vmin1fr之间变化:

如果你足够仔细的话,你可能已经发现了repeat()函数中都显式指定了列数,但很多平铺效果的布局中,我们并不知道准确的列数,这个时候就需要用到auto-fillauto-fit

重复填充:auto-fillauto-fit

auto-fillauto-fit 在CSS Grid布局中的作用是什么?

为了能比较形象的给大家描述清楚auto-fillauto-fit是什么,我们通过一个示例来描述。假设在Web布局中有这样的一个卡片平铺的布局效果。当我们知道卡片容器.card__container的大小、卡片数量、卡片宽度和卡片之间的间距时,我们可以很容易地在repeat()函数中指定卡片的在容器的重复个数以及宽度,比如前面看到的示例:

.card__container {
    grid-template-columns: repeat(4, 300px);
}

但问题是,当卡片容器无法有足够的空间容纳时,那么卡片就会溢出容器,打破布局:

.card__container {
    display: grid;
    grid-template-columns: repeat(3, 200px);
    gap: 2vmin;
    width: 500px;
}

效果如下:

就上面这个示例而言,容器的宽度显式设置为200px,卡片宽度为200px,卡片之间(列间距为2vmin),这样一来,所有卡片的其之间间距的宽度总和是:

w' = width * 3 + gap * 2 = 200px * 3 + 2vmin * 2

相当于:

width: calc(200px * 2 + 2vmin * 2)

溢出的宽度即是:

w'' = width * 3 + gap * 2 - W = 200px * 3 + 2vmin * 2 - 500px

即:

面对这样的场景,作为开发者总是希望,卡片容器中的卡片数量能够根据卡片容器、卡片宽度以及它们之间的间距做出一个最佳匹配。那么在CSS Grid中就可以轻易的做到,即: repeat()函数中使用auto-fillauto-fit替代确切的重复列数

.card__container {
    grid-template-columns: repeat(auto-fill, 200px);
}

// 或

.card__container {
    grid-template-columns: repeat(auto-fit, 200px);
}

auto-fillauto-fit关键词会告诉浏览器处理列的大小和元素换行,以便当容器宽度不足以容纳元素时,元素会自动换行而不会造成溢出。但auto-fillauto-fit两者之间还是有一些细微的差异。接下来就来探讨它们之间的差异以及幕后到底发生了什么?

auto-fill VS. auto-fit

auto-fillauto-fit之间的差异,先上张图来描述:

  • auto-fill:在同一行中填充尽可能多的列。因此,只要能容纳新的列,就会自动创建隐式列,因为它试图在同一行中填充尽可能多的列。新添加的列(隐式列)可以是空的,也可能是空的,但是它们仍然会在行中占据指定的空间
  • auto-fit:将当前可用的列扩展到空间中,以便它们占用容器可用空间。当容器有可用空间时,浏览器会将可用空间均分给列,让列自动变宽填满整个容器;当容器可用空间为负值时,会另起一行排列

为了让大家减少文字描述带来的困惑,我们用@Ana Tudor在CodePen的一个示例来向大家阐述这种行为,能帮助大家更好的理解auto-fillauto-fit之间的差异:

在这个示例中,使用了minmax()来定义列的宽度,其中最小宽度是15%,最大宽度是1fr;而每行的列数分别使用了auto-fillauto-fit

.auto-fit {
    grid-template-columns: repeat(auto-fit, minmax(15%, 1fr))
}

.auto-fill {
    grid-template-columns: repeat(auto-fill, minmax(15%, 1fr))
}

拖动示例中滑块,改变容器中的列数,你可以看到auto-fitauto-fill的差异:

这个示例,在minmax()中使用的都是相对单位值,所以在改变容器大小时,列只会改变大小,列并不会在新的一行显示(容器没有足够空间):

为了能更好的展示auto-fillauto-fit的作用以及差异,我们对上面的示例稍作修改,将示例中的15%换成150px

.auto-fit {
    grid-template-columns: repeat(auto-fit, minmax(150px, 1fr))
}

.auto-fill {
    grid-template-columns: repeat(auto-fill, minmax(150px, 1fr))
}

效果如下:

尝试着改变容器大小,当容器没有足够空间时,卡片会另起一行排列:

你可能已经发现了,在一定程度上,auto-fillauto-fit显示的结果是相同的

虽然在浏览器上显示的结果相同,但它们的内在行为并不完全相同。在特定的视窗宽度下,它们会给出相同的结果。auto-fillauto-fit开始显示不同结果的时间点主要取决于grid-template-columns定义的列数和大小。因此在不同的示例中会有所不同。

就上面示例而言,视窗宽度和网格容器宽度相同

当视窗宽度足够大(网格容器宽度足够大),可以将一个(或多个)额外的列放入到同一行中时,auto-fillauto-fit之间的差异就很明显。此时,浏览器针对这种情况会有两种处理方式,其处理方式很大程度上取决于是否有内容要放到额外的列中。

auto-fitauto-fill如何工作

我们来看看auto-fitauto-fill是如何工作的。同样用图来向大家展示auto-fitauto-fill之间的区别。比如下图所示。我调整视窗的大小(改变网格容器大小),可以明显地看到auto-fitauto-fill的差异:

如果还不够清楚,在Firefox浏览器打开网格编辑器,查看auto-fill状态下网格线的变化:

你是否发现了,如果网格容器有足够多的空间时,auto-fill会自动的创建隐式的列,而且列的宽度是minmax()中的minVal。最终这些创建的空列同样会将网格容器填满:

上图中的三个红色的框就是auto-fill创建的隐式列

auto-fit不同,当网格容器有足够的空间,auto-fit并不会创建隐式的列(列数和初始化相同),不同的是,auto-fit会让列取的值是minmax()中的maxVal,在该示例中是1fr,因此会均分网格容器剩余的空间:

如果minmax()的第二个参数maxVal不是一个相对单位的值,是一个固定值,比如:

.auto-fit {
    grid-template-columns: repeat( auto-fit, minmax(150px, 300px))
}

你发会现,当网格容器没有足够空间容纳列时,列会自动换行:

接着再回过头来看auto-fill:

.auto-fill {
    grid-template-columns: repeat( auto-fill, minmax(150px, 300px))
}

这个时候的表现和auto-fit是一样的:

如果上面描述的不够清楚,可以尝试着调整下面Demo中的列数:

再来看另外一种情景,就是repeat()函数中不使用minmax()。同样拿@Ana Tudor 写的另一个案例来举例:

.auto-fit {
    grid-template-columns: repeat(auto-fit, 14vw);
}

.auto-fill {
    grid-template-columns: repeat(auto-fill, 14vw)
}

效果如下:

先来看网格容器有足够空间的情景,效果如下:

尝试着调整视窗大小,你会发现效果是这样的:

这里主要是由于示例中采用的是视窗单位vw,当你改变视窗大小时,列的宽度也会随着变化:

如果换成固定单位时,auto-fillauto-fit在容器没有足够空间时,会让列断行排列:

简单地小结一下

只有当容器有足够空间可容纳更多列时,auto-fillauto-fit适应列大小的区别才会明显。

如果使用的是auto-fit,且repeat()结合minmax(val, 1fr)同时使用,网格容器有剩余空间时,将会把剩余空间分配给列,从效果上看,列会拉伸到填满整个容器。而使用auto-fill时,会创建隐式的列,就像用空列排满网格容器。

至于auto-fitauto-fill具体使用哪个,应该根据实际的场景来选择。这样才会更有意义。

来看一个示例

我们来使用auto-fitminmax()实现一个网格平铺的效果。就拿卡片类平铺来举例吧:

这个示例主要就是使用了前面介绍的内容,即repeat()auto-fitminmax()。示例通过minmax(300px, 1fr)设置了卡片的最小宽度是300px,最大的宽度为1fr,会根据容器可用空间合理计算出卡片的最大宽度。

示例中使用React构建了一个Card组件:

const Card = (props) => {
    const { imgUrl, imgDes, cardTitle, cardDes } = props;
    return (
        <li className="card">
            <div className="card__object">
                <img src={props.imgUrl} alt={props.imgDes} />
            </div>
            <div className="card__body">
                <h4 className="card__title">{props.cardTitle}</h4>
                <p>{props.cardDes}</p>
            </div>
        </li>
    );
};

为了更易于展示Demo,这里构建了几个假数据,然后map()出整个卡片列表:

const App = () => {
    const CardItem = CardData.map((item, index) => (
        <Card
        imgUrl={item.imgUrl}
        imgDes={item.imgDes}
        cardTitle={item.cardTitle}
        cardDes={item.cardDes}
        key={index}
        />
    ));
    return <ul className="card__container">{CardItem}</ul>;
};

就该示例而言,我们更关注的是CSS。换句话说,我们简单的几行代码就可以很容易的实现卡片平铺效果:

.card__container {
    gap: 2vmin;
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
}

这段代码很神奇,卡片能根据容器(此例就是视窗大小)自动匹配出合理的列数(即,每行显示多张卡片):

就这么几行简单地代码,就实现了卡片平铺效果,而且具备响应式效果

特别声明:《你可能不太熟知的布局技巧》一文整理一些有关于布局方面的技巧,类似上例一样,通过简单几行代码就可以实现一个难以想象的布局效果

CSS Grid布局是非常强大的,我们在上面的示例上,稍作一点处理,效果就会另一个样,比如说使用span合并列:

.card:nth-child(1) {
    grid-column: span 2;
}

.card:nth-child(6) {
    grid-column: span 3;
}

除了使用span之外,还可以像下面这样使用:

.card {
    grid-column: (start track) / (end track);
}

虽然这样可以让我很轻易的对卡片进行合并(类似于表格中的合并单元格),但在一些特定容器宽度下,布局效果略有瑕疵:

不过不用担心,CSS Grid中提供了一个属性,可以帮助我们轻易的避免这种现象:

.card__container {
    grid-auto-flow: dense;
}

最终的效果如下:

你可以尝试在新的标签页面打开这个Demo,拖动浏览器改变视窗大小查看Demo效果。

小结

现在,你可以通过CSS Grid模块提供的功能,使用最少的代码创建灵活的响应式网格(即响应式布局),并且这个效果不需要依赖任何媒体查询特性。

换句话说,当你使用类似repeat(auto-fit,minmax(300px, 1fr))即可实现带有响应多功能网格平铺效果,而且结合其他特性,能实现更强大的布局效果。这也是业内很多人所说的,一行代码实现响应式布局的效果。当然,在这篇文章中,还花了一定的篇幅和大家探讨了auto-fitauto-fill之间的差异,以及工作原理。

我想,你要是能坚持阅读到这里,你对auto-fitauto-fill会有一定的理解,也能清楚的知道在实际运用中应该选择使用哪个。如果你在这方面有更多的建议或经验,欢迎在下面的评论中与我们共享。