CSS Houdini 与 CSS-Typed-OM

深入理解 CSS Houdini 的架构:Worklet 的工作机制 Paint API、Layout API、Properties & Values API、Typed OM,以及 Houdini 如何让 CSS 具备可扩展性。

CSS Houdini 与 CSS-Typed-OM

为什么需要 Houdini

传统 CSS 的问题在于它是声明式的黑盒——你无法用 JavaScript 干预 CSS 引擎的内部工作。当 JavaScript 修改样式时,只能在 CSSOM 层面操作字符串,既无法获取类型安全的值,也无法在渲染阶段插入自定义逻辑。

CSS Houdini 是一组 Low-level API,提供了访问 CSS 引擎内部的能力,让开发者能够编写自定义的 Paint、Layout、Animation 等逻辑。这些 API 于 2016 年首次提出,经过多年的发展,正在逐步被浏览器实现。

理解 Houdini 的架构和 API,对于构建真正可扩展的 CSS 解决方案至关重要。

面试定位:Houdini 是 CSS 的高级特性,不是面试高频考点,但它代表了 CSS 的未来方向。面试官通过候选人对 Houdini 架构的理解、对各个 API 的掌握程度、以及对浏览器支持现状的认知,来评估其技术视野和工程判断能力。


Houdini 的核心 API 概览

Houdini 包含多个 API,每个 API 负责不同的渲染阶段:

API 功能 状态
Paint API 自定义绘制逻辑 较完善支持
Layout API 自定义布局算法 实验性
Properties & Values API 自定义属性和值类型 较完善支持
Animation Worklet 自定义动画逻辑 实验性
Font Metrics API 字体度量 提议中

Worklets 机制

Houdini 的核心概念是 Worklet。Worklet 是一种轻量级的脚本机制,允许开发者在渲染管线的特定阶段执行自定义代码。

Worklet vs Service Worker

Worklet 与 Service Worker 有相似之处,但关键区别在于:

  • Service Worker:运行在独立线程,处理网络请求和缓存
  • Worklet:运行在渲染管线特定阶段,处理绘制、布局等
// 注册 Paint Worklet
CSS.paintWorklet.addModule('paint-worklet.js');

Worklet 脚本必须遵循以下规则:

  1. 无状态:Worklet 中的代码应该无状态运行,每次调用都独立
  2. 单线程:Worklet 在主线程之外运行,但不意味着可以执行耗时操作
  3. 受限 API:Worklet 中只能访问有限的 API

Paint Worklet 示例

Paint Worklet 允许开发者自定义元素的背景、边框等绘制逻辑:

// paint-worklet.js
registerPaint('my-paint', class {
  // 上下文选项
  static get contextOptions() {
    return { alpha: true };
  }

  // 输入属性
  static get inputProperties() {
    return ['--my-color', '--my-size'];
  }

  // 主绘制方法
  paint(ctx, size, properties) {
    // ctx: PaintRenderingContext2D
    // size: PaintSize (元素尺寸)
    // properties: StylePropertyMap

    const color = properties.get('--my-color');
    const mySize = properties.get('--my-size');

    ctx.fillStyle = color.toString();
    ctx.fillRect(0, 0, size.width * mySize.value, size.height * mySize.value);
  }
});
.element {
  background-image: paint(my-paint);
  --my-color: blue;
  --my-size: 0.5;
}

Paint API 详解

Paint API 是 Houdini 中最成熟的 API,允许开发者使用 JavaScript 绘制背景、边框等。

registerPaint 参数

registerPaint(name, class, options)
  • name:注册的名称,在 CSS 中使用 paint(name) 引用
  • class:包含绘制逻辑的类
  • options:可选配置

类方法

contextOptions

返回上下文选项,目前只支持 { alpha: true }

static get contextOptions() {
  return { alpha: true };
}

inputProperties

返回需要监听的自定义属性列表:

static get inputProperties() {
  return [
    '--my-color',
    '--my-size',
    'width',       // 也可以监听原生属性
    'height'
  ];
}

paint(ctx, size, properties)

