浅尝 CSS Houdini

前言

近几年,浏览器的特性频出,特别是一些开放浏览器底层控制的API规范,比如:WebAssemblyCSS HoudiniWebGL2.0等;这些底层API使得我们有机会定制更多的内容,而且拥有更好的性能,因此也就获得了更多的创造性。

关于CSS Houdini也是近两年才听的比较多,只知道大概是一个自定义CSS属性的利器,也没比较详细的去了解,算是比较前沿的CSS规范了;看过一些案例后,觉得CSS Houdini确实能够做出一些十分强大的展示和动效,是时候深入了解一下了。

CSS Houdini 现状

img

上图[1]是截至目前为止(2020-7-11)各浏览器厂商对于CSS Houdini规范各个模块的支持情况,可以看到目前只有blink内核(chrome系)浏览器对于CSS Houdini有较好的支持,而webkit内核则是处于开发中,firefox那就只是有意图去实现,连开发阶段都没有进入;

而且可以看到,即便是CSS Houdini规范,blink内核对不同模块的支持程度也不一样;目前Paint APIProperties & Values APITyped OM完全支持;其他模块不是没有开发进度就是很少的部分特性支持,基本上用处不大;

img

再看下caniuse网站的统计,移动端那就更加惨烈了,只有安卓原生浏览器和安卓Chrome浏览器有支持,而且要求版本还比较高(2020-4月后才支持);

所以CSS Houdini目前支持程度有限,仅在blink内核浏览器有用武之处,真实应用场景会大打折扣;

CSS Houdini 规范

背景

那么为何要提出CSS Houdini规范?

首先,因为目前CSS很多属性在各个内核上可能有表现差异,导致兼容性问题,且原生polyfill几乎没有可能;

其次,CSS很多新特性从提案,W3C纳入规范再到厂商最终实现可能要面临几年以上的时间,这个流程极大地耗费开发者的耐心及应用推广;因此每次出现新特性提案时大多数人都会调侃说到等几年后再试试;

CSS Houdini规范正是为了解决上述问题而提出的,通过暴露CSS引擎渲染相关的流程,使得渲染不再是黑盒,而是变得可控,这样样式和渲染的决定权就交到了开发者的手里,也就能解决上述的问题(当然,前提是所有浏览器都支持CSS Houdini规范……)。

作用

img

上图[2]解释了CSS Houdini规范中每个模块具体作用于CSS渲染的哪个流程,从图中可以看出CSS Houdini规范的野心很大,试图掌控整个CSS渲染流程(这对于开发者来说肯定是个好消息);

  • CSS Parsing API:用于解析CSS词法结构,也就是对于渲染中解析器工作的部分;
  • CSS Typed OM:从CSS Houdini规范草案可以看出,CSS Typed OMCSSOM的高性能版本,是直接将CSS值转化为类JS对象,从而减少中间层的转换;

Converting CSSOM value strings into meaningfully typed JavaScript representations and back can incur a significant performance overhead. This specification exposes CSS values as typed JavaScript objects to facilitate their performant manipulation.[3]

  • CSS Properties & Values API:对CSS自定义属性进行指定值类型等相关配置,指定值类型后自定义属性就能够被应用于动画;
  • CSS Layout API:用于控制渲染中的布局(Layout)流程,从而可以自己增加display类型;
  • CSS Painting API:用于控制渲染中的绘制(Paint)流程,对于元素样式达到像素级的控制,且绘制APIcanvas的子集,方便上手;
  • Worklets:类似于Web Worker,是独立于主线程的脚本文件,只能使用指定的API,专门用于存放注册布局和绘制相关的代码;
  • Composited Scrolling & Animation:复合滚动和动画API,目的是在worklets中支持滚动(位置)及动画变化,使得相应的属性变化不会引起主线程中的重排和重绘,从而拥有高性能的滚动及动画表现;目前blink内核已经实现了一部分Animation Worklet[4]

CSS Properties & Values API 简介

这部分的API很简洁,主要是定义了CSS自定义属性的一些性质;API包含CSS.registerProperty()方法和@property规则,而这两个本质上作用是一样的,前者是在JS中使用,后者是在CSS中使用;

CSS.registerProperty()

CSS.registerProperty(PropertyDefinition)[5]

