为什么是 Houdini
哈里·胡迪尼(Harry Houdini,1874年3月24日—1926年10月31日),原名埃里克·韦斯(Ehrich Weiss),匈牙利裔美国魔术师,享誉国际的脱逃艺术家,能不可思议地解绳索、脚镣及手铐中脱困,他同时也是以魔术方法戳穿所谓“通灵术”的反伪科学先驱。
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');
}
获取元素实时计算样式:
const styles = getComputedStyle(element);
console.log(styles.getPropertyValue('background-color')); // 获取背景颜色
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');
css 注册属性 (与 js 等价):
// 属性名叫 --border-gradient-angle
@property --border-gradient-angle {
syntax: "<angle>";
inherits: true;
initial-value: 0turn;
}
注意:属性不只是 css变量
其中:
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}
其子类包括:
- 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")
其中 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 结合使用。
这里举一个 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 函数,以控制绘制元素的背景、边框或者内容区域。
直接上例子(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,所以现在表现为这样:
我再写一段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 的宽度。效果如下:
总结
Houdini 是一组底层 CSS 渲染引擎的 API,它允许开发者直接介入浏览器的渲染过程,实现更高级、更灵活的样式和动画效果。Houdini API 的目标是让开发者能够突破传统 CSS 的限制,实现更复杂的视觉效果和性能优化。