许多CSS专家去年对新的砖块式布局功能的可能语法进行了深入讨论。当时存在两个主要阵营和一个在两者之间取得平衡的第三个阵营:
使用 display: masonry
使用 grid-template-rows: masonry
使用 item-pack: collapse
我认为他们还没有得出结论。但您可能需要知道的是,Firefox已经使用第二种语法支持砖块式布局。而Chrome正在使用第一种语法进行测试。虽然看到CSS Masonry的原生支持正在发展是一件很酷的事情,但如果其他浏览器不支持相同的实现,我们就真的无法在生产中使用它……
因此,与其加入其中一个阵营,我继续研究如何在其他浏览器中实现砖块式布局。我很高兴地报告我找到了一种方法——而且,额外奖励!——支持可以通过仅66行JavaScript提供。
在这篇文章中,我将向您展示它是如何工作的。但首先,这里有一个供您玩的演示,只是为了证明我并没有胡说八道。请注意,由于我们正在等待图片加载,会有轻微的延迟。如果您在顶部折叠处放置砖块式布局,请考虑不包括图片,因为这样!
这是什么魔法?
现在,这个演示中包含了很多东西,尽管只有66行JavaScript:
您可以定义任意数量的列的砖块式布局。 每个项目可以跨越多列。 我们在计算每个项目的大小之前等待媒体加载。 我们通过监听与 ResizeObserver
的变化使其具有响应性。
这些使我的实现非常健壮,适合生产使用,而且比互联网上许多Flexbox砖块式布局更灵活。
现在,一个提示。 如果您将其与Tailwind的响应式变体和任意值结合使用,您可以在不编写更多CSS的情况下将更多灵活性纳入这个砖块式网格。
好吧,在你更加兴奋之前,让我们回到主要问题:这到底是怎么工作的?
让我们从补丁开始
Firefox已经通过第二个阵营的语法支持砖块式布局。这是您需要在Firefox中创建CSS砖块式网格布局的CSS。
.masonry {
display: grid;
grid-template-columns: repeat(
auto-fit,
minmax(min(var(--item-width, 200px), 100%), 1fr)
);
grid-template-rows: masonry;
grid-auto-flow: dense; /* 可选,但建议 */
}
由于Firefox已经具有原生的砖块式支持,我们自然不应该干扰它。检查砖块式是否默认支持的最好方法是检查grid-template-rows
是否可以持有masonry
值。
function isMasonrySupported(container) {
return getComputedStyle(container).gridTemplateRows === 'masonry'
}
如果砖块式受支持,我们将跳过我们的实现。否则,我们将采取一些措施。
const containers = document.querySelectorAll('.masonry')
containers.forEach(async container => {
if (isMasonrySupported(container)) return
})
简单的砖块式布局
现在,我想先声明一下,我并不是发明这项技术的人。
当我浏览网页,寻找实现砖块式网格的方法时,我发现了这项技术。所以,称赞一下第一个开发这个想法的未知开发人员——也许还有我,因为我理解、转换并使用了它。
这项技术是这样的:
我们将 grid-auto-rows
设置为0px
。然后,我们将 row-gap
设置为1px
。然后,我们通过 getBoundingClientRect
获取项目的宽度。然后,我们将项目的“行分配”大小通过将 width
值与column-gap
值相加来确定。
这对于那些按照标准方式使用CSS Grid的人来说真的不直观。但一旦你理解了这一点,你也可以理解它是如何工作的!
现在,由于这一点很不直观,我们将一步一步地讲解,让您看到整个过程如何演变成最终输出。
逐步讲解
首先,我们将grid-auto-rows
设置为0px
。这很奇怪,因为每个网格项目实际上都有“零高度”。但同时,CSS Grid保持列和行的顺序!
containers.forEach(async container => {
// ...
container.style.gridAutoRows = '0px'
})
其次,我们将row-gap
设置为1px
。一旦我们这样做,您开始注意到行之间有一个初始堆叠,每个行都在前一个行下方一像素。
containers.forEach(async container => {
// ...
container.style.gridAutoRows = '0px'
container.style.setProperty('row-gap', '1px', 'important')
})
第三,假设网格项目中没有图片或其他媒体元素,我们可以轻松地使用getBoundingClientRect
获取每个网格项目的高度。
然后,我们可以通过将grow-row-end
替换为height
值来在CSS Grid中恢复网格项目的高度。这是因为每个row-gap
现在都是1px
高。
当我们这样做时,您可以看到网格开始成形。每个项目现在(有点)回到了它们各自的位置:
containers.forEach(async container => {
// ...
let items = container.children
layout({ items })
})
functionlayout({ items }) {
items.forEach(item => {
const ib = item.getBoundingClientRect()
item.style.gridRowEnd = `span ${Math.round(ib.height)}`
})
}
现在,我们需要恢复项目之间的行间隙。幸运的是,由于砖块式网格通常具有相同的column-gap
和row-gap
值,我们可以通过读取column-gap
值来获取所需的行间隙。
一旦我们这样做,我们将它添加到grid-row-end
中,以扩展项目在网格中占据的行数(即“高度”):
containers.forEach(async container => {
// ...
const items = container.children
const colGap = parseFloat(getComputedStyle(container).columnGap)
layout({ items, colGap })
})
functionlayout({ items, colGap }) {
items.forEach(item => {
const ib = item.getBoundingClientRect()
item.style.gridRowEnd = `span ${Math.round(ib.height + colGap)}`
})
}
就这样,我们制作了砖块式网格!从现在开始,所有内容都是为了使这个准备好用于生产。
等待媒体加载
尝试向任何网格项目添加图片,您会注意到网格会中断。这是因为项目的宽度将是“错误的”。
这是因为在图片正确加载之前,我们获取了height
值。DOM还不知道图片的尺寸。为了解决这个问题,我们需要在运行layout
函数之前等待媒体加载。
我们可以使用以下代码完成此操作(由于这不是CSS技巧,我将不解释):
containers.forEach(async container => {
// ...
try {
awaitPromise.all([areImagesLoaded(container), areVideosLoaded(container)])
} catch(e) {}
// 图片加载后运行布局函数
layout({ items, colGap })
})
// 检查图片是否加载
asyncfunctionareImagesLoaded(container) {
const images = Array.from(container.querySelectorAll('img'))
const promises = images.map(img => {
returnnewPromise((resolve, reject) => {
if (img.complete) returnresolve()
img.onload = resolve
img.onerror = reject
})
})
returnPromise.all(promises)
}
// 检查视频是否加载
functionareVideosLoaded(container) {
const videos = Array.from(container.querySelectorAll('video'))
const promises = videos.map(video => {
returnnewPromise((resolve, reject) => {
if (video.readyState === 4) returnresolve()
video.onloadedmetadata = resolve
video.onerror = reject
})
})
returnPromise.all(promises)
}
瞧,我们有一个可以与图片和视频一起工作的CSS砖块式网格!
使其具有响应性
这是一个简单的步骤。我们只需要使用ResizeObserver API
来监听砖块式网格容器的尺寸的任何变化。
当发生变化时,我们再次运行layout
函数:
containers.forEach(async container => {
// ...
const observer = new ResizeObserver(observerFn)
observer.observe(container)
function observerFn(entries) {
for (const entry of entries) {
layout({colGap, items})
}
}
})
这个演示使用了标准的Resize Observer API。但您可以使用我们前几天构建的精简的resizeObserver
函数使其更简单。
containers.forEach(async container => {
// ...
const observer = resizeObserver(container, {
callback () {
layout({colGap, items})
}
})
})
基本上就是这样!现在您有一个可以在支持CSS Grid的每个浏览器中使用的健壮的砖块式网格!
令人兴奋,不是吗?这个实现非常简单易用!
使用Splendid Labz的砖块式网格
如果您不反对使用他人编写的代码,也许您可以考虑在Splendid Labz中获取我为您构建的砖块式网格。
为此,请安装辅助库并添加必要的代码:
# 安装库
npm install @splendidlabz/styles
/* 导入所有布局代码 */
@import '@splendidlabz/layouts';
// 使用砖块式脚本
import { masonry } from '@splendidlabz/styles/scripts'
masonry()
最后一件事情:我一直构建了大量工具,以帮助使网页开发对我们每个人来说都更容易。我将它们全部放在Splendid Labz品牌下——而今天我向您展示的砖块式网格就是这些示例之一。
如果您喜欢这个,您可能对其他使布局变得超级简单的布局工具感兴趣。
希望您今天喜欢这篇文章。如果您愿意,请释放您的新CSS砖块式网格,祝您一切顺利!
原文地址:https://css-tricks.com/making-a-masonry-layout-that-works-today/
作者:Zell Liew

優(yōu)網(wǎng)科技秉承"專業(yè)團(tuán)隊(duì)、品質(zhì)服務(wù)" 的經(jīng)營(yíng)理念,誠(chéng)信務(wù)實(shí)的服務(wù)了近萬家客戶,成為眾多世界500強(qiáng)、集團(tuán)和上市公司的長(zhǎng)期合作伙伴!
優(yōu)網(wǎng)科技成立于2001年,擅長(zhǎng)網(wǎng)站建設(shè)、網(wǎng)站與各類業(yè)務(wù)系統(tǒng)深度整合,致力于提供完善的企業(yè)互聯(lián)網(wǎng)解決方案。優(yōu)網(wǎng)科技提供PC端網(wǎng)站建設(shè)(品牌展示型、官方門戶型、營(yíng)銷商務(wù)型、電子商務(wù)型、信息門戶型、微信小程序定制開發(fā)、移動(dòng)端應(yīng)用(手機(jī)站、APP開發(fā))、微信定制開發(fā)(微信官網(wǎng)、微信商城、企業(yè)微信)等一系列互聯(lián)網(wǎng)應(yīng)用服務(wù)。