一、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有三个明显的缺陷:
- 不一定能实现或实现困难。
CSSOM开放给JavaScript的API很少,这意味着开发者能做的很有限,只能简单地操纵DOM并对样式做动态计算和调整,光是去实现一些复杂的CSS新特性的Polyfill就已经很难了,对于更深层次的Layout、Paint、Composite等渲染规则更是无能为力。所以当一个新的CSS特性被推出时,通过JavaScript Polyfill不一定能够完整地实现它。
- 实现效果差或有使用限制。
JavaScript Polyfill是通过JavaScript来模拟CSS特性的,而不是直接通过CSS引擎进行渲染,通常它们都会有一定的限制和缺陷。例如,大家熟知的css-scroll-snap-polyfill就是针对新的CSS特性Scroll Snap产生的Polyfill,但它在使用时就存在使用限制或者原生CSS表现不一致的问题。
- 性能较差。
JavaScript Polyfill可能造成一定程度的性能损耗。JavaScript Polyfill的执行时机是在DOM和CSSOM都构建完成并且完成渲染后,通常JavaScript Polyfill是通过给DOM元素设置内联样式来模拟CSS特性,这会导致页面的重新渲染或回流。尤其是当这些Polyfill和滚动事件绑定时,会造成更加明显的性能损耗。
三、Houdini APIs
上文提到CSS Houdini提供了很多CSS引擎相关的API,根据Houdini提供的规范说明文件,API共分为两种类型:high-level APIs
和 low-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()来引用。