主绘制方法,接收三个参数:

paint(ctx, size, properties) {
  // ctx: PaintRenderingContext2D (类似 Canvas 2D Context)
  // size: PaintSize { width, height }
  // properties: StylePropertyMap

  // 绘制逻辑
  ctx.fillStyle = 'red';
  ctx.fillRect(0, 0, size.width, size.height);
}

Paint API 实用示例

示例一:动态条纹背景

// stripe-worklet.js
registerPaint('stripe', class {
  static get contextOptions() {
    return { alpha: true };
  }

  static get inputProperties() {
    return ['--stripe-color', '--stripe-width', '--stripe-angle'];
  }

  paint(ctx, size, properties) {
    const color = properties.get('--stripe-color').toString().trim();
    const width = parseInt(properties.get('--stripe-width')) || 10;
    const angle = parseInt(properties.get('--stripe-angle')) || 45;

    ctx.fillStyle = color;

    // 绘制条纹
    const diagonal = Math.sqrt(size.width ** 2 + size.height ** 2);
    ctx.save();
    ctx.translate(size.width / 2, size.height / 2);
    ctx.rotate((angle * Math.PI) / 180);

    for (let i = -diagonal; i < diagonal; i += width * 2) {
      ctx.fillRect(i, -diagonal, width, diagonal * 2);
    }

    ctx.restore();
  }
});
.striped-bg {
  background-image: paint(stripe);
  --stripe-color: #667eea;
  --stripe-width: 20;
  --stripe-angle: 45;
}

示例二:渐变边框

registerPaint('gradient-border', class {
  static get contextOptions() {
    return { alpha: true };
  }

  static get inputProperties() {
    return ['--border-width', '--gradient-colors'];
  }

  paint(ctx, size, properties) {
    const borderWidth = parseInt(properties.get('--border-width')) || 4;
    const colors = properties.get('--gradient-colors').toString().split(',');

    const gradient = ctx.createLinearGradient(0, 0, size.width, size.height);
    colors.forEach((color, index) => {
      gradient.addColorStop(index / (colors.length - 1), color.trim());
    });

    ctx.strokeStyle = gradient;
    ctx.lineWidth = borderWidth;
    ctx.strokeRect(0, 0, size.width, size.height);
  }
});
.gradient-border {
  background-image: paint(gradient-border);
  --border-width: 4;
  --gradient-colors: #667eea, #764ba2, #f093fb;
}

Properties and Values API

Properties and Values API 允许开发者注册自定义属性,并为其定义类型、初始值和继承行为。

注册自定义属性

CSS.registerProperty({
  name: '--my-color',
  syntax: '<color>',
  inherits: false,
  initialValue: 'blue'
});

参数说明

参数 类型 描述
name string 属性名(必须以 -- 开头)
syntax string 语法描述
inherits boolean 是否继承
initialValue string 初始值

syntax 语法

// 颜色
CSS.registerProperty({
  name: '--my-color',
  syntax: '<color>',
  initialValue: 'blue'
});

// 数值(带单位)
CSS.registerProperty({
  name: '--my-length',
  syntax: '<length>',
  initialValue: '10px'
});

// 百分比
CSS.registerProperty({
  name: '--my-percentage',
  syntax: '<percentage>',
  initialValue: '50%'
});

// 多个可能值
CSS.registerProperty({
  name: '--my-transform',
  syntax: 'none | <transform-list>',
  initialValue: 'none'
});

// 自定义值
CSS.registerProperty({
  name: '--my-custom',
  syntax: 'a | b | c',
  initialValue: 'a'
});

与 CSS 变量的区别

自定义属性通过 Properties and Values API 注册后,具有类型检查能力:

// 注册了类型的属性
CSS.registerProperty({
  name: '--my-number',
  syntax: '<number>',
  initialValue: '0'
});

// 在 CSS 中使用
.element {
  --my-number: 10;     /* ✓ 有效 */
  --my-number: 20px;   /* ✗ 无效(类型不匹配) */
}

这使得动画和过渡可以作用于自定义属性:

.animated {
  --my-size: 0;
  animation: grow 1s forwards;
}

