自己在写的小项目中有瀑布流的需求,不久之前刚刚完成瀑布流的布局部分,这部分代码也已经上传到了Github gist。写的时候我就在思考:如果能有更优雅的方式快速实现瀑布流布局该多好。于是,我便想到了之前无聊时翻看MDN时,CSS Houdini里边所描述的CSS Layout API。正好最近刚写完瀑布流,实践起来比较方便。
warning
CSS Layout API目前还是First Public Working Draft,本文所述内容在将来随时可能过时。
warning
目前没有**任何**浏览器支持该特性,为了正常展示本文所述的所有demo,你需要使用edge/chrome浏览器并在flags中将Experimental Web Platform features启用。
〇. 结果
因为这篇文章前戏很长,所以将结果放在了最前面呈现,完整的示例可以前往 https://masonry.daidr.me 查看。

如果将来浏览器支持了该特性,那么使用瀑布流布局将会是一件易如反掌的事情,你需要做的,仅仅是
- 引入 masonry.js
- 准备一个父级容器,和一些瀑布流元素(例如卡片)
- 为这个父级元素加上一个布局样式。
Ⅰ. 一些新的知识
坑
我兴致冲冲地去MDN翻阅与CSS Layout API相关的文档,结果发现…居然什么都没有 🙄 …既然没有的话,直接去w3c上看看吧,于是,我打开了https://www.w3.org/TR/css-layout-api-1,结果经过我的一番尝试,连里边的示例都没法正常使用,才发现这个文档也过时了 😮
不过好在Editor’s Draft里面的内容一直在更新,这才让我有了继续写下去的动力。那么,让我们开始吧!
Typed OM
不知道大家在使用js操作样式时,是否会感到百般别扭:
因为返回的是字符串,进行运算的时候总是很狼狈,傻傻搞不清楚font-size
/fontSize
/margin-top
/marginTop
,更别提各种数值和单位的拼接,我已经不止一次犯过下面这样的错误了:
Typed OM便可以来解决我们直接操作CSSOM时发生的诸多不愉快。你可以通过元素的attributeStyleMap属性获取到一个StylePropertyMap对象,之后,便可以以map的方式读取元素的样式了。
返回的是一个CSSUnitValue对象(也可能是CSSMathValue或其子类的对象),我们可以很轻松地获取到属性值的数值部分,简化我们的操作。浏览器甚至能够自动转换em、rem等相对单位,得到绝对单位数值。我们还可以通过CSSUnitValue内置的to方法,进行快速的单位转换。不仅如此,浏览器还提供了大量的工厂方法来规范化表达css的属性值,比如我们的第一个例子,使用Typed OM进行操作就会是下面的样子。
舒服多了。在使用CSS Layout API的过程中,我们会经常看到Typed OM的身影。在MDN可以找到Typed OM相关的文档
CSS Properties and Values API
这个接口能够让我们注册一些自定义的css属性,并定义格式和默认值。
不仅可以在JavaScript中使用该接口,浏览器也提供了自定义属性值的 At Rule
自定义属性注册完成后,之后再通过Typed OM操作样式,浏览器便会按照你所提供的格式,返回对应的CSSUnitValue(或CSSMathValue)对象。倘若不这么做,浏览器将会返回一个携带原始css属性值的CSSUnparsedValue对象。
syntax字符串的内容其实很简单,syntax由一堆syntax component组成,默认情况下,syntax字段的内容是*。除此之外,还可以使用 | 来表示或, + 来表示接受使用空格分割的属性值, # 表示接受使用逗号分割的属性值。这里的syntax仅仅是Value Definition Syntax的一个子集。更详细的资料,可以去草案的第五节详细了解。
CSS Layout API
终于到了咱们的重头戏!布局的相关逻辑需要使用浏览器提供的Worklet接口,这个接口允许脚本独立于js运行环境,进行诸如绘图、布局、音频处理等需要高性能的操作。所以,我们需要一个脚本,用于将布局逻辑相关的代码载入到LayoutWorklet中。(别忘了检查一下浏览器兼容性)
接下来就是需要被载入到LayoutWorklet中的代码
这样我们就创建了一个名为masonry的布局方式,上面两段代码可以看作是一套模板,直接拿来用就行。
接下来就是噩梦了 😯 ,layout的这几个参数是什么,该如何操作?好在草案写得足够详细,也提供了一些示例以供参考。(这篇文章不会讨论breakToken的用法)
children
是一个许多LayoutChild对象组成的数组,代表着容器内的所有子元素。LayoutChild主要包含下面这些属性或方法
LayoutChild.intrinsicSizes()
返回一个promise,用以得到IntrinsicSizes对象,可以获取元素的最大/最小尺寸
LayoutChild.layoutNextFragment(constraints, breakToken)
返回一个promise,用以得到LayoutFragment对象,LayoutFragment对象主要包含下面这些属性:
- LayoutFragment.inlineSize:子元素内联方向上的尺寸,即宽度(只读)
- LayoutFragment.blockSize:子元素块级方向上的尺寸,即高度(只读)
- LayoutFragment.inlineOffset:子元素内联方向上的偏移
- LayoutFragment.blockOffset:子元素块级方向上的偏移,布局主要就靠这两个偏移了
LayoutChild.styleMap
返回一个StylePropertyMapReadOnly对象,用来操作子元素的样式
edges
是一个LayoutEdges对象(属性均只读),用来获取容器内外边距、滚动条导致的content box与border box产生的距离
- LayoutEdges.inlineStart:内联起始方向的距离
- LayoutEdges.inlineEnd:内联结束方向的距离
- LayoutEdges.blockStart:块级起始方向的距离
- LayoutEdges.blockEnd:块级结束方向的距离
- LayoutEdges.inline:内联方向的距离和
- LayoutEdges.block:块级方向的距离和
可能不是很直观,这里放一张草案里提供的rtl方向下的图(和ltr正好相反):

