十分钟教你快速记住 Web Houdini API

432 阅读10分钟

为什么是 Houdini

哈里·胡迪尼(Harry Houdini,1874年3月24日—1926年10月31日),原名埃里克·韦斯(Ehrich Weiss),匈牙利裔美国魔术师,享誉国际的脱逃艺术家,能不可思议地解绳索、脚镣及手铐中脱困,他同时也是以魔术方法戳穿所谓“通灵术”的反伪科学先驱。

image.png

Houdini 代表着突破常规和创新,这个名字恰如其分地体现了这一 API 的核心目标——使开发者能像 Houdini 一样,从“CSS 的束缚”中逃脱出来,创造出前所未有的效果和控制。

所以,Houdini API 就是 Web 引擎暴露出来的超脱现有束缚,打破次元壁的 API。

选读:什么是 Houdini API

CSS Houdini 是一组 JS API,它们使开发人员可以直接访问 CSS 对象模型(CSSOM),使开发人员能够更灵活地控制样式和布局,突破传统 CSS 的限制。通过 Houdini,开发者可以编写自定义的 CSS 属性和布局引擎,甚至可以直接操作浏览器的渲染管道。

优点:

  • 增强 CSS 功能: Houdini 让开发者能够扩展 CSS 的能力。例如,你可以创建自定义的 CSS 属性、布局算法,或者控制 CSS 如何计算样式。这些都能使开发者绕开现有 CSS 的限制,创建更加复杂的效果。

  • 更高效的样式渲染: 通过 Houdini API,开发者可以实现一些效果(如动画、渐变、复杂的布局等),并且这些效果通常比 JavaScript 实现的方式更加高效,因为 Houdini 直接在浏览器的渲染引擎中执行,而不需要额外的 JavaScript 计算。

  • 自定义属性和布局:

    • CSS Painting API 可以让你通过 JavaScript 绘制自定义的样式(比如自定义的背景图案或图形)。
    • CSS Layout API 可以让你实现自己的布局算法,赋予你完全的布局控制权。
    • CSS Properties and Values API 可以让你创建并管理自定义的 CSS 属性,灵活性大大提升。
  • 提高开发者工具的扩展性: Houdini 提供了开发者可以利用的 API,允许你为 CSS 增加新的功能,这种灵活性在现代 web 开发中尤其重要,尤其是在需求多样化时。

  • 标准化与浏览器兼容: Houdini 是由 W3C 标准化的,意味着它将在未来得到广泛支持,而不是某个特定浏览器的私有实现。这使得开发者可以依赖于一个跨浏览器的解决方案。

缺点:

  • 兼容性问题: 截止到2024年,Houdini API 并不是所有浏览器都支持,这可能导致跨浏览器开发时出现问题。

  • 学习曲线较陡: 对大多数前端开发者而言,Houdini 的 API 仍然是相对较新的工具,学习并掌握它的用法可能需要一定时间。特别是涉及到 CSS 的低级控制和自定义绘制时,可能需要更多的底层知识和理解。

  • 缺乏文档和生态: 目前文档和生态系统相对较少。社区的支持和教程资源可能不足。

  • 性能问题(理论上的潜在风险): 尽管 Houdini 的目标之一是提高渲染效率,但不当的使用也可能导致性能问题。例如,频繁地进行自定义绘制或布局算法可能对页面的性能产生负面影响,尤其是在低端设备或复杂页面中。

  • 浏览器实现的不一致: 不同浏览器对 Houdini API 的实现可能不完全一致,导致开发者在使用时需要注意进行浏览器兼容性测试,并可能需要为不同浏览器实现适配。

  • 需要浏览器支持更新: 由于 Houdini 的功能较为底层,它需要浏览器在渲染引擎层面进行更新,开发者无法像使用常规 CSS 一样直接在页面上做出所有更改。这也意味着,开发者的创新必须依赖于浏览器厂商对这些标准的支持和更新速度。

核心 Houdini API

