Поддержка CSS Typed OM появилась в Chrome. Как это облегчит жизнь разработчикам?

Liudmila Mzhachikh
5 min readMay 6, 2018

В предыдущем посте я писала о том, что пользовательские свойства CSS имеют одно значительное ограничение — они не типизированы, а как следствие не анимируемы. Как решить эту проблему? Кажется, ответ напрашивается сам собой — надо как-то сообщить браузеру, к какому типу относится то или иное пользовательское свойство. Но вот незадача: процесс работы модуля отображения браузера представляется неким “черным ящиком”, работающим по принципам черной магии. Разработчик практически не может на него влиять. Но совсем недавно был сделан огромный шаг к тому, чтобы исправить это — в Chrome 66 появилась поддержка CSS Typed Object Model. Какие преимущества это дает разработчикам, я постараюсь объяснить на примерах в этой статье.

Если вы когда-либо работали со стилями из JS, то знаете, что надо проделать вот такие странные действия:

element.style.transform = 
'translate(' + width + 'px, ' + (height + 20) + 'px)';

Приходится делать конкатенацию строк, шаблонные строки немного улучшают ситуацию, но не сильно

element.style.transform =
`translate(${width}px, ${height + 20}px)`;

Это не просто плохо читабельно, но и вообще весьма странно. У нас есть число, мы переводим его в строку, CSS снова переводит его в число. Пытаемся получить значение числового свойства CSS, а возвращается строка:

el.style.opacity = 0.3; // Это число
typeof el.style.opacity === 'string' // Да, это строка

Не говоря уже о том, что браузер производит много лишних действий по преобразованию.

На самом деле JS ничего не знает о CSS, он работает с CSS Object Model (Объектная модель CSS) —древовидной структурой, подобной DOM, содержащей маппинг свойств и селекторов, к которым эти свойства должны быть применены, а также определяющей API для работы с этой структурой из JS (я уже писала о том, какое место занимает построение CSSOM в пайплайне рендеринга). Здесь и кроется объяснение такому нелогичному поведению — дело в том, что объектная модель не типизирована, то есть значения свойств представлены всегда строками.

Конечно же, в CSS и так есть типы: например, мы знаем, что паддинг принимает значение длины либо процент (<length> | <percentage>). В спецификации определены типы значений CSS-свойств. Но проблема в том, что мы никак не можем с ними взаимодействовать.

Процесс рендеринга состоит из следующих стадий:

Сначала скачиваются и парсятся ресурсы, затем строится DOM-дерево и объектная модель CSS, на основе которых формируется дерево рендеринга. Затем рассчитывается Layout, то есть расположение элементов во вьюпорте. Затем происходит, собственно, отрисовка. В современных браузерах добавляется еще один этап — композиция. Для улучшения производительности некоторые элементы вытаскиваются в отдельных слой, поэтому на финальном этапе происходит композиция этих слоев в один.

И на самом деле разработчик может влиять на процесс из JS только на одной стадии.

Однако, существует проект Гудини, основная идея которого в соотвествии с The Extensible Web Manifesto открыть разработчикам браузерные API, дать возможность расширять возможности CSS самого по себе. Когда вы хотите использовать новые, еще не поддерживаемые возможности JS, вы берете полифилл или транспайлер. С CSS дела обстоят немного сложнее. Поэтому появилась рабочая группа проекта Гудини, который представляет собой набор API:

CSS Typed OM пришла к нам как раз из проекта Гудини. Есть полифилл для ее эмуляции. CSSTOM была под флагом в Chrome Canary и теперь поддерживается в Chrome 66.

Чтение и запись свойств

Как вы знаете, свойство element.style возвращает объект, который дает доступ к стилю элемента на чтение и запись.

const styles = el.style;

.style возвращает объект CSSStyleDeclaration.

// Запись
el.style.opacity = 0.3;
element.style.width="100px";
// Чтение
const elWidth = styles.width; // 100px
typeof elWidth // string (если точнее, DOMString)

