使用新的 CSS 类型化对象模型

要点

CSS 现在具有基于对象的适当 API,可在 JavaScript 中处理值。

el.attributeStyleMap.set('padding', CSS.px(42)); const padding = el.attributeStyleMap.get('padding'); console.log(padding.value, padding.unit); // 42, 'px' 

拼接字符串和细微 bug 的时代已经结束!

简介

旧版 CSSOM

CSS 多年来一直拥有对象模型 (CSSOM)。事实上,只要您在 JavaScript 中读取/设置 .style,就是在使用它:

// Element styles. el.style.opacity = 0.3; typeof el.style.opacity === 'string' // Ugh. A string!? // Stylesheet rules. document.styleSheets[0].cssRules[0].style.opacity = 0.3; 

新的 CSS 类型化对象模型

作为 Houdini 计划的一部分,新的 CSS 类型化对象模型(类型化 OM)通过向 CSS 值添加类型、方法和适当的对象模型,扩展了这种世界观。值以 JavaScript 对象的形式公开,而不是以字符串的形式公开,以便高效(且合理)地操纵 CSS。

您将不再使用 element.style,而是通过元素的新 .attributeStyleMap 属性和样式表规则的 .styleMap 属性来访问样式。两者都会返回一个 StylePropertyMap 对象。

// Element styles. el.attributeStyleMap.set('opacity', 0.3); typeof el.attributeStyleMap.get('opacity').value === 'number' // Yay, a number! // Stylesheet rules. const stylesheet = document.styleSheets[0]; stylesheet.cssRules[0].styleMap.set('background', 'blue'); 

由于 StylePropertyMap 是类似 Map 的对象,因此它们支持所有常见的嫌疑对象(get/set/keys/values/entries),因此可以灵活地使用:

// All 3 of these are equivalent: el.attributeStyleMap.set('opacity', 0.3); el.attributeStyleMap.set('opacity', '0.3'); el.attributeStyleMap.set('opacity', CSS.number(0.3)); // see next section // el.attributeStyleMap.get('opacity').value === 0.3 // StylePropertyMaps are iterable. for (const [prop, val] of el.attributeStyleMap) {  console.log(prop, val.value); } // → opacity, 0.3 el.attributeStyleMap.has('opacity') // true el.attributeStyleMap.delete('opacity') // remove opacity. el.attributeStyleMap.clear(); // remove all styles. 

请注意,在第二个示例中,opacity 设置为字符串 ('0.3'),但稍后读取该属性时会返回一个数字。

优势

那么,CSS Typed OM 试图解决哪些问题呢?通过查看上面的示例(以及本文的其余部分),您可能会认为 CSS Typed OM 比旧对象模型冗长得多。我同意!

在放弃 Typed OM 之前,请考虑它带来的一些关键功能:

  • 减少 bug。例如,数值始终以数字形式返回,而不是字符串形式。

    el.style.opacity += 0.1; el.style.opacity === '0.30.1' // dragons! 
  • 算术运算和单位换算。在绝对长度单位(例如 px -> cm)之间进行转换,并执行基本数学运算

  • 价值限制和舍入。类型化 OM 会舍入和/或钳制值,使其位于属性的可接受范围内。

  • 效果更出色。浏览器在序列化和反序列化字符串值方面需要完成的工作量更少。现在,引擎在 JS 和 C++ 中对 CSS 值的理解类似。Tab Atkins 展示了一些早期性能基准,这些基准表明,与使用旧 CSSOM 和字符串相比,Typed OM 在每秒操作数方面快了约 30%。对于使用 requestionAnimationFrame() 的快速 CSS 动画,这可能非常重要。crbug.com/808933 跟踪 Blink 中的其他性能工作。

  • 错误处理。新的解析方法为 CSS 带来了错误处理

  • “我应该使用采用驼峰式命名法的 CSS 名称还是字符串?”您无需再猜测名称是采用驼峰式命名法还是字符串(例如 el.style.backgroundColorel.style['background-color'])。Typed OM 中的 CSS 属性名称始终是字符串,与您在 CSS 中实际编写的内容一致 :)

浏览器支持和功能检测

Typed OM 已在 Chrome 66 中实现,目前正在 Firefox 中实现。Edge 已显示出支持迹象,但尚未将其添加到平台信息中心

对于特征检测,您可以检查是否定义了以下某个 CSS.* 数值工厂:

if (window.CSS && CSS.number) {  // Supports CSS Typed OM. } 

API 基础知识

访问样式

在 CSS 类型化对象模型中,值与单位是分开的。获取样式会返回一个包含 valueunitCSSUnitValue

el.attributeStyleMap.set('margin-top', CSS.px(10)); // el.attributeStyleMap.set('margin-top', '10px'); // string arg also works. el.attributeStyleMap.get('margin-top').value // 10 el.attributeStyleMap.get('margin-top').unit // 'px' // Use CSSKeyWorldValue for plain text values: el.attributeStyleMap.set('display', new CSSKeywordValue('initial')); el.attributeStyleMap.get('display').value // 'initial' el.attributeStyleMap.get('display').unit // undefined 

计算出的样式

计算样式已从 window 上的 API 移至 HTMLElement 上的新方法 computedStyleMap()

旧版 CSSOM

el.style.opacity = 0.5; window.getComputedStyle(el).opacity === "0.5" // Ugh, more strings! 

新的类型化 OM

el.attributeStyleMap.set('opacity', 0.5); el.computedStyleMap().get('opacity').value // 0.5 

价值限制 / 舍入

新对象模型的一项出色功能是自动限制和/或舍入计算出的样式值。举例来说,假设您尝试将 opacity 设置为超出可接受范围 [0, 1] 的值。在计算样式时,类型化 OM 会将值限制为 1

el.attributeStyleMap.set('opacity', 3); el.attributeStyleMap.get('opacity').value === 3 // val not clamped. el.computedStyleMap().get('opacity').value === 1 // computed style clamps value. 

同样,将 z-index:15.4 设置为 15 会进行四舍五入,因此该值仍为整数。

el.attributeStyleMap.set('z-index', CSS.number(15.4)); el.attributeStyleMap.get('z-index').value === 15.4 // val not rounded. el.computedStyleMap().get('z-index').value === 15 // computed style is rounded. 

CSS 数值

在类型化 OM 中,数字由两种类型的 CSSNumericValue 对象表示:

  1. CSSUnitValue - 包含单个单位类型(例如 "42px")的值。
  2. CSSMathValue - 包含多个值/单位的值,例如数学表达式(例如 "calc(56em + 10%)")。

单位值

简单数值 ("50%") 由 CSSUnitValue 对象表示。虽然您可以直接创建这些对象 (new CSSUnitValue(10, 'px')),但在大多数情况下,您会使用 CSS.* 工厂方法:

const {value, unit} = CSS.number('10'); // value === 10, unit === 'number' const {value, unit} = CSS.px(42); // value === 42, unit === 'px' const {value, unit} = CSS.vw('100'); // value === 100, unit === 'vw' const {value, unit} = CSS.percent('10'); // value === 10, unit === 'percent' const {value, unit} = CSS.deg(45); // value === 45, unit === 'deg' const {value, unit} = CSS.ms(300); // value === 300, unit === 'ms' 

如需查看 CSS.* 方法的完整列表,请参阅规范。

数学值

CSSMathValue 对象表示数学表达式,通常包含多个值/单位。一个常见示例是创建 CSS calc() 表达式,但所有 CSS 函数都有相应的方法:calc()min()max()

new CSSMathSum(CSS.vw(100), CSS.px(-10)).toString(); // "calc(100vw + -10px)" new CSSMathNegate(CSS.px(42)).toString() // "calc(-42px)" new CSSMathInvert(CSS.s(10)).toString() // "calc(1 / 10s)" new CSSMathProduct(CSS.deg(90), CSS.number(Math.PI/180)).toString(); // "calc(90deg * 0.0174533)" new CSSMathMin(CSS.percent(80), CSS.px(12)).toString(); // "min(80%, 12px)" new CSSMathMax(CSS.percent(80), CSS.px(12)).toString(); // "max(80%, 12px)" 

嵌套表达式

使用数学函数创建更复杂的值会有点令人困惑。 以下是一些示例,可帮助您快速入门。我添加了额外的缩进,以便于阅读。

calc(1px - 2 * 3em) 的结构如下:

new CSSMathSum(  CSS.px(1),  new CSSMathNegate(  new CSSMathProduct(2, CSS.em(3))  ) ); 

calc(1px + 2px + 3px) 的结构如下:

new CSSMathSum(CSS.px(1), CSS.px(2), CSS.px(3)); 

calc(calc(1px + 2px) + 3px) 的结构如下:

new CSSMathSum(  new CSSMathSum(CSS.px(1), CSS.px(2)),  CSS.px(3) ); 

算术运算

CSS 类型化对象模型最实用的功能之一是,您可以对 CSSUnitValue 对象执行数学运算。

基本操作

支持基本操作(add/sub/mul/div/min/max):

CSS.deg(45).mul(2) // {value: 90, unit: "deg"} CSS.percent(50).max(CSS.vw(50)).toString() // "max(50%, 50vw)" // Can Pass CSSUnitValue: CSS.px(1).add(CSS.px(2)) // {value: 3, unit: "px"} // multiple values: CSS.s(1).sub(CSS.ms(200), CSS.ms(300)).toString() // "calc(1s + -200ms + -300ms)" // or pass a `CSSMathSum`: const sum = new CSSMathSum(CSS.percent(100), CSS.px(20))); CSS.vw(100).add(sum).toString() // "calc(100vw + (100% + 20px))" 

转化

绝对长度单位可以转换为其他长度单位:

// Convert px to other absolute/physical lengths. el.attributeStyleMap.set('width', '500px'); const width = el.attributeStyleMap.get('width'); width.to('mm'); // CSSUnitValue {value: 132.29166666666669, unit: "mm"} width.to('cm'); // CSSUnitValue {value: 13.229166666666668, unit: "cm"} width.to('in'); // CSSUnitValue {value: 5.208333333333333, unit: "in"} CSS.deg(200).to('rad').value // 3.49066... CSS.s(2).to('ms').value // 2000 

等式

const width = CSS.px(200); CSS.px(200).equals(width) // true const rads = CSS.deg(180).to('rad'); CSS.deg(180).equals(rads.to('deg')) // true 

CSS 转换值

CSS 转换通过 CSSTransformValue 创建,并传递转换值数组(例如 CSSRotateCSScaleCSSSkewCSSSkewXCSSSkewY)。例如,假设您想重新创建以下 CSS:

transform: rotateZ(45deg) scale(0.5) translate3d(10px,10px,10px); 

翻译为类型化 OM:

const transform = new CSSTransformValue([  new CSSRotate(CSS.deg(45)),  new CSSScale(CSS.number(0.5), CSS.number(0.5)),  new CSSTranslate(CSS.px(10), CSS.px(10), CSS.px(10)) ]); 

除了其冗长的内容(笑!),CSSTransformValue 具有一些很棒的功能。它具有一个布尔值属性来区分 2D 和 3D 转换,以及一个 .toMatrix() 方法来返回转换的 DOMMatrix 表示形式:

new CSSTranslate(CSS.px(10), CSS.px(10)).is2D // true new CSSTranslate(CSS.px(10), CSS.px(10), CSS.px(10)).is2D // false new CSSTranslate(CSS.px(10), CSS.px(10)).toMatrix() // DOMMatrix 

示例:为立方体添加动画效果

下面我们来看一个使用转换的实际示例。我们将使用 JavaScript 和 CSS 转换来为立方体添加动画效果。

const rotate = new CSSRotate(0, 0, 1, CSS.deg(0)); const transform = new CSSTransformValue([rotate]); const box = document.querySelector('#box'); box.attributeStyleMap.set('transform', transform); (function draw() {  requestAnimationFrame(draw);  transform[0].angle.value += 5; // Update the transform's angle.  // rotate.angle.value += 5; // Or, update the CSSRotate object directly.  box.attributeStyleMap.set('transform', transform); // commit it. })(); 

请注意:

  1. 数值意味着我们可以直接使用数学方法来增加角度!
  2. 动画通过更新底层 CSSTransformValue 数据对象来驱动,而不是在每个帧上触及 DOM 或回读值(例如,没有 box.style.transform=`rotate(0,0,1,${newAngle}deg)`),从而提高性能

演示

如果您的浏览器支持 Typed OM,您会在下方看到一个红色立方体。当您将鼠标悬停在立方体上时,立方体开始旋转。动画由 CSS Typed OM 提供支持!🤘

CSS 自定义属性值

CSS var() 在类型化对象模型中变为 CSSVariableReferenceValue 对象。它们的值会被解析为 CSSUnparsedValue,因为它们可以采用任何类型(px、%、em、rgba() 等)。

const foo = new CSSVariableReferenceValue('--foo'); // foo.variable === '--foo' // Fallback values: const padding = new CSSVariableReferenceValue(  '--default-padding', new CSSUnparsedValue(['8px'])); // padding.variable === '--default-padding' // padding.fallback instanceof CSSUnparsedValue === true // padding.fallback[0] === '8px' 

如果您想获取自定义属性的值,需要做一些工作:

<style> body { --foo: 10px; } </style> <script> const styles = document.querySelector('style'); const foo = styles.sheet.cssRules[0].styleMap.get('--foo').trim(); console.log(CSSNumericValue.parse(foo).value); // 10 </script> 

位置值

采用以空格分隔的 x/y 位置的 CSS 属性(例如 object-position)由 CSSPositionValue 对象表示。

const position = new CSSPositionValue(CSS.px(5), CSS.px(10)); el.attributeStyleMap.set('object-position', position); console.log(position.x.value, position.y.value); // → 5, 10 

解析值

Typed OM 为 Web 平台引入了解析方法!这意味着,您终于可以在尝试使用 CSS 值之前,以编程方式解析这些值了!这项新功能有助于尽早发现 bug 和格式错误的 CSS,从而挽救生命。

解析完整样式:

const css = CSSStyleValue.parse(  'transform', 'translate3d(10px,10px,0) scale(0.5)'); // → css instanceof CSSTransformValue === true // → css.toString() === 'translate3d(10px, 10px, 0) scale(0.5)' 

将值解析为 CSSUnitValue

CSSNumericValue.parse('42.0px') // {value: 42, unit: 'px'} // But it's easier to use the factory functions: CSS.px(42.0) // '42px' 

错误处理

示例 - 检查 CSS 解析器是否会接受此 transform 值:

try {  const css = CSSStyleValue.parse('transform', 'translate4d(bogus value)');  // use css } catch (err) {  console.err(err); } 

总结

很高兴 CSS 终于有了更新的对象模型。我一直觉得处理字符串不太顺手。CSS Typed OM API 有点冗长,但希望它能减少 bug,并最终生成性能更高的代码。