[譯] 開始使用新的 CSS Typed Object Model

justjavac發表於2018-03-28

原文: Working with the new CSS Typed Object Model


0. 前言

現在,CSS 擁有一個適當的基於物件的 API 來處理 JavaScript 中的值。

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

手動拼接字串和各種奇怪錯誤的日子已經結束了!

注:Chrome 66 為 CSS 屬性的一個子集增加了 CSS Typed Object Model 的支援 。

1. 介紹

1.1 舊的 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;
複製程式碼

1.2 新的 CSS Typed OM

作為 Houdini 工作的一部分,新的 CSS 型別物件模型(Typed 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');
複製程式碼

因為 StylePropertyMaps 是類似 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.
複製程式碼

請注意,在第 2 個示例中,opacity 設定為字串('0.3'),但稍後回讀屬性時會返回一個數字。

如果給定的 CSS 屬性支援數字,Typed OM 將接受一個字串作為輸入,但總是返回一個數字!舊 CSSOM 和新 Typed OM 之間的類比就如同 .className 的一步一步發展,最終有了自己的 API .classList

2. 優點

那麼 CSS Typed OM 試圖解決什麼問題?看一下上面的例子(以及本文的其餘部分),您可能會認為 CSS Typed OM 比舊的物件模型冗長得多。我同意!

在您放棄 Typed OM 之前,請考慮它帶來的一些主要特性:

  • 更少的bug。例如數字值總是以數字形式返回,而不是字串。

    el.style.opacity += 0.1;
    el.style.opacity === '0.30.1' // dragons!
    複製程式碼
  • 算術運算和單位轉換。在絕對長度單位(例如 px -> cm)之間進行轉換並進行基本的數學運算。

  • 數值範圍限制和舍入。Typed OM 通過對值進行範圍限制和舍入,以使其在屬性的可接受範圍內。

  • 更好的效能。瀏覽器必須做更少的工作序列化和反序列化字串值。現在,對於 CSS 值,引擎可以對 JS 和 C++ 使用相似的理解。Tab Akins 已經展示了一些早期的效能基準測試,與使用舊的 CSSOM 和字串相比,Typed OM 的執行速度快了 ~30%。這對使用 requestionAnimationFrame() 處理快速 CSS 動畫可能很重要 。crbug.com/808933 可以追蹤 Blink 的更多效能演示。

  • 錯誤處理。新的解析方法帶來了 CSS 世界中的錯誤處理。

  • “我應該使用駱駝式的 CSS 名稱還是字串呢?” 你不再需要猜測名字是駱駝還或字串(例如 el.style.backgroundColor vs el.style['background-color'])。Typed OM 中的 CSS 屬性名稱始終是字串,與您實際在 CSS 中編寫的內容一致:)

3. 瀏覽器支援和功能檢測

Typed OM 跟隨 Chrome 66 釋出,Firefox 也正在開發中。Edge 已經顯示出支援的跡象,但尚未將其新增到他們的 platform dashboard

注意:現在 Chrome 66+ 僅支援 CSS 屬性的一個子集

對於功能檢測,您可以使用如下程式碼:

if (window.CSS && CSS.number) {
  // Supports CSS Typed OM.
}
複製程式碼

4. API 基礎

4.1 訪問樣式

在 CSS Typed OM 中,單位是分開的。獲取樣式返回一個 CSSUnitValue,包含 valueunit:

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
複製程式碼

4.2 計算樣式

Computed styles 已經從 window 移動到了 HTMLElement,新的方法是 computedStyleMap()

舊的CSSOM

el.style.opacity = 0.5;
window.getComputedStyle(el).opacity === "0.5" // Ugh, more strings!
複製程式碼

新 Typed OM

el.attributeStyleMap.set('opacity', 0.5);
el.computedStyleMap().get('opacity').value // 0.5
複製程式碼

注:window.getComputedStyle()element.computedStyleMap() 有一個不同點,前者返回解析後的值,而後者返回計算後的值。例如,Typed OM 保留百分比值(width: 50%),而 CSSOM 將其解析為長度(例如 width: 200px)。

數值範圍限制/舍入

新物件模型的一個很好的功能是對計算樣式值進行自動範圍約束或舍入。舉一個例子,假設你嘗試為 opacity 設定一個超出可接受範圍 [0,1] 的值。Typed 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.
複製程式碼

5. CSS 數值

數字由 Typed OM 中 CSSNumericValue 物件的兩種型別來表示:

  • CSSUnitValue - 包含單個單位型別(例如 "42px")的值。
  • CSSMathValue - 包含多個值/單位的值,如數學表示式(例如 "calc(56em + 10%)")。

5.1 單位值

簡單的數值("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'
複製程式碼

注意:如示例所示,這些方法可以傳遞一個 NumberString 型別的數字。

請參閱規範以獲取完整的 CSS.* 方法列表

5.2 數學值

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)
);
複製程式碼

5.3 算術運算

CSS Typed OM 最有用的功能之一是可以對 CSSUnitValue 物件執行數學運算。

5.3.1 基本操作

基本操作(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))"
複製程式碼

5.3.2 轉變

絕對長度單位可以轉換為其他單位長度:

// 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.49066rad"
CSS.s(2).to('ms').value // 2000
複製程式碼

5.3.3 等值判斷

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
複製程式碼

6. CSS transform 值

使用 CSSTransformValue 可以建立 CSS 變換,引數為 transform 值組成的陣列(例如 CSSRotateCSScaleCSSSkewCSSSkewXCSSSkewY)。作為一個例子,假設你想重新建立這個 CSS:

{
transform: rotateZ(45deg) scale(0.5) translate3d(10px,10px,10px);
}
複製程式碼

翻譯成 TypedOM:

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))
]);
複製程式碼

除了它的冗長(lolz!)之外,CSSTransformValue 還有一些很酷的功能。它具有區分二維和三維變換的布林屬性以及 .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 transform 來為多維資料集製作動畫。

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.
})();
複製程式碼

請注意:

  • 數值(Numerical value)意味著我們可以直接使用數學方法增加角度!

  • 不需要操作 DOM 或者在每一幀都讀取當前的值(例如使用 box.style.transform=\rotate(0,0,1,${newAngle}deg)`),通過更新底層CSSTransformValue` 資料物件來驅動動畫,從而提高效能

演示

下面,如果您的瀏覽器支援 Typed OM,您會看到一個紅色的立方體。當您將滑鼠懸停在該立方體上時,該立方體開始旋轉。動畫由 CSS Typed OM 提供支援!

7. CSS 自定義屬性值

CSS 在 Typed OM 中 var() 成為一個 CSSVariableReferenceValue 物件。它們的值被解析為 CSSUnparsedValue 因為它們可以採用任何型別(px%emrgba() 等)。

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>
複製程式碼

7.1 位置值

CSS 屬性的位置值採用空格分隔的 x/y,例如 object-positionCSSPositionValue 物件表示。

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
複製程式碼

7.2 解析值

Typed OM 將解析方法引入到 Web 平臺!這意味著您可以在使用它之前以程式設計方式解析 CSS 值!這個新功能可以捕獲 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'
複製程式碼

7.3 錯誤處理

例子 - 檢查 CSS 解析器是否符合 transform 值:

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

8. 結論

很高興終於有了一個更新的 CSS 物件模型。我從來沒有覺得使用字串很舒服。CSS Typed OM API 雖然有點冗長,但希望它可以減少錯誤和提升效能。

相關文章