Теперь доступ к стилям можно получить посредством .attributeStyleMap. Он возвращает объект StylePropertyMap.

// Запись
el.attributeStyleMap.set('width', CSS.px(100));
el.attributeStyleMap.set('opacity', CSS.number(0.3));
// Чтение
el.attributeStyleMap.get('opacity'); // number

В этой записи создается новое значение с помощью factory function, это то же самое, как если бы я написала: “new CSSUnitValue(10, ‘px’)”. Кстати, если в качестве значения передавать строку, то это тоже будет работать.

Смотрите, что возвращается в качестве значения ширины:

Теперь можно получить значение от дельно от единиц измерения:

const {value, unit} = styles.get('width');
// value === 100, unit === 'px'

Чтобы получить стили с учетом каскада:

window.getComputedStyle(el).opacity; // string
el.computedStyleMap().get('opacity').value; // number

Обратите внимание, что метод в первом случае применяется к window, а во втором — к элементу.

Иерархия классов

Базовый класс CSSStyleValue, от него наследуются остальные. Если свойство нельзя отнести ни к одному из дочерних классов, оно относится к CSSStyleValue. CSSTOM определяет следующую иерархию свойств:

- CSSStyleValue  - CSSUnparsedValue (--my-prop: some value)  - CSSKeywordValue (opacity: initial)  - CSSNumericValue
- CSSUnitValue (height: 20px)
- CSSMathValue (width: calc(50% + 5px))
- CSSPositionValue (object-position: top 50%) - CSSTransformValue (transform: translate(50%, 50%)) - CSSResourceValue
- CSSImageValue (background-image: ...)
- CSSFontFaceValue (@font-face {...})

Пример разбора

Давайте рассмотрим на примере, как происходит преобразование. Вот такая строка

calc(42px + var(--foo, 15em) + var(--bar, var(--far) + 15px))

будет преобразована в CSSUnparsedValue, внутри которого будут:

Парсинг значений

У класса CSSStyleValue есть метод parse, то есть теперь можно распарсить строковое представление значения CSS:

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

CSS Custom Properties

CSS-переменные являются объектами CSSVariableReferenceValue. Их значения относятся к классу CSSUnparsedValue (т.к. заранее неизвестно, что записано в переменную).

const foo = new CSSVariableReferenceValue('--foo');

// Fallback values:
const padding = new CSSVariableReferenceValue(
'--default-padding', new CSSUnparsedValue(['8px']));

Браузерная поддержка

Проверка:

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

Поддержка есть в Chrome, внедряется в FF.

Что все это дает

  • Удобство, лучшая читабельность и логичность за счет того, что больше не надо делать конкатенацию строк
  • Потенциально меньше багов, связанных с этим
  • Лучшая производительность, так как не нужно делать лишнюю работу — преобразоваться числа в строки и обратно
  • Можно получать отдельно значение от единиц измерения
  • Можно парсить значения свойств
  • Можно отлавливать ошибки на ранних этапах (с помощью того же парсинга)
try {
const css = CSSStyleValue.parse(
'transform', 'translate4d(bogus value)');
} catch (err) {
console.err(err);
}

Может показаться, что вместе с CSS Typed OM пришло больше кода и заморочек, ведь мы жили же мы как-то раньше, и все работало. Но новая модель дает новые возможности, более логичную и прозрачную схему работы с CSS. Она дает возможность ловить баги. Рука об руку с CSS Typed OM идет возможность задавать тип CSS-переменным, а значит, возможность их анимировать (CSS Properties & Values API). Но самое главное — что это огромный шаг на пути к тому, чтобы CSS перестал быть черным ящиком для разработчиков.

Материалы по теме

Подписывайтесь на блоги:

Телеграм: frontend_thoughts

Instagram: lucy_frontend

--

--

Liudmila Mzhachikh

Frontend developer at Mail.Ru Group 👩‍💻, leader of moscowcss community, conference speaker 🎤, write about IT, channel: t.me/frontend_thoughts