TL;DR
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++. Таб Акинс продемонстрировал несколько ранних тестов производительности , которые показали, что типизированная OM работает примерно на 30% быстрее по количеству операций в секунду по сравнению со старым CSSOM и строками. Это может быть существенно для быстрой CSS-анимации с использованием
requestionAnimationFrame(). crbug.com/808933 отслеживает дополнительную работу над производительностью в Blink.Обработка ошибок . Новые методы анализа привносят обработку ошибок в мир CSS.
«Что использовать: CSS-имена в стиле «camelCase» или строки?» Больше не нужно гадать, написаны ли имена в стиле «camelCase» или являются строками (например,
el.style.backgroundColorилиel.style['background-color']). Имена CSS-свойств в типизированной объектной модели (OM) всегда являются строками, что соответствует тому, что вы фактически пишете в CSS :)
Поддержка браузеров и обнаружение функций
Типизированный 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 в типизированной OM:
-
CSSUnitValue— значения, содержащие один тип единицы измерения (например"42px"). -
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.
})();
Обратите внимание, что:
- Числовые значения означают, что мы можем увеличивать угол напрямую, используя математику!
- Вместо обращения к DOM или считывания значения в каждом кадре (например, no
box.style.transform=`rotate(0,0,1,${newAngle}deg)`), анимация управляется путем обновления базового объекта данныхCSSTransformValue, что повышает производительность .
Демо
Ниже вы увидите красный куб, если ваш браузер поддерживает 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>
Значения позиций
Свойства 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. Работа со строками всегда казалась мне неудобной. API CSS Typed OM немного громоздкий, но, надеюсь, это приведёт к уменьшению количества ошибок и повышению производительности кода в будущем.