CSS Parser API (概念)

这是一个更直接地暴露出 CSS 解析器的 API,能够把任意 CSS 类语言解析成为一种中间类型。目前官方只是提出了这个概念,还未放出公开的 API。

笔者推测的使用方式:

// 创建一个 CSSParser 实例来解析
CSS const parser = new CSSParser();
// 解析 CSS 字符串
const cssText = ` body { background-color: #fff; color: #333; } `;
const parsedStyles = parser.parse(cssText);

目前主流浏览器,可以使用 document.styleSheets、css库、postcss库等来解析 css。

CSS Layout API (概念)

该 API 能够让开发者去书写他们自己的布局算法,比如 masonry 或者 line snapping。该 API 目前还没有原生支持。

可以参考一下 W3C 的未来展望的设计:drafts.css-houdini.org/css-layout-…

根据官方暴露出来的草稿,推测可能的使用方式:

// layout-worklet.js
class MyLayout {
  async intrinsicSizes() {}
  async layout(children, edges, constraints, styleMap) {
    // 自定义布局逻辑
    return { childFragments: [] };
  }
}
registerLayout('my-layout', MyLayout);
.container {
  display: layout(my-layout);
}

CSS Properties and Values API

CSS Properties and Values API 是一个实验性 API,旨在为开发者提供一种访问、查询和操作 CSS 属性及其值的标准化方式。

这个 API 目前已经有部分浏览器适用了,可以参考我这篇文章的应用案例:# css 边框流光效果

检测css支持性:

if (CSS.supports('color', 'rgb(255, 0, 0)')) {
  console.log('The browser supports red color');
}

image.png

获取元素实时计算样式:

const styles = getComputedStyle(element);
console.log(styles.getPropertyValue('background-color')); // 获取背景颜色

image.png

js 注册 css 属性:

// js 声明了一个属性,叫 --my-color,他是 color 类型的
window.CSS.registerProperty({
  name: '--my-color',
  syntax: '<color>',
  inherits: true,
  initial-value: 'yellow'
});

// 使用注册的属性
const element = document.querySelector('div');
element.style.setProperty('--my-color', 'red');

image.png

image.png

css 注册属性 (与 js 等价):

// 属性名叫 --border-gradient-angle
@property --border-gradient-angle {
  syntax: "<angle>"; 
  inherits: true; 
  initial-value: 0turn; 
}

注意:属性不只是 css变量

image.png

其中:

syntax 的枚举值:

<color>

代表颜色值,可以是任何有效的颜色格式,例如 red、#ff0000、rgb(255, 0, 0) 等。
示例:syntax: '<color>'

<length>

代表长度值,可以是如 10px、2em、3rem 等带单位的数值。
示例:syntax: '<length>'

<percentage>

代表百分比值,通常用来定义与父元素的比例。
示例:syntax: '<percentage>'

<number>

代表数字值,可以是整数或浮动数(没有单位)。
示例:syntax: '<number>'

<angle>

代表角度值,通常用来表示旋转角度等。可以是 deg、rad、turn 等单位。
示例:syntax: '<angle>'

<time>

代表时间值,可以是如 2s 或 500ms 这样的时间单位。
示例:syntax: '<time>'

<url>

代表 URL,通常用于指定资源的路径,例如背景图像的路径。
示例:syntax: '<url>'

<identifier>

代表标识符,通常用于 CSS 中的关键字、标签名等。
示例:syntax: '<identifier>'

<string>

代表字符串值,可以是用引号括起来的字符。
示例:syntax: '<string>'

<easing-function>

代表一个缓动函数的表达式(例如 ease, linear 等)。
示例:syntax: '<easing-function>'

<length-percentage>

代表一种长度值,可以是具体的长度(如 10px),也可以是百分比值(如 50%)。
示例:syntax: '<length-percentage>'

<custom-ident>

代表一个自定义标识符,通常用于自定义的属性值,类似于 CSS 中的某些自定义关键字。
示例:syntax: '<custom-ident>'

