研究了下Houdini中的CSS Layout API

这篇文章发布于 2020年09月13日,星期日,23:13,归类于 CSS相关。 阅读 4164 次, 今日 6 次 10 条评论

 

CSS layout自定义布局

一、基本概念和一些属性API

CSS Layout API可以让开发者自定义布局方式,例如实现瀑布流布局效果,全新的表格布局效果等。

CSS Layout API的使用分为两部分。

第1部分是在CSS中设置自定义的布局名称,和Flex布局、Grid布局一样,也是使用display属性,区别在于CSS Layout API自定义的布局使用layout()函数表示。

例如自定义一个瀑布流布局名为'masonry',则在CSS中使用的语法就会是下面这样:

.container {
    display: layout(masonry);
}

第2部分是使用JavaScript书写关于如何布局的功能模块。CSS Layout API已经约定好了布局模块书写的语法,如果单看规范文档,会觉得像天书一样,无从下手,实际上,自定义布局的代码书写是有固定的套路的,也就是大体的JavaScript代码框架都是固定的,只需要在指定的函数位置书写对容器元素以及容器子元素的位置和尺寸进行设置的代码就可以实现想要的效果了。

举个例子,要自定义一个瀑布流布局效果,JavaScript部分的代码该如何书写呢?

首先在页面中需要调用关于瀑布流布局的模块,可以使用layoutWorklet,具体代码如下所示:

if ('layoutWorklet' in CSS) {
    // 把自定义的瀑布流布局脚本添加到Layout Worklet中
    CSS.layoutWorklet.addModule('masonry.js');
}

然后就是重点也是难点所在,masonry.js的代码该如何书写?其实基本结构非常简单,代码如下所示:

registerLayout('masonry', class {
    async layout(children, edges, constraints, styleMap, breakToken) {
        // 这里写瀑布流布局相关代码
    }
});

从上面的例子可以看出,CSS Layout API大的框架并不难理解,上手成本几乎没有。但是要想玩转CSS Layout API,我可以断言,大部分的前端开发者够呛,因为其中关于定位和布局的API是需要对CSS的尺寸体系有一定程度的了解才知道是什么意思的。

所以我觉得有必要整理下CSS Layout API中出现的属性名称及其对应的含义(假设在默认的文档流方向下),详见下表。

表1 CSS Layout API中你需要了解的属性名称和含义
属性名 对应含义
inlineSize 内联方向的尺寸,对应于width属性。
blockSize 块级方向的尺寸,对应于height属性。
inlineOffset 相对于容器元素在内联方向的偏移,默认是左侧偏移的大小。
blockOffset 块级方向的偏移,默认是顶部偏移的大小。
minContentSize 最小内容尺寸。
maxContentSize 最大内容尺寸。
availableInlineSize 可用内联方向的尺寸,通常表示可用宽度。
availableBlockSize 可用块级方向的尺寸,通常表示可用高度。
fixedInlineSize 固定的内联方向的尺寸,水平方向的宽度通常是可以确定的,因此即使没有设置width属性也是有值。
fixedBlockSize 固定的块级方向的尺寸,如果height设置的是auto,属性值会是null
percentageInlineSize 内联方向的百分比尺寸。
percentageBlockSize 块级方向的百分比尺寸。
inlineStart 水平起始方向content box外边缘到border box外边缘的距离,在默认文档流下就是padding-leftborder-width-left外加滚动条宽度(如果有)的计算值之和。
inlineEnd 水平结束方向content box外边缘到border box外边缘的距离。
blockStart 垂直起始方向content box外边缘到border box外边缘的距离。
blockEnd 垂直结束方向content box外边缘到border box外边缘的距离。
inline 整个水平方向content box外边缘到border box外边缘的距离之和。
block 整个垂直方向content box外边缘到border box外边缘的距离之和。

还记不记得在展示CSS自定义布局模块基本结构代码那里出现了下面这部分JavaScript代码:

layout(children, edges, constraints, styleMap, breakToken)

这个layout()方法是整个CSS Layout API的核心所在,其中出现了5个参数,理解了上面这5个参数值,CSS Layout API也就理解了80%。

其中,前3个参数与表1中的这些属性密切相关,后1个参数styleMap用户获取外部设置的样式,主要是用来获取外部设置的CSS自定义属性值,最后1个参数breakToken用在打印等常见,大家可以暂时不用关心。

所以,接下来的重点会介绍childrenedgesconstraintsstyleMap这4个参数。

二、layout()参数值之间的逻辑关系

1. 参数children

参数children表示设置了display:layout(zhangxinxu)元素的子元素们,包含了子元素布局相关的一些信息。

我们可以通过循环获取所有子元素的布局信息,例如:

children.forEach(child => {
    // child干嘛干嘛
});

上面代码中出现的变量child包含1个属性和2个方法,可以用户获取子元素的尺寸等信息,具体如下:

  • styleMap
  • child.intrinsicSizes()
  • child.layoutNextFragment(constraints, breakToken)

其中:

child.styleMap
可以用来获取子元素的样式信息,具体参见接下来会介绍的“参数styleMap”。
child.intrinsicSizes()

