Работа с новой типизированной объектной моделью 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'

Времена объединения строк и мелких ошибок прошли!

Введение

Старый CSSOM

В CSS уже много лет существует объектная модель (CSSOM). Фактически, каждый раз, когда вы читаете/устанавливаете .style в 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;

Новый CSS-типизированный OM

Новая модель типизированных объектов CSS (Typed OM), являющаяся частью усилий Houdini , расширяет это мировоззрение, добавляя типы, методы и правильную объектную модель к значениям 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, рассмотрите некоторые ключевые особенности, которые он приносит:

  • Меньше ошибок . например, числовые значения всегда возвращаются в виде чисел, а не строк.

    el.style.opacity += 0.1;
    el.style.opacity === '0.30.1' // dragons!
    
  • Арифметические операции и преобразование единиц измерения . конвертируйте абсолютные единицы длины (например, px -> cm ) и выполняйте базовые математические операции .

  • Фиксация и округление значений . Введенные значения OM округляются и/или ограничиваются , чтобы они находились в допустимых диапазонах для свойства.

  • Лучшая производительность . Браузеру приходится выполнять меньше работы по сериализации и десериализации строковых значений. Теперь движок использует одинаковое понимание значений CSS в JS и C++. Таб Акинс продемонстрировал несколько ранних тестов производительности , которые показали, что Typed OM работает примерно на 30% быстрее в операциях в секунду по сравнению с использованием старого CSSOM и строк. Это может быть важно для быстрой CSS-анимации с использованием requestionAnimationFrame() . crbug.com/808933 отслеживает дополнительную работу по повышению производительности в Blink.

  • Обработка ошибок . Новые методы синтаксического анализа привносят обработку ошибок в мир CSS.

  • «Должен ли я использовать CSS-имена или строки в верблюжьем стиле?» Больше не нужно гадать, написаны ли имена в верблюжьем регистре или в виде строк (например, el.style.backgroundColor или el.style['background-color'] ). Имена свойств CSS в Typed OM всегда представляют собой строки, соответствующие тому, что вы на самом деле пишете в CSS :)

Поддержка браузера и обнаружение функций

Typed OM появился в Chrome 66 и реализуется в Firefox. Edge продемонстрировал признаки поддержки , но еще не добавил ее на панель управления своей платформы .

Для обнаружения функций вы можете проверить, определена ли одна из числовых фабрик CSS.* :

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

Основы API

Доступ к стилям

Значения отделены от единиц в CSS Typed OM. Получение стиля возвращает CSSUnitValue , содержащий value и unit измерения:

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

Вычисляемые стили

Вычисляемые стили были перенесены из API в window в новый метод 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

Числа представлены двумя типами объектов CSSNumericValue в Typed OM:

  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 Typed OM является то, что вы можете выполнять математические операции над объектами 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 и передачи массива значений преобразования (например, CSSRotate , CSScale , CSSSkew , CSSSkewX , CSSSkewY ). В качестве примера предположим, что вы хотите воссоздать этот 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. Вместо того, чтобы касаться DOM или считывать значение в каждом кадре (например, нет box.style.transform=`rotate(0,0,1,${newAngle}deg)` ), анимация управляется обновлением базовых данных CSSTransformValue . объект, улучшающий производительность .

Демо

Ниже вы увидите красный куб, если ваш браузер поддерживает Typed OM. Куб начинает вращаться, когда вы наводите на него курсор мыши. Анимация создана на основе CSS Typed OM! 🤘

Значения пользовательских свойств CSS

CSS var() становится объектом CSSVariableReferenceValue в типизированной OM. Их значения анализируются в 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>

Значения позиции

Свойства CSS, которые занимают позицию x/y, разделенную пробелами, например 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 представляет методы синтаксического анализа на веб-платформе! Это означает, что вы, наконец, можете программно анализировать значения 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'

Обработка ошибок

Пример . Проверьте, будет ли синтаксический анализатор CSS доволен этим значением transform :

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

Заключение

Приятно наконец иметь обновленную объектную модель для CSS. Мне никогда не казалось правильным работать со струнами. CSS Typed OM API немного многословен, но, надеюсь, это приведет к меньшему количеству ошибок и повышению производительности кода.