举例:比如一个属性声明为角度属性:

@property --my-rotation {
  syntax: "<angle>";
  inherits: true;
  initial-value: 0turn;
}

他一方面可以作为 css 变量使用:

transform: rotate(var(--my-rotation))

另一方面可以真的作为一个css 属性赋值:


@keyframes buttonBorderSpin {
  0% {
    --my-rotation: 0turn;
  }

  100% {
    --my-rotation: 1turn;
  }
}

在上面的 buttonBorderSpin 动画中,--my-rotation 会随着动画均匀的被修改,进而又可以影响到作为变量使用的地方。这比原来的属性只能是属性,变量只能是变量灵活得多。

CSS Typed OM

该 API 将 CSSOM 字符串转化为有类型意义的 JavaScript。将 CSS 值以类型化处理的 JavaScript 对象的形式暴露出来,以使其表现可以被控制。

  • CSSStyleValue

所有可通过类型对象模型访问 CSS 值的基类。

// 解析 css
const css = CSSStyleValue.parse(
  "transform",
  "translate3d(10px,10px,0) scale(0.5)",
);

// 输出:CSSTransformValue {0: CSSTranslate, 1: CSSScale, length: 2, is2D: false}

image.png

image.png

其子类包括:

  • CSSStyleValue
    • CSSImageValue
    • CSSKeywordValue
    • CSSMathValue
    • CSSNumericValue
    • CSSTransformValue (上面的例子便是)
    • CSSUnitValue 表示为单个单位或具名数字和百分比的数值
// get the element
const button = document.querySelector("button");

// Retrieve all computed styles with computedStyleMap()
const allComputedStyles = button.computedStyleMap();

// Return the CSSImageValue Example
console.log(allComputedStyles.get("background-image"));
console.log(allComputedStyles.get("background-image").toString());

// CSSKeywordValue 设置样式
let myElement = document.getElementById("myElement").attributeStyleMap;
myElement.set("display", new CSSKeywordValue("initial"));

console.log(myElement.get("display").value); // 'initial'

// CSSMathValue 获取css样式:width: calc(30% - 20px);
const styleMap = document.querySelector("div").computedStyleMap();

console.log(styleMap.get("width")); // CSSMathSum {values: CSSNumericArray, operator: "sum"}
console.log(styleMap.get("width").operator); // 'sum'
console.log(styleMap.get("width").values[1].value); // -20

// CSSNumericValue 终于可以在 css 里计算了
let mathSum = CSS.px("23")
  .add(CSS.percent("4"))
  .add(CSS.cm("3"))
  .add(CSS.in("9"));
// Prints "calc(23px + 4% + 3cm + 9in)"
console.log(mathSum.toString());

// CSSUnitValue
new CSSUnitValue(5, "px")

image.png

其中 attributeStyleMap 是新增加的 dom 样式属性,属于 StylePropertyMap 类,有 append、delete、clear、set 等方法

computedStyleMap() 则返回一个只读的样式表。

Worklets

该 API 允许脚本独立于 JavaScript 执行环境,运行在渲染流程的各个阶段。Worklets 在概念上很接近于 Web Workers ,它由渲染引擎调用,并扩展了渲染引擎。(摘自MDN)

目前,浏览器支持以下几种 Worklet:

  • PaintWorklet: 用于自定义 CSS 绘制行为,通常与 CSS Paint API 结合使用。
  • AudioWorklet: 用于音频处理,通常与 Web Audio API 结合使用。
  • AnimationWorklet: 用于高性能动画,通常与 CSS Animation API 结合使用。
  • LayoutWorklet: 用于自定义布局,通常与 CSS Layout API 结合使用。

image.png

这里举一个 AnimationWorklet 的使用例子。

写一个独立的脚本:

// animation-worklet.js
class MyAnimator {
  constructor(options) {
    this.rate = options.rate;
  }