此方法返回的是一个Promise对象,称为IntrinsicSizes对象,支持下面这2个属性:

  • IntrinsicSizes.minContentSize(只读)
  • IntrinsicSizes.maxContentSize(只读)

也就是child.intrinsicSizes()返回的是最小内容尺寸和最大内容尺寸的大小。

child.layoutNextFragment(constraints, breakToken)
此方法返回的也是一个Promise对象,称为LayoutFragment对象,其中包括下面2个只读属性和2个可写属性:

  • LayoutFragment.inlineSize(只读)
  • LayoutFragment.blockSize(只读)
  • LayoutFragment.inlineOffset(可写)
  • LayoutFragment.blockOffset(可写)

在CSS Layout API的实际应用中,LayoutFragment.inlineOffsetLayoutFragment.blockOffset是2个高频使用的属性,因为可以对子元素的偏移位置进行设置,实现想要的布局定位效果。

child.layoutNextFragment()方法中的constraints参数就是指的接下来要介绍的layout()方法中constraints参数。

2. 参数edges

edges称为LayoutEdges对象,所有的属性都是只读的,用来返回容器元素内容边缘到边框边缘的距离大小,支持下面这些属性:

  • LayoutEdges.inlineStart(只读)
  • LayoutEdges.inlineEnd(只读)
  • LayoutEdges.blockStart(只读)
  • LayoutEdges.blockEnd(只读)
  • LayoutEdges.inline(只读)
  • LayoutEdges.block(只读)

各个属性的含义参见表1,当使用LayoutFragment.inlineOffsetLayoutFragment.blockOffset对子元素进行定位的时候往往需要用到LayoutEdges对象,以确保定位的精确,因为LayoutFragment.inlineOffsetLayoutFragment.blockOffset定位是相对于border box边缘的。

3. 参数constraints

constraints称为LayoutConstraints对象,所有的属性都是只读的,用来返回布局容器的尺寸信息,这些属性包括下面这些:

  • LayoutConstraints.availableInlineSize(只读)
  • LayoutConstraints.availableBlockSize(只读)
  • LayoutConstraints.fixedInlineSize(只读)
  • LayoutConstraints.fixedBlockSize(只读)
  • LayoutConstraints.percentageInlineSize(只读)
  • LayoutConstraints.percentageBlockSize(只读)

各个属性的含义参见表1,其中需要注意的是上面这些属性浏览器并非全部都支持,目前Chrome仅支持fixedInlineSizefixedBlockSize这两个属性,其他属性暂时还不能使用,以后可能会支持。

4. 参数styleMap

styleMap称为StylePropertyMapReadOnly对象,是一个不包含set()clear()等写入方法的类Map结构的数据类型,主要用来获取容器元素或者子元素的常规CSS属性值或者CSS自定义属性值,多使用get()方法单个获取,例如有如下CSS代码:

.container {
    --gap: 10;
    display: layout(someLayout);
}

此时使用styleMap.get('--gap')就获得--gap的属性值。

需要注意的是,styleMap获得的CSS属性需要提前指定好,通过静态属性inputProperties完成,例如:

registerLayout('someLayout', class {
    // 指定可以输入的CSS属性
    static inputProperties = ['line-height'];
    async layout(children, edges, constraints, styleMap, breakToken) {
        // 可以得到容器元素的行高计算值大小
        let lineHeightParse = styleMap.get('line-height');
    }
});

三、文本居中同时一侧对齐的布局案例

上面的参数和属性的介绍属于基础理论知识,可以让我们了解CSS Layout API的精神内核,接下来就是通过具体的案例让大家知道具体该如何使用。

由于瀑布流布局的案例过于复杂,因此,这里我会举一个更简单也更实用的案例演示下如何使用CSS Layout API自定义一种全新的布局效果。

已知有一串数字列表,相关HTML如下所示:

<section align="right">
    <p>102.00</p>
    <p>23.80</p>
    <p>12,334.00</p>
    <p>2.88</p>
    <p>99.99</p>
</section>

为了方便一看看出数字的大小,这个列表显然需要右对齐,但是由于列表宽度较宽,简单的右对齐可能会有大量留白不好看,此时产品经理就希望这列数字个体右对齐的同时整体居中对齐,效果如下图所示。

在过去只能通过在列表元素的外面再包裹一层元素的方法实现,现在有了CSS Layout API我们就可以自已创造一种对齐布局方式,比方说我们定义这种全新的布局名称是center,于是就可以对列表容器元素进行如下所示的设置:

section {
    display: layout(center);
}

新建一个名为layout-center.js用来书写布局代码,然后在页面中引入该文件模块:

CSS.layoutWorklet.addModule('layout-center.js');

layout-center.js的代码如下所示:

