Поддержка CSS-in-JS в DevTools

Алексей Руденко
Alex Rudenko

В этой статье рассказывается о поддержке CSS-in-JS в DevTools, появившейся начиная с Chrome 85, и в целом о том, что мы подразумеваем под CSS-in-JS и чем он отличается от обычного CSS, который поддерживается DevTools в течение длительного времени.

Что такое CSS-в-JS?

Определение CSS-in-JS довольно расплывчато. В широком смысле это подход к управлению CSS-кодом с помощью JavaScript. Например, это может означать, что содержимое CSS определяется с помощью JavaScript, а окончательный вывод CSS генерируется приложением «на лету».

В контексте DevTools CSS-in-JS означает, что содержимое CSS вводится на страницу с помощью CSSOM API . Обычный CSS внедряется с использованием элементов <style> или <link> и имеет статический источник (например, узел DOM или сетевой ресурс). Напротив, CSS-in-JS часто не имеет статического источника. Особым случаем здесь является то, что содержимое элемента <style> может быть обновлено с помощью CSSOM API, что приведет к рассинхронизации источника с фактической таблицей стилей CSS.

Если вы используете какую-либо библиотеку CSS-in-JS (например, styled-comment , Emotion , JSS ), библиотека может внедрить стили с помощью API-интерфейсов CSSOM «под капотом» в зависимости от режима разработки и браузера.

Давайте рассмотрим несколько примеров того, как можно внедрить таблицу стилей с помощью API CSSOM, аналогично тому, как это делают библиотеки CSS-in-JS.

// Insert new rule to an existing CSS stylesheet
const element = document.querySelector('style');
const stylesheet = element.sheet;
stylesheet.replaceSync('.some { color: blue; }');
stylesheet.insertRule('.some { color: green; }');

Вы также можете создать совершенно новую таблицу стилей :

// Create a completely new stylesheet
const stylesheet = new CSSStyleSheet();
stylesheet.replaceSync('.some { color: blue; }');
stylesheet.insertRule('.some { color: green; }');

// Apply constructed stylesheet to the document
document.adoptedStyleSheets = [...document.adoptedStyleSheets, sheet];

Поддержка CSS в DevTools

В DevTools наиболее часто используемой функцией при работе с CSS является панель «Стили» . На панели «Стили» вы можете просмотреть, какие правила применяются к определенному элементу, а также редактировать правила и видеть изменения на странице в режиме реального времени.

До прошлого года поддержка правил CSS, измененных с помощью API CSSOM, была довольно ограниченной: вы могли только видеть примененные правила, но не могли их редактировать. Основная цель, которую мы преследовали в прошлом году, заключалась в том, чтобы разрешить редактирование правил CSS-in-JS с помощью панели «Стили». Иногда мы также называем стили CSS-in-JS «созданными» , чтобы указать, что они были созданы с использованием веб-API.

Давайте углубимся в детали редактирования стилей в DevTools.

Механизм редактирования стилей в DevTools

Механизм редактирования стилей в DevTools

Когда вы выбираете элемент в DevTools, отображается панель «Стили» . Панель «Стили» выдает команду CDP под названием CSS.getMatchedStylesForNode , чтобы получить правила CSS, применимые к элементу. CDP означает протокол Chrome DevTools и представляет собой API, который позволяет интерфейсу DevTools получать дополнительную информацию о проверяемой странице.

При вызове CSS.getMatchedStylesForNode идентифицирует все таблицы стилей в документе и анализирует их с помощью анализатора CSS браузера. Затем он создает индекс, который связывает каждое правило CSS с позицией в исходной таблице стилей.

Вы можете спросить, зачем нужно снова анализировать CSS? Проблема здесь в том, что из соображений производительности сам браузер не заботится об исходных позициях правил CSS и, следовательно, не сохраняет их. Но DevTools нужны исходные позиции для поддержки редактирования CSS. Мы не хотим, чтобы обычные пользователи Chrome платили за производительность, но мы хотим, чтобы пользователи DevTools имели доступ к исходным позициям. Этот подход повторного анализа подходит для обоих вариантов использования с минимальными недостатками.

Затем реализация CSS.getMatchedStylesForNode запрашивает механизм стилей браузера предоставить правила CSS, соответствующие данному элементу. И, наконец, метод связывает правила, возвращаемые обработчиком стилей, с исходным кодом и предоставляет структурированный ответ о правилах CSS, чтобы DevTools знал, какая часть правила является селектором или свойствами. Это позволяет DevTools редактировать селектор и свойства независимо.

Теперь давайте посмотрим на редактирование. Помните, что CSS.getMatchedStylesForNode возвращает исходные позиции для каждого правила? Это очень важно для редактирования. Когда вы меняете правило, DevTools выдает другую команду CDP, которая фактически обновляет страницу. Команда включает исходное положение фрагмента обновляемого правила и новый текст, которым необходимо обновить фрагмент.

На внутренней стороне при обработке вызова редактирования DevTools обновляет целевую таблицу стилей. Он также обновляет копию источника таблицы стилей, которую он поддерживает, и обновляет исходные позиции для обновленного правила. В ответ на вызов редактирования интерфейс DevTools возвращает обновленные позиции только что обновленного текстового фрагмента.