  animate(currentTime, effect) {
    // 根据时间计算动画进度
    const progress = (currentTime * this.rate) % 1.0;
    effect.localTime = progress * 1000; // 设置动画的本地时间
  }
}

registerAnimator('my-animator', MyAnimator);

定义一个动画类,有一个成员方法叫 animate,动画的每一帧都会调用 animate 方法,currentTime 是当前的时间(以毫秒为单位)。progress 是根据 currentTime 和 rate 计算出的动画进度;

effect.localTime 是动画的本地时间,通过 progress * 1000 将进度映射到动画的持续时间范围内(0 到 1000 毫秒)。

然后注册到浏览器:

await CSS.animationWorklet.addModule('animation-worklet.js');

接下来就可以调用了:

const element = document.querySelector('.animated-element');

const animation = new WorkletAnimation(
  'my-animator', // 动画名称
  new KeyframeEffect(
    element,
    [
      { transform: 'translateX(0px)' },
      { transform: 'translateX(200px)' }
    ],
    { duration: 1000, iterations: Infinity }
  ),
  document.timeline,
  { rate: 1.0 } // 传递给 class MyAnimator 的参数
);

animation.play();

每一帧获取一下 effect.localTime 来判断动画进度,执行平移操作。

有点类似于 document.documentElement.animate

CSS Painting API

该 API 允许开发者通过 paint() 方法书写 JavaScript 函数,以控制绘制元素的背景、边框或者内容区域。

image.png

直接上例子(developer.mozilla.org/en-US/play)…

我们写这么一个 worklets 脚本:

registerPaint('boxbg', class {

// 是否允许 Alpha
// 默认情况下设置为 true,如果设置为 false,画布上使用的所有颜色都将具有完全不透明度,或 alpha 为 1.0
static get contextOptions() { return {alpha: true}; }

// 使用此函数检索为元素定义的任何自定义属性,并将它们返回到指定的数组中
static get inputProperties() { return ['--boxColor', '--widthSubtractor']; }

paint(ctx, size, props) {
    // ctx -> 画布 context
    // size -> paintSize: width and height
    // props -> properties: get() method

		ctx.fillStyle = props.get('--boxColor'); 
		ctx.fillRect(0, size.height/3, size.width*0.4 - props.get('--widthSubtractor'), size.height*0.6);

  }
});

然后注册到浏览器:

CSS.paintWorklet.addModule(
  "脚本路径",
);

针对这个dom元素:

<ul>
  <li>item 1</li>
  <li>item 2</li>
  <li>item 3</li>
  <li>item 4</li>
  <li>item 5</li>
  <li>item 6</li>
  <li>item 7</li>
  <li>item 8</li>
  <li>item 9</li>
  <li>item 10</li>
  <li>item N</li>
</ul>

我们开始设置 脚本定义好的 css。

li {
  background-image: paint(boxbg);
  --boxColor: hsl(55 90% 60%);
}

这段 css 声明要用 boxbg 来绘制背景图片。接下来声明脚本定义好的自定义属性 --boxColor 是个米黄色(hsl(55 90% 60%)),在脚本的 paint 函数里,使用 fillStyle 来填充 --boxColor,所以现在表现为这样:

image.png

我再写一段css:

li:nth-of-type(3n) {
  --boxColor: hsl(155 90% 60%);
  --widthSubtractor: 20;
}

他表示3的倍数行为天蓝色(hsl(155 90% 60%)),并设置 --widthSubtractor 是 20. --widthSubtractor 在脚本里这样定义的:

ctx.fillRect(0, size.height/3, size.width*0.4 - props.get('--widthSubtractor'), size.height*0.6)

其用于计算 li 的宽度。效果如下:

image.png

总结

Houdini 是一组底层 CSS 渲染引擎的 API,它允许开发者直接介入浏览器的渲染过程,实现更高级、更灵活的样式和动画效果。Houdini API 的目标是让开发者能够突破传统 CSS 的限制,实现更复杂的视觉效果和性能优化。