constraints
是一个LayoutConstraints对象(属性均只读),用来获取元素(这里是指容器)的尺寸信息
- LayoutConstraints.availableInlineSize:内联方向上的可用尺寸
- LayoutConstraints.availableBlockSize:块级方向上的可用尺寸
- LayoutConstraints.fixedInlineSize:内联方向上的确定尺寸
- LayoutConstraints.fixedBlockSize:块级方向上的确定尺寸
- LayoutConstraints.percentageInlineSize:内联方向上的尺寸(百分比表示)
- LayoutConstraints.percentageBlockSize:块级方向上的尺寸(百分比表示)
不过似乎目前浏览器提供的 LayoutConstraints 对象只能获取到 fixedInlineSize 和 fixedBlockSize 这两个属性…
styleMap
是一个 StylePropertyMapReadOnly 对象,用来操作容器的样式
Ⅱ. 开始实现瀑布流
使用CSS Layout API实现瀑布流的基本逻辑其实和其他实现方式基本是一致的。
我们先来定义两个自定义属性,方便之后进行属性值的格式化。
顺便把layout-masonry.js载入到layoutWorklet中
接下来的所有代码若没有额外说明则均在layout-masonry.js的layout逻辑内部。
首先,我们来获取容器的内容盒子宽度:
接下来,我们来获取瀑布流列数(因为值是整数且默认值为4,我们无需做任何处理,读进来就好)
接着,我们需要得到每列的间距,此时情况就复杂了。不过好在所有相对单位和绝对单位在传入时都会自动转换成px,所以实际上我们只需要处理百分比和calc函数,css里边的calc函数是支持嵌套的,所以我们这里使用递归来完成计算,同时将百分比转换为像素值。
我们需要根据列数和间隔计算出子元素的宽度
下面的代码可以算是模板,我们需要获取子元素的fragment,只有这样我们才可以修改子元素的偏移
紧接着,就是瀑布流的逻辑了,基本上所有瀑布流的逻辑是类似的。在我的Github gist中vue的版本也是这么实现的。我们需要记录每一列的当前高度,在布局新元素时,选取其中最短的一列进行插入操作(倘若按照顺序插入会导致每列的高度差距过大)
与普通瀑布流唯一的不同可能是在最后一步,我们需要更新容器的高度,所以每布局一个子元素,都尝试记录目前最高那列的高度。
最后,我们需要固定返回一个包含容器高度和子元素fragment的对象
注:按照草案中的描述,此处应该返回一个FragmentResult对象,但是目前没有任何一个浏览器实现了这个类…
完整的代码可以在文章开头的仓库中找到。