该方法接收一个配置对象,该对象有以下几个属性:

  • name必填,自定义属性名(注意要包含前面的--);
  • syntax:可选,指定自定义属性的语法值类型;可用的值类型可以参考CSS的值与单位 - 学习 Web 开发 | MDN,默认值为"*"
  • inherits:自定义属性值是否可以被继承,默认为false(不能被继承);
  • initialValue:可选,自定义属性的初始值;
1
2
3
4
5
6
7
8
9
// 先判断API是否能用
if ('registerProperty' in CSS && 'CSSUnitValue' in window) {
CSS.registerProperty({
name: '--circle-color',
syntax: '<color>',
initialValue: '#39f',
inherits: false
})
}

上面就是一个注册自定义属性的例子,定义了具体值类型的自定义属性就能够被用于动画了(即可以自动生成关键帧了);CSSUnitValueCSS Typed OM规范中的一个APICSS.registerProperty()对其有依赖;

@property

@property规则语法和CSS.registerProperty()类似:

1
2
3
4
5
@property --circle-color {
syntax: '<color>';
inherits: false;
initial-value: #39f;
}

CSS Painting API 简介

CSS Painting API主要是由PaintWorklet组成,PaintWorklet定义了一套该类型worklet内所能访问的全局对象(PaintWorkletGlobalScope),绘制上下文(PaintRenderingContext2D,是CanvasRenderingContext2D API的子集)及注册和加载方法;

注册

worklet文件(也是JS文件)内使用registerPaint()进行注册,该方法接收两个参数:

registerPaint(name, class)[6]

  • name:该paintWorklet的名称,用于在CSS中进行引用;
  • class:实现该paintWorklet的类;

其中类可以实现以下(抽象)成员或方法:

  • static get inputProperties():返回一个数组,包含绘制函数所需要的属性(包括自定义属性);

  • static get inputArguments():返回一个数组,指定传入绘制函数的参数语法类型(类似于registerProperty方法中的syntax参数);

  • static get contextOptions():返回一个配置对象,用于设置绘制上下文(PaintRenderingContext2D);

  • paint(ctx, size, props, args)必须实现;绘制函数接收四个函数:

    • ctx:绘制上下文,即当前paintWorklet对应的PaintRenderingContext2D实例;
    • sizePaintSize对象,包含widthheight两个属性,即paintWorklet作用的元素的尺寸信息
    • propsStylePropertyMapReadOnly对象,只能访问由inputProperties属性返回的属性;
    • args:参数数组,可以接收由CSS paint()函数传入的参数;

关于 StylePropertyMapReadOnly 对象

该对象类似于Map对象,但是不能set,因此是只读的;通过get()方法可以获取到对应属性,不过得到并不是属性值,而是一个CSSStyleValue对象(属于CSS Typed OM),不同syntax类型的属性会有不同类型的CSSStyleValue对象;

不过一般来说数值类型的属性值可以通过value属性来获取值,颜色类型则通过toString()方法来获取值,如:

1
2
const boxColor = props.get('--box-color').toString()
const boxWidth = props.get('--box-width').value

加载

事实上,worklet的加载方式很类似,都是通过addModule(url)方法来进行:

1
2
3
4
// 先判断paintWorklet是否可用
if ('paintWorklet' in CSS) {
CSS.paintWorklet.addModule('worklet-path.js')
}

worklet文件地址仅支持httpslocalhost方式

使用

paintWorklet加载后就能够在CSS中进行使用了,可以使用paint(name)函数来启用workletname就是注册时传入的名称

  • name不需要加上引号
  • paint()函数返回值类型为<image>,因此只能在属性值类型为<image>的属性中进行使用!如:background-imgaelist-style-image等;更多关于<image>类型的信息可以参考 - CSS(层叠样式表) | MDN

传参

paint()函数也能传参,然后被paintWorklet中的paint()方法所接收:

1
paint(paint-name, arg1, arg2, ...)

一个案例

img

看到上面这个动画,在不使用CSS Houdini之前,很难想象纯CSS能够得到这样的效果;

相关文档


  1. Is Houdini Ready Yet? ↩︎

  2. Houdini:CSS 领域最令人振奋的革新 - 知乎 ↩︎

  3. https://drafts.css-houdini.org/css-typed-om ↩︎

  4. https://developers.google.com/web/updates/2018/10/animation-worklet ↩︎

  5. CSS.registerProperty() - Web APIs | MDN ↩︎

  6. PaintWorklet.registerPaint - Web APIs | MDN ↩︎