[Перевод] Тёмная сторона использования полифиллов на CSS

В прошлом году я написал статью для Smashing Magazine о Houdini и назвал его «самым потрясающим проектом CSS, о котором вы никогда не слышали». В этой статье я объясню, что набор Houdini API позволит (среди прочего) расширить функции 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 и т. д.).

На протяжении всей остальной статьи мы будем работать с демо-страницей, которую я показывал в своей лекции. Вот как она выглядит:

566763c5bce8580a63874bdb22d63b22.png
Пример, как может выглядеть сайт, где используется ключевое слово 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 можно добавить на страницу с помощью или элементов