Obsługa CSS-in-JS w Narzędziach deweloperskich

Alex Rudenko
Alex Rudenko

W tym artykule omawiamy obsługę CSS-in-JS w DevTools, która pojawiła się w Chrome 85. Ogólnie rzecz biorąc, wyjaśniamy, co rozumiemy przez CSS-in-JS i czym różni się on od zwykłego CSS, który jest obsługiwany przez DevTools od dawna.

Co to jest CSS-in-JS?

Definicja CSS-in-JS jest raczej niejasna. W szerszym znaczeniu jest to podejście do zarządzania kodem CSS za pomocą JavaScriptu. Może to na przykład oznaczać, że zawartość CSS jest definiowana za pomocą JavaScriptu, a końcowy kod CSS jest generowany na bieżąco przez aplikację.

W kontekście DevTools CSS-in-JS oznacza, że zawartość CSS jest wstrzykiwana na stronę za pomocą interfejsów API CSSOM. Zwykły kod CSS jest wstrzykiwany za pomocą elementów <style> lub <link> i ma statyczny źródło (np. węzeł DOM lub zasób sieciowy). Z kolei CSS-in-JS często nie ma źródła statycznego. Wyjątkiem jest tutaj możliwość aktualizowania zawartości elementu <style> za pomocą interfejsu API CSSOM, co powoduje, że źródło staje się niezsynchronizowane z rzeczywistym arkuszem stylów CSS.

Jeśli używasz biblioteki CSS-in-JS (np. styled-component, Emotion, JSS), może ona wstrzykiwać style za pomocą interfejsów API CSSOM, zależnie od trybu programowania i przeglądarki.

Przyjrzyjmy się kilku przykładom wstrzykiwania arkusza stylów za pomocą interfejsu API CSSOM, które są podobne do tego, co robią biblioteki 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; }');

Możesz też utworzyć zupełnie nową arkusz stylów:

// 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];

Obsługa CSS w Narzędziach deweloperskich

W DevTools najczęściej używaną funkcją podczas pracy z CSS jest panel Style. W panelu Style możesz sprawdzić, które reguły mają zastosowanie do danego elementu, edytować reguły i obserwować zmiany na stronie w czasie rzeczywistym.

Przed zeszłorocznym rokiem obsługa reguł CSS modyfikowanych za pomocą interfejsów API CSSOM była dość ograniczona: można było tylko wyświetlać zastosowane reguły, ale nie można ich było edytować. Głównym celem w zeszłym roku było umożliwienie edytowania reguł CSS-in-JS za pomocą panelu Style. Czasami nazywamy style CSS-in-JS „utworzonymi”, aby wskazać, że zostały utworzone za pomocą interfejsów API dla stron internetowych.

Przyjrzyjmy się bliżej edytowaniu stylów w Narzędziach deweloperskich.

Mechanizm edycji stylów w Narzędziach deweloperskich

Mechanizm edycji stylów w Narzędziach deweloperskich

Po wybraniu elementu w narzędziach dla deweloperów wyświetla się panel Style. Panel Style wysyła do CDP polecenie CSS.getMatchedStylesForNode, aby uzyskać reguły CSS, które mają zastosowanie do elementu. CDP to skrót od Chrome DevTools Protocol. Jest to interfejs API, który umożliwia interfejsowi NDK uzyskiwanie dodatkowych informacji o przeglądanej stronie.

Po wywołaniu CSS.getMatchedStylesForNode identyfikuje wszystkie arkusze stylów w dokumencie i analizuje je za pomocą parsowania CSS przeglądarki. Następnie tworzy indeks, który łączy każdą regułę CSS z pozycją w źródle arkusza stylów.

Możesz się zastanawiać, dlaczego musi ponownie przeanalizować plik CSS. Problem polega na tym, że ze względu na wydajność przeglądarka nie interesuje się źródłowymi pozycjami reguł CSS i w związku z tym ich nie przechowuje. Jednak DevTools potrzebuje pozycji źródłowych, aby obsługiwać edycję CSS. Nie chcemy, aby zwykłe użytkownicy Chrome ponosili konsekwencje spadku wydajności, ale chcemy, aby użytkownicy Narzędzi deweloperskich mieli dostęp do pozycji źródłowych. To podejście do ponownego parsowania umożliwia obsługę obu przypadków użycia z minimalnymi wadami.

Następnie implementacja CSS.getMatchedStylesForNode prosi mechanizm stylów przeglądarki o przesłanie reguł CSS pasujących do danego elementu. Na koniec metoda łączy reguły zwracane przez silnik stylów z kodem źródłowym i zapewnia uporządkowaną odpowiedź na temat reguł CSS, aby DevTools wiedział, która część reguły jest selektorem lub właściwością. Umożliwia Narzędzi deweloperskim edytowanie selektora i właściwości niezależnie od siebie.

Teraz przyjrzyjmy się edytowaniu. Pamiętasz, że funkcja CSS.getMatchedStylesForNode zwraca pozycje źródeł dla każdej reguły? Jest to kluczowe dla edycji. Gdy zmienisz regułę, Narzędzia programistyczne wydadzą kolejne polecenie CDP, które zaktualizuje stronę. Polecenie zawiera pierwotną pozycję fragmentu reguły, który jest aktualizowany, oraz nowy tekst, którym należy go zastąpić.

