[Перевод] Тёмная сторона использования полифиллов на 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