倒反天罡,CSS 中竟然可以写 JavaScript
引言
最近和大佬学习写 像素风组件库 里面有很多复杂而有趣的样式,于是跑去研究了一下,震惊的发现,大佬竟然在 CSS 中写 JavaScript !
一般来说 CSS 是网页样式的声明性语言,而 JavaScript 则负责交互逻辑。我仔细研究了一下,原来是通过 CSS Houdini 实现了用 JavaScript 来扩展 CSS 的能力。所以写了这篇文章来探讨一下 CSS Houdini。
CSS Houdini是什么?
CSS Houdini 是一组低级 API,允许开发者直接访问 CSS 对象模型(CSSOM),从而能够扩展 CSS 的功能。它的名字来源于著名魔术师 Harry Houdini,寓意"逃离" CSS 的限制,就像魔术师从束缚中挣脱一样。
那么问题来了,为什么选择 CSS Houdini 而不是直接使用 JavaScript 来操作样式?
性能优势
与使用 JavaScript 对 HTMLElement.style
进行样式更改相比,Houdini 可实现更快的解析。JavaScript 修改样式通常会触发浏览器的重排(reflow) 和重绘(repaint),特别是在动画中,这可能导致性能问题。而 Houdini 工作在浏览器渲染流程的更低层级,能够更高效地处理样式变化,减少不必要的计算。
扩展和复用性
使用 JavaScript 修改样式本质上是在操作 DOM,而 Houdini 直接扩展了 CSS 的能力。这使得自定义效果可以像原生 CSS 特性一样工作,包括继承、级联和响应式设计。
Houdini API 允许创建真正的 CSS 模块,可以像使用标准 CSS 属性一样使用自定义功能,提高代码的可维护性和复用性。
主要API概览
接下来是重点,我们来看看到底如何去使用 CSS Houdini。CSS Houdini 包含多个API,下面通过具体案例来说明一下使用方式。
1. CSS Painting API
CSS Paint API 允许我们使用 JavaScript 和 Canvas API 创建自定义的 CSS 图像,然后在 CSS 样式中使用这些图像,例如 background-image
、border-image
、mask-image
等。它的使用方法分为下面三步:
第一步,绘制背景
在这一步,使用 registerPaint()
定义一个 paint worklet (可以翻译理解为自定义画笔),来画你想要的图案。我们需要的变量可以通过 CSS 变量的形式定义并引入,在 inputProperties
指定我们需要读取的参数。
// myPainter.js
registerPaint(
'myPainter',
class {
static get inputProperties() {
return ['--my-color', '--wave-amplitude', '--wave-frequency'];
}
paint(ctx, size, properties) {
const color = properties.get('--my-color').toString() || '#3498db';
const amplitude = parseFloat(properties.get('--wave-amplitude')) || 20;
const frequency = parseFloat(properties.get('--wave-frequency')) || 0.03;
// 画渐变背景
const gradient = ctx.createLinearGradient(0, 0, size.width, size.height);
gradient.addColorStop(0, color);
gradient.addColorStop(1, '#fff');
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, size.width, size.height);
// 画波浪
ctx.beginPath();
ctx.moveTo(0, size.height / 2);
for (let x = 0; x <= size.width; x++) {
const y =
size.height / 2 +
Math.sin(x * frequency) * amplitude +
Math.sin(x * frequency * 0.5) * (amplitude / 2);
ctx.lineTo(x, y);
}
ctx.lineTo(size.width, size.height);
ctx.lineTo(0, size.height);
ctx.closePath();
ctx.fillStyle = color + '88'; // 半透明主色
ctx.fill();
}
}
);
第二步,注册刚才定义的 worklet
在这一步,通过 CSS.paintWorklet.addModule
来引入我们自定义的 paint worklet。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<style>
</style>
</head>
<body>
<div class="box">一条大河波浪宽</div>
<script>
if ('paintWorklet' in CSS) {
CSS.paintWorklet.addModule('myPainter.js');
}
</script>
</body>
</html>
最后,在 CSS 中使用 paint
我们在 CSS 属性值中通过 paint(myPainter)
的方式来指定使用我们的 paint worklet,同时,通过 CSS 变量传递需要的参数。
.box {
width: 300px;
height: 200px;
text-align: center;
color: #fff;
background-image: paint(myPainter);
/* 定义paint需要的变量 */
--my-color: #0087ff;
--wave-amplitude: 30;
--wave-frequency: 0.04;
}
最后看下效果
有了 JavaScript 和 Canvas API 的加持,可以画很多酷炫的效果。
2. CSS Properties and Values API
这个 API 允许我们定义自定义 CSS 属性的类型、初始值和继承行为。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>CSS Properties and Values API 示例</title>
<style>
.color-box {
width: 200px;
height: 200px;
margin: 50px auto;
background-color: var(--my-color);
transition: --my-color 1s;
}
.color-box:hover {
--my-color: green;
}
</style>
</head>
<body>
<div class="color-box" id="colorBox"></div>
<script>
// 检查浏览器是否支持CSS Properties and Values API
if (window.CSS && CSS.registerProperty) {
// 注册一个自定义属性
CSS.registerProperty({
name: '--my-color',
syntax: '<color>',
inherits: false,
initialValue: 'blue',
});
}
</script>
</body>
</html>
在上面这个示例中,我们定义了一个自定义属性,名字为 --my-color
,通过 syntax: '<color>'
来指定这个属性的值类型是颜色(比如 blue
、#fff
、rgb(0,0,0)
等),这样浏览器就能识别并支持动画、过渡等。通过 inherits: false
指定这个属性不会从父元素继承。通过 initialValue: 'blue'
指定它的默认值为 blue
。
定义之后,我们可以通过 var(--my-color)
来引用这个变量,也可以通过 --my-color: green
来更改它的值。
那为什么不能直接定义个 CSS 变量,而是要通过 CSS.registerProperty
来注册一个属性呢?
- 普通的 CSS 变量,浏览器只当作字符串处理,不能直接做动画、过渡等。而用
registerProperty
注册后,浏览器知道它是<color>
类型,就能支持动画、过渡等高级特性。
3. CSS Typed Object Model
CSS Typed OM API 将 CSS 值以类型化的 JavaScript 对象形式暴露出来,来让方便我们对其进行操作。
比起直接使用 HTMLElement.style
的形式操作 CSS 样式,CSS Typed OM 拥有更好的逻辑性和性能。
computedStyleMap
通过 computedStyleMap()
可以以 Map 形式获取一个元素所有的 CSS 属性和值,包括自定义属性。
获取不同的属性返回值类型不同,需要用不同的读取方式。computedStyleMap()
返回的是只读的计算样式映射,不能直接修改。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<style>
.box {
color: rgb(13 5 17);
}
</style>
</head>
<body>
<div
class="box"
style="
width: 100px;
height: 50px;
background-image: linear-gradient(to right, red, blue);
"
></div>
<script>
const box = document.querySelector('.box');
// 获取所有属性
const computedStyles = box.computedStyleMap();
// 读取指定属性的值
console.log(computedStyles.get('color').toString()); // rgb(13, 5, 17)
console.log(computedStyles.get('background-image').toString()); // linear-gradient(to right, rgb(255, 0, 0), rgb(0, 0, 255))
console.log(computedStyles.get('height').value); // 100
console.log(computedStyles.get('height').unit); // px
console.log(computedStyles.get('position').value); // 'static'
</script>
</body>
</html>
attributeStyleMap
通过 element.attributeStyleMap
可以获取和设置 CSS 的内联样式。
<!DOCTYPE html>
<html lang="en">
<head>
<style>
.box {
background-color: blue; /* 样式表中的样式 */
}
</style>
</head>
<body>
<div class="box" style="width: 100px; height: 100px;"></div>
<script>
const box = document.querySelector('.box');
const inlineStyles = box.attributeStyleMap;
console.log('width:', inlineStyles.get('width')?.toString()); // "100px"
console.log('height:', inlineStyles.get('height')?.toString()); // "100px"
console.log('background-color:', inlineStyles.get('background-color')); // undefined,因为是在样式表中定义的
setInterval(() => {
inlineStyles.set('width', CSS.px(inlineStyles.get('width').value + 1));
}, 30);
</script>
</body>
</html>
在这个例子中,读取了 width
并进行设置,让它宽度逐渐变大。
4. Layout Worklet 和 Animation Worklet
除了上述的三种 API,Hounidi 还包含了 Layout Worklet 和 Animation Worklet 分别用于自定义布局和动画,但是目前还在实验中,支持度不是很好,所以就不提供使用案例了。
参考资源
来源:juejin.cn/post/7515707680927055923