registerLayout('center', class {
    // 需要获取相应值的CSS属性
    static inputProperties = ['line-height', 'text-align'];
    // 需要,不能省略
    async intrinsicSizes(children, edges, styleMap) {}
    // 主布局方法
    async layout(children, edges, constraints, styleMap, breakToken) {
        // 外部CSS属性值获取,主要是行高和对齐方式
        let lineHeight = styleMap.get('line-height').value;
        let textAlign = styleMap.get('text-align').value;
        // 返回所有子元素的内容长度数据
        const childrenSizes = await Promise.all(children.map((child) => {
            return child.intrinsicSizes();
        }));
        // 求得最大内容宽度,对齐需要
        const maxContentSize = childrenSizes.reduce((max, childSizes) => {
            return Math.max(max, childSizes.maxContentSize);
        }, 0) + edges.inline;

        // 下面这4个const语句是固定且必要的
        const availableInlineSize = constraints.fixedInlineSize - edges.inline;
        const availableBlockSize = constraints.fixedBlockSize ?
            constraints.fixedBlockSize - edges.block : lineHeight;
        const childConstraints = { availableInlineSize, availableBlockSize };
        const childFragments = await Promise.all(children.map((child) => {
            return child.layoutNextFragment(childConstraints);
        }));

        // 垂直偏移的起始距离
        let blockOffset = edges.blockStart;
        // 设置每一个子元素的垂直偏移大小
        childFragments.forEach((fragment, index) => {
            // 设置当前子元素的水平偏移大小
            fragment.inlineOffset = Math.max(0, availableInlineSize - maxContentSize) / 2;
            // 右对齐需要增加最大内容尺寸的偏差值
            if (textAlign == 'right' || textAlign == 'end') {
                fragment.inlineOffset += (maxContentSize - childrenSizes[index].maxContentSize);
            }            
            // 设置当前子元素的垂直偏移大小
            fragment.blockOffset = blockOffset;
            // 偏移递增
            blockOffset += lineHeight;
        });

        // 最终容器元素的高度大小
        const autoBlockSize = blockOffset + edges.blockEnd;

        return {
            autoBlockSize,
            childFragments,
        };
    }
});

各个语句的含义均在上面代码中使用注释描述了,相信不难理解。

自定义模块布局代码中有时候会使用一个名为layoutOptions的静态对象,可以指定子元素的显示类型和基本尺寸表现(例如类似于块级元素):

registerLayout('center', class {
    static inputProperties = ['line-height', 'text-align'];
    static layoutOptions = {
        childDisplay: 'normal',
        sizing: 'block-like'
    };
    ...
});

上面的例子有可访问的demo演示页面,您可以狠狠地点击这里:CSS Layout API实现列表居中右对齐demo

CSS Layout API目前在Blink内核浏览器下才有效果,例如Chrome浏览器或者改用Blink内核的Microsoft Edge浏览器,如果你的浏览器版本比较新,但是却看不到效果,请尝试使用Canary金丝雀版本,或者在地址栏输入chrome://flags然后开启Experimental Web Platform features这一项进行体验。

上面的演示页面还演示了居中同时左对齐的效果,具体如下图所示。

CSS Layout API还有不少其他细节知识,例如元素设置了display:layout(zhangxinxu)之后,其原本的尺寸就会崩塌,不习惯的开发者可能会比较茫然,不知道发生了什么事情,需要在JavaScript代码中以autoBlockSize属性形式把元素的高度进行返回。

又例如由于布局效果需要在JavaScript模块引入之后执行,因此在页面加载的过程中会看到页面内容跳动的糟糕体验问题出现,还需要通过一些技术手段规避(上面的实例页面是通过margin负值实现的)。

另外,实际开发的时候,CSS Layout API更多的是与CSS自定义属性配合使用,这样会表现出更加灵活的动态特性,例如如果我们使用CSS Layout API实现瀑布流布局效果,那么各个元素之间的间隙,每一栏的宽度等就需要CSS自定义属性进行设置。这样,当需要调整间隙或者宽度的时候,只需要修改对应的CSS自定义属性值就可以了,CSS变量的语义和优势就发挥出来了。

四、最后的点评

最后,对CSS Layout API进行点评下,CSS Layout API是非常强大的特性,可以让Web布局有更多的想象空间,由于现在浏览器还没有完全开放给用户,因此暂时实用性还不足。同时由于学习成本比较高,需要CSS和JavaScript同时有一定的造诣才能驾驭,因此,CSS Layout API以后注定是小部分开发者的玩具,最终出现的局面一定是少部分人创造,大部分人直接使用。

好了,本文内容就这样,感谢阅读,欢迎分享。

(本篇完)

分享到:


发表评论(目前10 条评论)

  1. tcdona说道:

    学习了 ~

  2. shiyang说道:

    “最后1个参数breakToken用在打印等常见”
    应该是个病句

  3. 代码如诗如画说道:

    好久没更新了

  4. cc说道:

    有点像在手写 canvas的排版
    提高了前端对于底层API的控制能力,但是同时对图形 和几何学有了更高的要求

  5. 某某某说道:

    想到当年IE的expression…

  6. 码农说道:

    有时容器或者body没有设置line-height为具体数值会失效,加个容错处理
    let lineHeight = styleMap.get(‘line-height’).value||1.8;

  7. ziven27说道:

    上手有点高

  8. liyongleihf2006说道:

    这个太厉害了

  9. ruiiiii说道:

    兼容性可以简单提一下吗?chrome 80.0.3987.162 好像不兼容,demo没有效果。