Praca z nowym obiektem typu CSS

TL;DR

CSS ma teraz odpowiedni interfejs API oparty na obiektach do pracy z wartościami w JavaScript.

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

Koniec z łączeniem ciągów znaków i subtelnymi błędami.

Wprowadzenie

Stary CSSOM

CSS ma model obiektowy (CSSOM) od wielu lat. W rzeczywistości używasz go za każdym razem, gdy odczytujesz lub ustawiasz wartość .style w JavaScript:

// 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;

Nowy model obiektowy CSS

Nowy model obiektów z określonym typem CSS (Typed OM), który jest częścią projektu Houdini, rozszerza tę perspektywę, dodając typy, metody i odpowiedni model obiektów do wartości CSS. Zamiast ciągów znaków wartości są udostępniane jako obiekty JavaScript, co ułatwia wydajne (i rozsądne) manipulowanie CSS.

Zamiast element.style będziesz uzyskiwać dostęp do stylów za pomocą nowej właściwości .attributeStyleMap dla elementów i właściwości .styleMap dla reguł arkusza stylów. Obie zwracają obiekt 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');

Ponieważ obiekty StylePropertyMap są podobne do map, obsługują wszystkie standardowe metody (get/set/keys/values/entries), co sprawia, że są elastyczne w użyciu:

// 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.

Zwróć uwagę, że w drugim przykładzie wartość opacity jest ustawiona jako ciąg znaków ('0.3'), ale gdy później odczytasz właściwość, otrzymasz liczbę.

.

Zalety

Jakie problemy ma rozwiązać CSS Typed OM? Patrząc na powyższe przykłady (i na resztę tego artykułu), można stwierdzić, że CSS Typed OM jest znacznie bardziej rozbudowany niż stary model obiektowy. Zgadzam się!

Zanim odrzucisz Typed OM, zapoznaj się z jego najważniejszymi funkcjami:

  • Mniej błędów, np. wartości liczbowe są zawsze zwracane jako liczby, a nie ciągi znaków.

    el.style.opacity += 0.1;
    el.style.opacity === '0.30.1' // dragons!
    
  • Działania arytmetyczne i przeliczanie jednostek: przeliczanie jednostek długości bezwzględnej (np. px –> cm) i wykonywanie podstawowych działań matematycznych.

  • Ograniczanie i zaokrąglanie wartości Wpisany OM zaokrągla lub ogranicza wartości, aby mieściły się w dopuszczalnych zakresach właściwości.

  • Zwiększona skuteczność Przeglądarka musi wykonać mniej pracy przy serializacji i deserializacji wartości ciągów znaków. Teraz silnik podobnie interpretuje wartości CSS w JS i C++. Tab Akins zaprezentował wstępne testy wydajności, które pokazują, że Typed OM jest o około 30% szybszy w operacjach na sekundę w porównaniu ze starym CSSOM i ciągami znaków. Może to mieć duże znaczenie w przypadku szybkich animacji CSS wykorzystujących requestionAnimationFrame(). crbug.com/808933 zawiera informacje o dodatkowych pracach nad wydajnością w Blinku.

  • Obsługa błędów Nowe metody analizowania wprowadzają obsługę błędów w świecie CSS.

  • „Czy mam używać nazw CSS w notacji camel-case czy ciągów znaków?” Nie musisz już zgadywać, czy nazwy są pisane wielbłądzią notacją, czy są ciągami znaków (np.el.style.backgroundColor czy el.style['background-color']). Nazwy właściwości CSS w Typed OM są zawsze ciągami znaków, co odpowiada temu, co faktycznie piszesz w CSS.

Obsługa przeglądarek i wykrywanie funkcji

Wpisane OM pojawiły się w Chrome 66 i są wdrażane w Firefoxie. Przeglądarka Edge wykazuje oznaki obsługi, ale nie dodała jeszcze tej funkcji do panelu platformy.

W przypadku wykrywania funkcji możesz sprawdzić, czy zdefiniowano jedną z tych fabryk wartości liczbowych: CSS.*.

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

Podstawy interfejsu API

Dostęp do stylów

W CSS Typed OM wartości są oddzielone od jednostek. Pobranie stylu zwraca obiekt CSSUnitValue zawierający 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

Style wynikowe

Obliczone style zostały przeniesione z interfejsu API na window do nowej metody na HTMLElement, computedStyleMap():

Stary CSSOM

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

Nowy typ OM

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

Ograniczanie / zaokrąglanie wartości

Jedną z przydatnych funkcji nowego modelu obiektów jest automatyczne ograniczanie lub zaokrąglanie obliczonych wartości stylu. Załóżmy na przykład, że próbujesz ustawić wartość parametru opacity na wartość spoza dopuszczalnego zakresu [0, 1]. Wpisany OM ogranicza wartość do 1 podczas obliczania stylu:

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

Podobnie ustawienie z-index:15.4 zaokrągla się do 15, więc wartość pozostaje liczbą całkowitą.

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.

Wartości liczbowe CSS