@keyframes grow {
  to { --my-size: 100; }
}

.animated:hover {
  --my-size: 200; /* 支持过渡 */
  transition: --my-size 0.3s;
}

CSS Typed OM

传统的 CSSOM 使用字符串操作样式值,既不直观也容易出错。CSS Typed OM 提供了类型化的对象来表示 CSS 值。

StylePropertyMap

element.attributeStyleMapelement.computedStyleMap() 返回 StylePropertyMap 对象。

// 设置值
element.attributeStyleMap.set('padding', CSS.px(10));
element.attributeStyleMap.set('opacity', CSS.number(0.5));

// 获取值
const padding = element.computedStyleMap().get('padding');
console.log(padding.value);  // 10
console.log(padding.unit);   // 'px'

const opacity = element.computedStyleMap().get('opacity');
console.log(opacity.value);  // 0.5
console.log(opacity.type);   // CSSUnitType.NUMBER

CSSUnitValue

表示带单位的数值:

const length = CSS.px(100);
console.log(length.value);   // 100
console.log(length.unit);    // 'px'

// 运算
const doubled = CSS.px(length.value * 2);

// 类型转换
const rem = length.to('rem');

CSSMathValue

表示计算值:

// 加法
const sum = CSS.px(10).add(CSS.px(20));  // 30px

// 乘法
const scaled = CSS.px(10).multiply(2);   // 20px

支持的类型

类型 工厂方法 示例
长度 CSS.px(), CSS.em(), CSS.rem() CSS.px(10)
百分比 CSS.percent() CSS.percent(50)
数值 CSS.number() CSS.number(0.5)
颜色 CSS.rgb(), CSS.hsl() CSS.rgb(255, 0, 0)
transform CSS.transform() CSS.transform([...])
计算 CSS.calc() CSS.calc(CSS.px(10).add(CSS.em(2)))

Layout API

Layout API 允许开发者自定义元素的布局算法。这是一个实验性 API,目前支持有限。

registerLayout

registerLayout('my-layout', class {
  async intrinsicSizes() {
    // 返回固有尺寸
    return { minSize: 0, maxSize: 0 };
  }

  async layout(children, edges, constraints, styleMap) {
    // 布局逻辑
    const childFragments = await Promise.all(
      children.map(child => child.layoutNextFragment())
    );

    // 计算尺寸
    const width = constraints.fixedInlineSize;
    const height = 100; // 自定义高度

    return {
      autoBlockSize: height,
      childFragments
    };
  }
});
.outer {
  display: layout(my-layout);
}

Animation Worklet

Animation Worklet 允许开发者创建高性能的 JavaScript 动画。

registerAnimator('my-animator', class {
  constructor(options) {
    this.options = options;
  }

  animate(currentTime, effect) {
    // effect.target: 动画目标元素
    // effect.localTime: 本地时间
    const progress = currentTime / 1000; // 转换为秒
    effect.target.style.transform = `rotate(${progress * 360}deg)`;
  }
});
// 注册 worklet
await CSS.animationWorklet.addModule('animator.js');

// 创建动画
const element = document.querySelector('.rotating');
const animation = new Animation(
  new WorkletEffect({ target: element }),
  new DocumentTimeline()
);
animation.play();

浏览器支持与注意事项

当前支持状态

Houdini 的各个 API 支持程度不同:

  • Paint API:Chromium 系浏览器较完善支持
  • Properties and Values API:Chromium 系浏览器较完善支持
  • CSS Typed OM:Chromium 系浏览器较完善支持
  • Layout API:实验性支持
  • Animation Worklet:实验性支持

使用 Feature Query

@supports (background-image: paint(my-paint)) {
  .element {
    background-image: paint(my-paint);
  }
}

渐进增强策略

Houdini 应该作为渐进增强的一部分使用:

/* 基础样式 */
.box {
  background: blue;
}

/* Houdini 增强 */
@supports (background-image: paint(my-paint)) {
  .box {
    background-image: paint(my-paint);
    --my-color: red;
  }
}

延展阅读