一、Houdini 是什么

Houdini(/huːˈdiːni/)的出现最直接的目的是为了解决浏览器对新的CSS特性支持较差以及Cross-Browser的问题。 我们知道有很多新的CSS特性虽然很棒,但它们由于不被主流浏览器广泛支持而很少有人去使用。

随着CSS规范在不断地更新迭代,越来越多有益的特性被纳入进来,但是一个新的CSS特性从被提出到成为一个稳定的CSS特性,需要经过漫长地等待,直到被大部分浏览器支持时,才能被开发者广泛地使用。

而 Houdini 的出现正是洞察和解决了这一痛点,它将一系列CSS引擎API开放出来,让开发者可以通过JavasScript创造或者扩展现有的CSS特性,甚至创造自己的CSS渲染规则,给开发者更高的CSS开发自由度,实现更多复杂的效果。

Houdini的名称与一位著名美国逃脱魔术师Harry Houdini的名称一样,也许正是取逃脱之意,让CSS新特性逃离浏览器的掌控。

简单来说:CSS Houdini是一组新的低级API,它们使开发者能够更直接地与浏览器的CSS引擎进行交互。简单地说,Houdini试图”开放”浏览器的CSS引擎,让开发者能够扩展CSS,并将其能力推向前所未有的境地。对于自己想用的CSS功能不用必须等到浏览器支持之后了。

二、JS Polyfill vs Houdini

有人会问,实际上很多新的CSS特性在被浏览器支持之前,也有可替代的JavaScript Polyfill可以使用,为什么我们仍然需要Houdini呢?这些Polyfill不是同样可以解决我们的问题吗?

要回答这个问题也很简单,JavaScript Polyfill相对于Houdini有三个明显的缺陷:

  1. 不一定能实现或实现困难。

CSSOM开放给JavaScript的API很少,这意味着开发者能做的很有限,只能简单地操纵DOM并对样式做动态计算和调整,光是去实现一些复杂的CSS新特性的Polyfill就已经很难了,对于更深层次的Layout、Paint、Composite等渲染规则更是无能为力。所以当一个新的CSS特性被推出时,通过JavaScript Polyfill不一定能够完整地实现它。

  1. 实现效果差或有使用限制。

JavaScript Polyfill是通过JavaScript来模拟CSS特性的,而不是直接通过CSS引擎进行渲染,通常它们都会有一定的限制和缺陷。例如,大家熟知的css-scroll-snap-polyfill就是针对新的CSS特性Scroll Snap产生的Polyfill,但它在使用时就存在使用限制或者原生CSS表现不一致的问题。

  1. 性能较差。

JavaScript Polyfill可能造成一定程度的性能损耗。JavaScript Polyfill的执行时机是在DOM和CSSOM都构建完成并且完成渲染后,通常JavaScript Polyfill是通过给DOM元素设置内联样式来模拟CSS特性,这会导致页面的重新渲染或回流。尤其是当这些Polyfill和滚动事件绑定时,会造成更加明显的性能损耗。

三、Houdini APIs

上文提到CSS Houdini提供了很多CSS引擎相关的API,根据Houdini提供的规范说明文件,API共分为两种类型:high-level APIslow-level APIs

1. high-level APIs

顾名思义是高层次的API,这些API与浏览器的渲染流程相关。

(1)Paint API

提供了一组与绘制(Paint)过程相关的API,我们可以通过它自定义的渲染规则,例如调整颜色(color)、边框(border)、背景(background)、形状等绘制规则。

(2)Animation API

提供了一组与合成(composite)渲染相关的API,我们可以通过它调整绘制层级和自定义动画。

(3)Layout API

提供了一组与布局(Layout)过程相关的API,我们可以通过它自定义的布局规则,类似于实现诸如flex、grid等布局,自定义元素或子元素的对齐(alignment)、位置(position)等布局规则。

(这边高低API的划分还有待考究)

2. low-level APIs

低层次的API,这些API是high-level APIs的实现基础。

(1)Typed Object Model API

在Houdini出现以前,我们通过JavaScript操作CSS Style的方式很简单:

1
2
3
4
const size = 30
const imgUrl = 'https://www.exampe.com/sample.png'
target.style.cssText = 'font-size:' + size + 'px; background: url('+ imgUrl +')'
// "font-size:30px; background: url(https://www.exampe.com/sample.png)"

我们可以看到CSS样式在被访问时被解析为字符串返回,设置CSS样式时也必须以字符串的形式传入。开发者需要手动拼接数值、单位、格式等信息,这种方式非常原始和落后,很多开发者为了节省性能损耗,会选择将一长串的CSS Style字符串传入cssText,可读性很差,而且很容易产生隐蔽的语法错误。
相比于上面晦涩的传统方法,Typed Object Model将CSS属性值包装为Typed JavaScript Object,让每个属性值都有自己的类型,简化了CSS属性的操作,并且带来了性能上的提升。

1
2
3
4
5
// 要访问和操作CSSStyleValue还需要借助两个工具,分别是attributeStyleMap和computedStyleMap(),前者用于处理内联样式,可以进行读写操作,后者用于处理非内联样式(stylesheet),只有读操作。
// 获取stylesheet样式
target.computedStyleMap().get("font-size"); // { value: 30, unit: "px"}
// 设置内联样式
target.attributeStyleMap.set("font-size", CSS.em(5));

(2)CSS Properties & Values API

根据MDN的定义,CSS Properties & Values API也是Houdini开放的一部分API,它的作用是让开发者显式地声明自定义属性(css custom properties),并且定义这些属性的类型、默认值、初始值和继承方法。

1
2
3
4
5
6
7
8
9
10
11
--my-color: red;

// 在被声明之后,这些自定义属性可以通过var()来引用,例如:

// 在:root下可声明全局自定义属性
:root {
--my-color: red;
}
#container {
background-color: var(--my-color)
}

我们能否通过自定义属性来帮助我们完成一些过渡效果呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// DOM
<div id="container">container</div>
// Style
:root {
--my-color: red;
}
#container {
transition: --my-color 1s;
background-color: var(--my-color)
}
#container:hover {
--my-color: blue;
}

// 在鼠标hover经过container的时候,红色的背景色并没有自动修改为蓝色。原因是浏览器不知道该如何去解析--my-color这个变量(因为它并没有明确的类型,只是被当做字符串处理)

但是,通过CSS Properties & Values API提供的CSS.registerProperty()方法就可以做到,就像这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// DOM
<div id="container">container</div>

// JavaScript
CSS.registerProperty({
name: '--my-color', // 变量的名字,不允许重复声明或者覆盖相同名称的变量,否则浏览器会给出相应的报错。
syntax: '<color>', // 告诉浏览器如何解析这个变量。它的可选项包含了一些预定义的值等。此处是syntax告诉浏览器把--my-color当做color去解析
inherits: false, // 告诉浏览器这个变量是否继承它的父元素。
initialValue: '#c0ffee', // 设置该变量的初始值,并且将该初始值作为fallback。
});

// Style
#container {
transition: --my-color 1s;
background-color: var(--my-color)
}
#container:hover {
--my-color: blue;
}

(3)Worklets

Worklets是轻量级的 Web Workers,它提供了让开发者接触底层渲染机制的API,Worklets的工作线程独立于主线程之外,适用于做一些高性能的图形渲染工作。并且它只能被使用在HTTPS协议中(生产环境)或通过localhost来启用(开发调试)。

Worklets不像Web Workers,我们不能将任何计算操作都放在Worklets中执行,Worklets开放了特定的属性和方法,让我们能处理图形渲染相关的操作。我们能使用的Worklet类型暂时有如下几种:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
1、PaintWorklet - Paint API
Paint API允许开发者通过Canvas 2d的方法来绘制元素的背景、边框、内容等图形,这在原始的CSS规则中是无法做到的。