Liczby są reprezentowane przez 2 typy CSSNumericValueobiektów w Typed OM:

  1. CSSUnitValue – wartości zawierające jeden typ jednostki (np. "42px").
  2. CSSMathValue – wartości zawierające więcej niż 1 wartość lub jednostkę, np.wyrażenie matematyczne (np. "calc(56em + 10%)").

Wartości jednostek

Proste wartości liczbowe ("50%") są reprezentowane przez obiekty CSSUnitValue. Możesz bezpośrednio tworzyć te obiekty (new CSSUnitValue(10, 'px')), ale najczęściej będziesz używać metod fabrycznych 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'

Pełną listę metod CSS.* znajdziesz w specyfikacji.

Wartości matematyczne

CSSMathValue obiekty reprezentują wyrażenia matematyczne i zwykle zawierają więcej niż jedną wartość lub jednostkę. Typowym przykładem jest utworzenie wyrażenia CSS calc(), ale istnieją metody dla wszystkich funkcji 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)"

Wyrażenia zagnieżdżone

Używanie funkcji matematycznych do tworzenia bardziej złożonych wartości może być nieco mylące. Poniżej znajdziesz kilka przykładów, które pomogą Ci zacząć. Dodałem dodatkowe wcięcia, aby były bardziej czytelne.

calc(1px - 2 * 3em) będzie wyglądać tak:

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

calc(1px + 2px + 3px) będzie wyglądać tak:

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

calc(calc(1px + 2px) + 3px) będzie wyglądać tak:

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

Operacje arytmetyczne

Jedną z najbardziej przydatnych funkcji interfejsu CSS Typed OM jest możliwość wykonywania operacji matematycznych na obiektach CSSUnitValue.

Podstawowe operacje

Obsługiwane są podstawowe operacje (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))"

Konwersja

Jednostki długości bezwzględnej można przeliczać na inne jednostki długości:

// 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

Equality

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

Wartości przekształceń CSS

Przekształcenia CSS są tworzone za pomocą funkcji CSSTransformValue i przekazywania tablicy wartości przekształceń (np. CSSRotate, CSScale, CSSSkew, CSSSkewX, CSSSkewY). Załóżmy, że chcesz odtworzyć ten kod CSS:

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

Przetłumaczono na wpisany 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))
]);

Oprócz tego, że jest bardzo rozbudowany (lolz!), CSSTransformValue ma kilka fajnych funkcji. Ma właściwość logiczną, która odróżnia przekształcenia 2D i 3D, oraz metodę .toMatrix(), która zwraca reprezentację DOMMatrix przekształcenia:

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

Przykład: animowanie sześcianu

Przyjrzyjmy się praktycznemu przykładowi użycia przekształceń. Do animowania sześcianu będziemy używać JavaScriptu i przekształceń 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.
})();

Pamiętaj, że:

  1. Wartości liczbowe oznaczają, że możemy bezpośrednio zwiększać kąt za pomocą działań matematycznych.
  2. Zamiast modyfikować DOM lub odczytywać wartość w każdej klatce (np. bez box.style.transform=`rotate(0,0,1,${newAngle}deg)`), animacja jest sterowana przez aktualizowanie bazowego obiektu danych CSSTransformValue, co zwiększa wydajność.

Prezentacja

Jeśli Twoja przeglądarka obsługuje Typed OM, zobaczysz poniżej czerwony sześcian. Kostka zaczyna się obracać, gdy najedziesz na nią kursorem. Animacja jest oparta na interfejsie CSS Typed OM. 🤘

Wartości właściwości niestandardowych CSS

CSS var() staje się obiektem CSSVariableReferenceValue w Typed OM. Ich wartości są analizowane w CSSUnparsedValue, ponieważ mogą przyjmować dowolny typ (px, %, em, rgba() itp.).

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'

Jeśli chcesz uzyskać wartość właściwości niestandardowej, musisz wykonać kilka czynności:

<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>

Wartości pozycji

Właściwości CSS, które przyjmują rozdzieloną spacją pozycję x/y, np. object-position, są reprezentowane przez obiekty 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

Analizowanie wartości

Wpisany model obiektów wprowadza do platformy internetowej metody analizowania. Oznacza to, że możesz wreszcie programowo analizować wartości CSS zanim spróbujesz ich użyć. Ta nowa funkcja może uratować Ci życie, ponieważ pozwala wykrywać błędy i nieprawidłowo sformatowany kod CSS na wczesnym etapie.

Analizowanie pełnego stylu:

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)'

Przetwarzanie wartości w CSSUnitValue:

CSSNumericValue.parse('42.0px') // {value: 42, unit: 'px'}

// But it's easier to use the factory functions:
CSS.px(42.0) // '42px'

Obsługa błędów

Przykład – sprawdź, czy parser CSS zaakceptuje tę wartość transform:

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

Podsumowanie

Miło, że w końcu mamy zaktualizowany model obiektów CSS. Praca z ciągami znaków nigdy mi nie odpowiadała. Interfejs CSS Typed OM API jest nieco rozbudowany, ale miejmy nadzieję, że w przyszłości pozwoli to zmniejszyć liczbę błędów i zwiększyć wydajność kodu.