Это объясняет, почему редактирование CSS-in-JS в DevTools не сработало «из коробки»: CSS-in-JS нигде не хранится, а правила CSS живут в памяти браузера в структурах данных CSSOM .

Как мы добавили поддержку CSS-in-JS

Итак, для поддержки редактирования правил CSS-in-JS мы решили, что лучшим решением будет создание источника построенных таблиц стилей, которые можно будет редактировать с помощью существующего механизма, описанного выше.

Первым шагом является построение исходного текста. Механизм стилей браузера хранит правила CSS в классе CSSStyleSheet . Это тот класс, экземпляры которого вы можете создать из JavaScript, как обсуждалось ранее. Код для создания исходного текста выглядит следующим образом:

String InspectorStyleSheet::CollectStyleSheetRules() {
  StringBuilder builder;
  for (unsigned i = 0; i < page_style_sheet_->length(); i++) {
    builder.Append(page_style_sheet_->item(i)->cssText());
    builder.Append('\n');
  }
  return builder.ToString();
}

Он перебирает правила, найденные в экземпляре CSSStyleSheet, и создает из них одну строку. Этот метод вызывается при создании экземпляра класса InspectorStyleSheet. Класс InspectorStyleSheet оборачивает экземпляр CSSStyleSheet и извлекает дополнительные метаданные, необходимые DevTools:

void InspectorStyleSheet::UpdateText() {
  String text;
  bool success = InspectorStyleSheetText(&text);
  if (!success)
    success = InlineStyleSheetText(&text);
  if (!success)
    success = ResourceStyleSheetText(&text);
  if (!success)
    success = CSSOMStyleSheetText(&text);
  if (success)
    InnerSetText(text, false);
}

В этом фрагменте мы видим CSSOMStyleSheetText , который внутренне вызывает CollectStyleSheetRules . CSSOMStyleSheetText вызывается, если таблица стилей не является встроенной или не является таблицей стилей ресурса. По сути, эти два фрагмента уже позволяют выполнять базовое редактирование таблиц стилей, созданных с помощью new CSSStyleSheet() .

Особым случаем являются таблицы стилей, связанные с тегом <style> , которые были изменены с помощью CSSOM API. В этом случае таблица стилей содержит исходный текст и дополнительные правила, которых нет в исходнике. Чтобы справиться с этим случаем, мы вводим метод объединения этих дополнительных правил с исходным текстом. Здесь порядок имеет значение, поскольку правила CSS можно вставлять в середину исходного текста. Например, представьте, что исходный элемент <style> содержит следующий текст:

/* comment */
.rule1 {}
.rule3 {}

Затем страница вставила несколько новых правил с помощью JS API, создав следующий порядок правил: .rule0, .rule1, .rule2, .rule3, .rule4. Результирующий исходный текст после операции слияния должен быть следующим:

.rule0 {}
/* comment */
.rule1 {}
.rule2 {}
.rule3 {}
.rule4 {}

Сохранение исходных комментариев и отступов важно для процесса редактирования, поскольку положение правил в исходном тексте должно быть точным.

Еще одна особенность таблиц стилей CSS-in-JS заключается в том, что они могут быть изменены на странице в любое время . Если фактические правила CSSOM не будут синхронизированы с текстовой версией, редактирование не будет работать. Для этого мы ввели так называемый зонд , который позволяет браузеру уведомлять серверную часть DevTools при изменении таблицы стилей. Измененные таблицы стилей затем синхронизируются во время следующего вызова CSS.getMatchedStylesForNode.

Со всеми этими элементами редактирование CSS-in-JS уже работает, но мы хотели улучшить пользовательский интерфейс, чтобы он указывал, была ли создана таблица стилей. Мы добавили новый атрибут isConstructed в CSS.CSSStyleSheetHeader CDP, который интерфейс использует для правильного отображения источника правила CSS:

Конструируемая таблица стилей

Выводы

Подводя итог нашей истории, мы рассмотрели соответствующие варианты использования, связанные с CSS-in-JS, которые DevTools не поддерживали, и рассмотрели решение для поддержки этих вариантов использования. Интересной частью этой реализации является то, что мы смогли использовать существующую функциональность, сделав правила CSSOM CSS обычным исходным текстом, избежав необходимости полностью перепроектировать редактирование стиля в DevTools.

Для получения дополнительной информации ознакомьтесь с нашим предложением по дизайну или с ошибкой отслеживания Chromium, которая ссылается на все соответствующие исправления.

Загрузите предварительный просмотр каналов

Рассмотрите возможность использования Chrome Canary , Dev или Beta в качестве браузера для разработки по умолчанию. Эти каналы предварительного просмотра дают вам доступ к новейшим функциям DevTools, тестируют передовые API-интерфейсы веб-платформы и находят проблемы на вашем сайте раньше, чем это сделают ваши пользователи!

Связь с командой Chrome DevTools

Используйте следующие параметры, чтобы обсудить новые функции и изменения в публикации или что-либо еще, связанное с DevTools.