[Перевод] Тёмная сторона использования полифиллов на CSS
Хотя та статья была в целом хорошо принята, один и тот же вопрос постоянно задавали мне в письмах и твиттере. Основная суть вопроса:
Что такого сложного в полифиллах CSS? Я использую много полифиллов CSS, и они у меня нормально работают.
И я понял — конечно же, у людей возникают такие вопросы. Если вы никогда не пробовали сами написать полифилл CSS, то, вероятно, никогда не испытывали эту боль.
Так что лучший способ ответить на этот вопрос — и объяснить, почему меня так восхищает Houdini — это наглядно показать, почему настолько трудно использовать полифиллы CSS.
И лучший способ сделать это — написать полифилл самим.
Примечание: эта статья представляет собой текстовую версию лекции, которую я прочитал на dotCSS 2 декабря 2016 года. В статье рассматривается чуть больше подробностей, но если вы предпочитаете посмотреть видео, я его тоже вставил сюда.Ключевое слово
randomФункция, из которой мы хотим сделать полифилл — это новое (предположим, что оно новое) ключевое слово
random, которое возвращает число между 0 и 1 (так же, как Math.random() в JavaScript). Вот пример использования random:
.foo {
color: hsl(calc(random * 360), 50%, 50%);
opacity: random;
width: calc(random * 100%);
}Как видите, поскольку
random возвращает безразмерное число, то его можно использовать с calc(), чтобы превратить практически в любое значение. И поскольку у него может быть любое значение, то его можно применить с любым свойством (например, color, opacity, width и т. д.).На протяжении всей остальной статьи мы будем работать с демо-страницей, которую я показывал в своей лекции. Вот как она выглядит:

Пример, как может выглядеть сайт, где используется ключевое слово random
Это базовая страница «Hello World» из стартового шаблона Bootstrap, где в верхнюю часть области контента добавлены четыре элемента .progress-bar.
Кроме bootstrap.css, она содержит ещё один файл CSS со следующим правилом:
.progress-bar {
width: calc(random * 100%);
}Хотя на моей демо-странице явно указана значения ширины индикаторов выполнения, идея в том, что при использовании полифиллов каждый раз при загрузке страницы эти индикаторы будут иметь разную, случайную ширину.Как работают полифиллы
В JavaScript полифиллы относительно просто написать, потому что язык настолько динамичный и позволяет вам изменять встроенные объекты в реальном времени.
Например, если хотите сделать полифилл из Math.random(), то пишете что-то вроде такого:
if (typeof Math.random != 'function') {
Math.random = function() {
// Implement polyfill here...
};
}С другой стороны, CSS не настолько динамичен. Невозможно (по крайней мере, пока) изменить среду выполнения таким образом, чтобы сообщить браузеру о новой функции, которую он нативно не поддерживает.
Это значит, что для применения полифилла с функцией в CSS, которую браузер не поддерживает, вам придётся динамически изменить CSS, чтобы подделать поведение функции с помощью функций CSS, которые браузер поддерживает.
Другими словами, вам нужно превратить это:
.foo {
width: calc(random * 100%);
}в нечто вроде такого, что случайно генерируется во время выполнения кода в браузере:
.foo {
width: calc(0.35746 * 100%);
}Изменение CSS
Теперь мы знаем, что нам нужно изменить существующий CSS и добавить новые правила стилей, которые имитируют поведение функции из полифилла.
Наиболее естественным местом, где вы могли бы предположить возможность совершения такого действия, будет CSS Object Model (CSSOM), доступный через document.styleSheets. Код может выглядеть примерно так:
for (const stylesheet of document.styleSheets) {
// Flatten nested rules (@media blocks, etc.) into a single array.
const rules = [...stylesheet.rules].reduce((prev, next) => {
return prev.concat(next.cssRules ? [...next.cssRules] : [next]);
}, []);
// Loop through each of the flattened rules and replace the
// keyword `random` with a random number.
for (const rule of rules) {
for (const property of Object.keys(rule.style)) {
const value = rule.style[property];
if (value.includes('random')) {
rule.style[property] = value.replace('random', Math.random());
}
}
}
}Примечание: в настоящем полифилле вы не будете использовать простую функцию поиска и замены слова
random, потому что оно может присутствовать в разных формах, а не только в ключевом слове (например, в URL, в названии какого-то свойства, в закавыченном тексте в свойстве content и т. д.). Реальный код в окончательной версии демо использует более надёжный механизм замены, но для простоты я здесь использую упрощённую версию.Если загрузить демо № 2, вставить вышеприведённый код в консоль JavaScript и запустить, то он реально сделает то, что должен делать, но после его выполнения вы не увидите никаких индикаторов выполнения случайной ширины.
Причина в том, что в CSSOM нет ни одного правила с ключевым словом random!
Как вы наверное уже знаете, если браузер встречает правило CSS, которое не понимает, то просто игнорирует его. В большинстве случаев это хорошо, потому что так вы можете загружать CSS в старые браузеры и не поломаете страницу. К сожалению, это также означает, что если вам нужен доступ к изначальному, неизменённому CSS, то придётся доставать его самостоятельно.
Извлечение стилей страниц вручную
Правила CSS можно добавить на страницу с помощью или элементов
, или , так что для получения изначального, неизменённого CSS вы можете применить querySelectorAll() на документе и вручную достать содержимое любых тегов или применить fetch(), получив URL ресурсов для всех тегов .Следующий код определяет функцию getPageStyles, которая должна вернуть полный код CSS для всех стилей страницы:
const getPageStyles = () => {
// Query the document for any element that could have styles.
var styleElements =
[...document.querySelectorAll('style, link[rel="stylesheet"]')];
// Fetch all styles and ensure the results are in document order.
// Resolve with a single string of CSS text.
return Promise.all(styleElements.map((el) => {
if (el.href) {
return fetch(el.href).then((response) => response.text());
} else {
return el.innerHTML;
}
})).then((stylesArray) => stylesArray.join('\n'));
}Если открыть демо № 3 и вставить вышеприведённый код в консоль JavaScript для установки функции
getPageStyles(), то вы сможете запустить код ниже, чтобы получить лог полного текста CSS: getPageStyles().then((cssText) => {
console.log(cssText);
});Парсинг извлечённых стилей
Когда вы получили оригинальный текст CSS, нужно осуществить парсинг.
Вы можете подумать, что если в браузере уже есть встроенный парсер, то можно вызвать какую-то функцию и распарсить CSS. К сожалению, так не получится. И даже если бы браузер давал доступ к функции parseCSS(), это не отменяет того факта, что браузер не понимает ключевое слово random, так что функция parseCSS(), вероятно, всё равно не будет работать (есть надежда, что будущие спецификации парсинга позволяет обработку незнакомых ключевых слов, которые иным образом совместимы с существующим синтаксисом).
Есть несколько хороших open source парсеров CSS, и для целей данного демо мы будем использовать PostCSS (поскольку он работает как браузер и поддерживает систему плагинов, которая пригодится нам позже).
Если запустить postcss.parse() на следующем тексте CSS:
.progress-bar {
width: calc(random * 100%);
}то получим что-то вроде такого:
{
"type": "root",
"nodes": [
{
"type": "rule",
"selector": ".progress-bar",
"nodes": [
{
"type": "decl",
"prop": "width",
"value": "calc(random * 100%)"
}
]
}
]
}Это то, что известно как абстрактное синтаксическое дерево (АСД), а вы можете представить его как собственную версию CSSOM.
Теперь у нас есть служебная функция для получения полного текста CSS и функция для его парсинга, тогда вот как выглядит наш полифилл на данный момент:
import postcss from 'postcss';
import getPageStyles from './get-page-styles';
getPageStyles()
.then((css) => postcss.parse(css))
.then((ast) => console.log(ast));Если открыть демо № 4 и посмотреть в консоль JavaScript, то увидите лог объекта, содержащий полное АСД для PostCSS для всех стилей на странице.Внедрение полифилла
К настоящему моменту мы написали много кода, но удивительно, что он совершенно не связан с реальной функциональностью нашего полифилла. Это была просто необходимая платформа для того, чтобы вручную сделать много вещей, которые браузер должен был сделать за нас.
Для реальной реализации логики полифилла нам нужно:
- Изменить АСД CSS, заменить встреченные
randomслучайным числом. - Вставить изменённое АСД в строковом виде обратно в CSS.
- Заменить существующие стили страниц на изменённые стили.
Изменение абстрактного синтаксического дерева CSS
PostCSS поставляется с хорошей системой плагинов со многими вспомогательными функциями для модификации абстрактного синтаксического дерева CSS. Мы можем использовать эти функции, чтобы заменить встреченные
random случайным числом: const randomKeywordPlugin = postcss.plugin('random-keyword', () => {
return (css) => {
css.walkRules((rule) => {
rule.walkDecls((decl, i) => {
if (decl.value.includes('random')) {
decl.value = decl.value.replace('random', Math.random());
}
});
});
};
});Вставка АСД в строковом виде обратно в CSS
Ещё одна приятная особенность использования плагинов PostCSS — у них уже есть встроенная логика для вставки АСД в строковом виде обратно в CSS. Всё что нужно сделать — это создать инстанс PostCSS, передать его плагину (или плагинам), которые вы хотите использовать, и запустить
process(), который должен вернуть объект с CSS в строковом виде: postcss([randomKeywordPlugin]).process(css).then((result) => {
console.log(result.css);
});Замена стилей страницы
Для замены стилей страницы мы можем написать служебную функцию (похожую на
getPageStyles()), которая находит все элементы и и удаляет их. Она также создаёт новый тег и устанавливает содержимое стиля на любой текст CSS, который передан функции: const replacePageStyles = (css) => {
// Get a reference to all existing style elements.
const existingStyles =
[...document.querySelectorAll('style, link[rel="stylesheet"]')];
// Create a new