Houdini - 揭秘 CSS

您是否曾想过 CSS 要完成的工作量?您更改一个属性,整个网站的布局突然就变了。这有点像魔法。到目前为止,我们 Web 开发者社区只能见证和观察这种魔力。如果我们想创造自己的魔法,该怎么办?如果我们想成为魔术师,该怎么做?

欢迎使用 Houdini!

Houdini 工作组由 Mozilla、Apple、Opera、Microsoft、HP、Intel 和 Google 的工程师组成,他们共同致力于向 Web 开发者公开 CSS 引擎的某些部分。该工作组正在处理一系列草稿,目标是让这些草稿获得 W3C 的批准,成为实际的 Web 标准。他们为自己设定了一些高级目标,并将其转换为规范草稿,进而产生了一组支持性的低级规范草稿。

当有人谈论“Houdini”时,通常是指这些草稿的集合。在撰写本文时,草稿列表尚未完成,其中一些草稿只是占位符。

规范

Worklet(规范

工作流本身并不实用。它们是一种概念,旨在为后续的许多草稿提供可能。如果您在读到“worklet”时想到了 Web Worker,那您没有错。它们在概念上有很多重叠之处。既然我们已经有了 Worker,为什么还要推出这个新功能?

Houdini 的目标是公开新的 API,以便 Web 开发者将自己的代码连接到 CSS 引擎和周围系统。假设其中一些代码段必须在每一帧运行,这可能并不不切实际。其中一些是根据定义必须提供的。引用 Web Worker 规范

这意味着 Web Worker 无法实现 Houdini 打算执行的操作。因此,我们发明了 Worklet。工作流利用 ES2015 类定义一组方法,这些方法的签名由工作流的类型预定义。它们是轻量级且短暂的。

CSS Paint API(规范

在 Chrome 65 中,Paint API 默认处于启用状态。阅读详细说明

合成器 worklet

此处介绍的 API 已废弃。我们重新设计了 compositor worklet,并将其命名为“Animation Worklet”。详细了解 API 的当前迭代

尽管 compositor worklet 规范已移至 WICG 并将进行迭代,但它是我最为兴奋的规范。某些操作由 CSS 引擎外包到计算机的显卡,但这通常取决于您的显卡和设备。

浏览器通常会采用 DOM 树,并根据特定条件决定为一些分支和子树提供自己的层。这些子树会将自己绘制到该 Surface 上(未来可能会使用绘制 Worklet)。最后一步是,将所有这些现已绘制的图层堆叠并彼此叠加,并遵循 z 深度索引、3D 转换等,以生成屏幕上显示的最终图片。此过程称为“合成”,由合成器执行。

合成过程的优势在于,您无需在页面稍微滚动时让所有元素自行重绘。不过,您可以重复使用上一个帧中的层,只需使用更新后的滚动位置重新运行合成器即可。这样可以加快速度。这有助于我们达到 60fps。

合成器 Worklet。

顾名思义,借助混合渲染器 Worklet,您可以钩入混合渲染器,并影响已绘制的元素层在其他层之上的排列方式和层次结构。

更具体地说,您可以告知浏览器您想钩入特定 DOM 节点的绘制流程,并请求访问滚动位置、transformopacity 等特定属性。这会强制将此元素放置在自己的层上,并且在每个帧都会调用您的代码。您可以通过操控图层转换来移动图层,并更改其属性(例如 opacity),从而以高达 60 fps 的速度执行各种精彩的操作。

以下是使用合成器 Worklet 实现视差滚动的完整实现。

// main.js
window.compositorWorklet.import('worklet.js')
    .then(function() {
    var animator = new CompositorAnimator('parallax');
    animator.postMessage([
        new CompositorProxy($('.scroller'), ['scrollTop']),
        new CompositorProxy($('.parallax'), ['transform']),
    ]);
    });

// worklet.js
registerCompositorAnimator('parallax', class {
    tick(timestamp) {
    var t = self.parallax.transform;
    t.m42 = -0.1 * self.scroller.scrollTop;
    self.parallax.transform = t;
    }

    onmessage(e) {
    self.scroller = e.data[0];
    self.parallax = e.data[1];
    };
});

Robert Flack 为 compositor worklet 编写了 polyfill,您可以试一试,不过显然性能会受到更大影响。

布局 Worklet(规范

第一个真正的规范草稿已提出。实施还需要一段时间。

同样,它的规范实际上为空,但其概念很有趣:编写您自己的布局!布局 Worklet 应该可让您执行 display: layout('myLayout') 并运行 JavaScript,以便在节点的框中排列节点的子项。

当然,运行 CSS 的 flex-box 布局的完整 JavaScript 实现比运行等效的原生实现要慢,但不难想象,在某些情况下,偷工减料可以提升性能。假设有一个网站,其中只有功能块,例如 Windows 10 或砖块式布局。不使用绝对定位和固定定位,也不使用 z-index,并且元素绝不会重叠或具有任何类型的边框或溢出。如果能够在重新布局时跳过所有这些检查,则可以提升性能。

registerLayout('random-layout', class {
    static get inputProperties() {
        return [];
    }
    static get childrenInputProperties() {
        return [];
    }
    layout(children, constraintSpace, styleMap) {
        const width = constraintSpace.width;
        const height = constraintSpace.height;
        for (let child of children) {
            const x = Math.random()*width;
            const y = Math.random()*height;
            const constraintSubSpace = new ConstraintSpace();
            constraintSubSpace.width = width-x;
            constraintSubSpace.height = height-y;
            const childFragment = child.doLayout(constraintSubSpace);
            childFragment.x = x;
            childFragment.y = y;
        }

        return {
            minContent: 0,
            maxContent: 0,
            width: width,
            height: height,
            fragments: [],
            unPositionedChildren: [],
            breakToken: null
        };
    }
});

类型化 CSSOM(规范

类型化 CSSOM(CSS 对象模型或级联样式表对象模型)解决了我们可能都遇到过并且刚刚学会应付的问题。我用一行 JavaScript 代码来举例说明:

    $('#someDiv').style.height = getRandomInt() + 'px';

我们要进行数学运算,将数字转换为字符串以附加单位,目的是让浏览器解析该字符串,并将其转换回 CSS 引擎的数字。如果使用 JavaScript 操控转换,情况会更糟。不用了!CSS 即将开始输入。

此草稿是较为成熟的草稿之一,我们已经在开发 polyfill。(免责声明:使用 polyfill 显然会增加更多计算开销。关键在于展示该 API 有多方便。)

您将处理元素的 StylePropertyMap,而不是字符串,其中每个 CSS 属性都有自己的键和相应的值类型。width 等属性的值类型为 LengthValueLengthValue 是所有 CSS 单位(例如 emrempxpercent 等)的字典。设置 height: calc(5px + 5%) 会产生 LengthValue{px: 5, percent: 5}。某些属性(如 box-sizing)只接受某些关键字,因此具有 KeywordValue 值类型。然后,您可以在运行时检查这些属性的有效性。

<div style="width: 200px;" id="div1"></div>
<div style="width: 300px;" id="div2"></div>
<div id="div3"></div>
<div style="margin-left: calc(5em + 50%);" id="div4"></div>
var w1 = $('#div1').styleMap.get('width');
var w2 = $('#div2').styleMap.get('width');
$('#div3').styleMap.set('background-size',
    [new SimpleLength(200, 'px'), w1.add(w2)])
$('#div4')).styleMap.get('margin-left')
    // => {em: 5, percent: 50}

属性和值

规范

您了解 CSS 自定义属性(或其非官方别名“CSS 变量”)吗? 这些只不过是类型而已!到目前为止,变量只能使用字符串值,并使用了简单的“搜索并替换”方法。此草稿不仅可以让您指定变量的类型,还可以定义默认值并使用 JavaScript API 影响继承行为。从技术层面来说,这还允许使用标准 CSS 转换和动画为自定义属性添加动画效果,我们也在考虑这一点。

["--scale-x", "--scale-y"].forEach(function(name) {
document.registerProperty({
    name: name,
    syntax: "<number>",
    inherits: false,
    initialValue: "1"
    });
});

字体指标

字体测量参数的含义与字面意思完全相同。当我使用字体 Y 以大小 Z 渲染字符串 X 时,边界框(或边界框)是什么?如果我使用 Ruby 注解,该怎么做?用户一直在请求此功能,Houdini 应该终于能实现这些愿望了。

但不止如此!

Houdini 的草稿列表中还有更多规范,但这些规范的未来发展前景相当不确定,它们只不过是一些想法的占位符。示例包括自定义溢出行为、CSS 语法扩展 API、原生滚动行为的扩展,以及类似的雄心勃勃的功能,所有这些都可以在网络平台上实现以前无法实现的功能。

演示

我已开源演示代码(使用 polyfill 进行实时演示)。