Podczas obsługi wywołania edycji w backendzie Narzędzia deweloperskie aktualizuje docelowy arkusz stylów. Zaktualizuje też kopię źródła arkusza stylów, którą przechowuje, oraz pozycje źródła w zaktualizowanej regule. W odpowiedzi na wywołanie edycji interfejs DevTools otrzymuje zaktualizowane pozycje dla właśnie zaktualizowanego fragmentu tekstu.

To wyjaśnia, dlaczego edytowanie CSS-in-JS w DevTools nie działało od razu: CSS-in-JS nie ma żadnego rzeczywistego źródła, które byłoby przechowywane gdzieś w pliku, a reguły CSS są przechowywane w pamięci przeglądarki w strukturach danych CSSOM.

Jak dodaliśmy obsługę CSS-in-JS

Aby umożliwić edycję reguł CSS-in-JS, uznaliśmy, że najlepszym rozwiązaniem będzie utworzenie źródła dla skompilowanych arkuszy stylów, które można edytować za pomocą dotychczasowego mechanizmu opisanego powyżej.

Pierwszym krokiem jest utworzenie tekstu źródłowego. Silnik stylów przeglądarki przechowuje reguły CSS w klasie CSSStyleSheet. Ta klasa jest tą, której instancje możesz tworzyć z JavaScriptu, jak już wspomniano. Kod służący do tworzenia tekstu źródłowego:

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();
}

Przechodzi po regułach znalezionych w wystąpieniu CSSStyleSheet i tworzy z nich jeden ciąg znaków. Ta metoda jest wywoływana, gdy tworzona jest instancja klasy InspectorStyleSheet. Klasa InspectorStyleSheet otula instancję CSSStyleSheet i wyodrębnia dodatkowe metadane wymagane przez 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);
}

W tym fragmencie kodu widzimy funkcję CSSOMStyleSheetText, która wewnętrznie wywołuje funkcję CollectStyleSheetRules. Funkcja CSSOMStyleSheetText jest wywoływana, jeśli szata nie jest wbudowana ani nie jest szata zasobów. Te 2 fragmenty kodu umożliwiają już podstawową edycję arkuszy stylów utworzonych za pomocą konstruktora new CSSStyleSheet().

Wyjątkiem są arkusze stylów powiązane z tagiem <style>, które zostały zmutowane za pomocą interfejsu API CSSOM. W tym przypadku szata zawiera tekst źródłowy i dodatkowe reguły, których nie ma w źródle. Aby rozwiązać ten problem, wprowadzamy metodę łączenia tych dodatkowych reguł z tekstem źródłowym. W tym przypadku kolejność ma znaczenie, ponieważ reguły CSS można wstawiać w środku oryginalnego tekstu źródłowego. Załóżmy na przykład, że oryginalny element <style> zawierał ten tekst:

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

Następnie strona wstawiła nowe reguły za pomocą interfejsu JS API, tworząc następującą kolejność reguł: .rule0, .rule1, .rule2, .rule3, .rule4. Wynikowy tekst źródłowy po operacji scalania powinien wyglądać tak:

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

Zachowanie oryginalnych komentarzy i wcięć jest ważne dla procesu edycji, ponieważ pozycje tekstów źródłowych reguł muszą być precyzyjne.

Kolejną cechą stylów CSS-in-JS jest to, że są one w każdej chwili dostępne dla strony. Jeśli rzeczywiste reguły CSSOM nie będą zgodne z wersją tekstową, edytowanie nie będzie możliwe. W tym celu wprowadziliśmy tzw. próbkę, która umożliwia przeglądarce powiadamianie backendu DevTools o zmianach w arkuszu stylów. Zmutowane arkusze stylów są następnie synchronizowane podczas następnego wywołania metody CSS.getMatchedStylesForNode.

Dzięki tym wszystkim elementom edytowanie CSS-in-JS działa już poprawnie, ale chcieliśmy ulepszyć interfejs, aby wskazywał, czy została utworzona ściąga. Dodaliśmy nowy atrybut isConstructed do atrybutu CSS.CSSStyleSheetHeader w CDP, którego front-end używa do prawidłowego wyświetlania źródła reguły CSS:

Konstrukcyjny arkusz stylów

Podsumowanie

Podsumowując, omówiliśmy tutaj odpowiednie przypadki użycia związane z CSS-in-JS, których nie obsługuje DevTools, oraz przedstawiliśmy rozwiązanie umożliwiające obsługę tych przypadków. Ciekawym aspektem tej implementacji jest to, że udało nam się wykorzystać istniejące funkcje, dzięki czemu reguły CSSOM mają zwykły tekst źródłowy. Dzięki temu nie musieliśmy całkowicie przeprojektowywać edycji stylów w DevTools.

Więcej informacji znajdziesz w naszej propozycji projektu lub w artykule o błędzie Chromium, który zawiera odniesienia do wszystkich powiązanych poprawek.

Pobieranie kanałów podglądu

Rozważ użycie jako domyślnej przeglądarki deweloperskiej wersji Canary, Dev lub Beta przeglądarki Chrome. Te kanały wersji wstępnej zapewniają dostęp do najnowszych funkcji DevTools, umożliwiają testowanie najnowocześniejszych interfejsów API platformy internetowej i pomagają znaleźć problemy w witrynie, zanim zrobią to użytkownicy.

Kontakt z zespołem Narzędzi deweloperskich w Chrome

Aby omówić nowe funkcje, aktualizacje lub inne kwestie związane z Narzędziami deweloperskimi, skorzystaj z tych opcji.