2、LayoutWorklet - Animation API
在过去,当我们想要对DOM元素执行动画时,通常只有两个选择:CSS Transitions和CSS Animations。这两者在使用上虽然简单,也能满足大部分的动画需求,但是它们有两个共同的缺点:
(1)仅仅依赖时间来执行动画(time-driven):动画的执行仅和时间有关。
(2)无状态(stateless):开发者无法干预动画的执行过程,获取不到动画执行的中间状态。
Animation API在功能方面,它是CSS Transitions和CSS Animations的扩展,它允许用户干预动画执行的过程,例如结合用户的scroll、hover、click事件来控制动画执行,像是为动画增加了进度条,通过进度条控制动画进程,从而实现一些更加复杂的动画场景。
在性能方面,它依赖于AnimationWorklet,运行在单独的Worklet线程,因此具有更高的动画帧率和流畅度,这在低端机型中尤为明显

3、AnimationWorklet - Layout API
Layout API允许用户自定义新的布局规则,创造类似flex、grid之外的布局。

4、AudioWorklet - Audio API(处于草案阶段,暂不介绍)

Worklets提供了唯一的方法Worklet.addModule(),这个方法用于向Worklet添加执行模块,具体的使用方法,我们在后续的Paint API、Layout API、Animation API中介绍。

(4)Font Metrics API

目前处于草案阶段,但未来Font Metrics API 将会提供一系列API,允许开发者干预文字的渲染过程,创建文字或者动态修改文字的渲染效果等。

(5)CSS Parser API

它将会提供更多CSS解析器相关的API,用于解析任意形式的CSS描述。

四、利用Houdini实现一个动态波浪纹效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<!DOCTYPE html>
<html lang=en>

<head>
<meta charset=utf-8>
<meta http-equiv=X-UA-Compatible content="IE=edge">
<meta name=viewport content="width=device-width,initial-scale=1">
</head>
<style>
#wave {
width: 20%;
height: 70vh;
margin: 10vh auto;
background-color: #ff3e81;
background-image: paint(wave); /* CSS 中使用的时候,只需要调用 paint 方法 */
}
</style>

<body>
<div id="wave"></div>
<script>
/* HTML 中通过 Worklets 载入样式的自定义代码
Worklets 也是 Houdini 提供的 API 之一,负责加载和执行样式的自定义 JS 代码。
它类似于 Web Worker,是一个运行于主代码之外的独立工作进程,但比 Worker 更为轻量,负责 CSS 渲染任务最为合适。
*/
if ("paintWorklet" in CSS) {
CSS.paintWorklet.addModule("paintworklet.js"); // 加载 paintworklet.js
const wave = document.querySelector("#wave");
let tick = 0;
requestAnimationFrame(function raf(now) {
tick += 1;
wave.style.cssText = `--animation-tick: ${tick};`;
requestAnimationFrame(raf);
});
}
</script>
</body>

</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// paintworklet.js
registerPaint('wave', class {
static get inputProperties() {
return ['--animation-tick'];
}

/* 定义了一个wave的paint类,当 wave 被使用时,会实例化 wave 并自动触发 paint 方法执行渲染。*/
paint(ctx, geom, properties) { // ctx 参数是一个 Canvas 的 Context 对象,因此 paint 的逻辑跟 Canvas 的绘制方式一样。
let tick = Number(properties.get('--animation-tick')); // 获取节点 CSS 定义的 --animation-tick 变量
const {
width,
height
} = geom;
const initY = height * 0.4;
tick = tick * 2;


ctx.beginPath();
ctx.moveTo(0, initY + Math.sin(tick / 20) * 10);
for (let i = 1; i <= width; i++) {
ctx.lineTo(i, initY + Math.sin((i + tick) / 20) * 10);
}
ctx.lineTo(width, height);
ctx.lineTo(0, height);
ctx.lineTo(0, initY + Math.sin(tick / 20) * 10);
ctx.closePath();


ctx.fillStyle = 'rgba(255, 255, 255, 0.5)';
ctx.fill();
}
})

五、总结

Houdini是CSS开放的一组新的底层API,作用是让开发者扩展现有的CSS特性,或者实现一些CSS原生没有或浏览器不支持的一些效果。JS Polyfill也可以实现浏览器不支持的一些CSS效果,但是缺点是polyfill不是直接通过CSS引擎进行渲染,效率低。常用的Houdini的特性就是CSS中自定义属性,然后通过var()来引用。