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. Szczególnym przypadkiem jest to, że zawartość elementu <style> można aktualizować przy użyciu interfejsu CSSOM API, co powoduje brak synchronizacji źródła 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 Narzędziach deweloperskich najczęściej wykorzystywaną funkcją w przypadku obsługi stylów 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 „skonstruowanymi”, aby wskazać, że zostały one utworzone za pomocą interfejsów API dla sieci.

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

Mechanizm edycji stylu w Narzędziach deweloperskich

Mechanizm edycji stylu 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 front-endowi DevTools 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. Aby umożliwić edycję CSS, Narzędzia deweloperskie potrzebują pozycji źródeł. 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. Ta analiza obejmuje oba przypadki 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 faktycznie zaktualizuje stronę. Polecenie zawiera pierwotną pozycję fragmentu reguły, który jest aktualizowany, oraz nowy tekst, którym fragment musi zostać zastąpiony.

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 gdzieś zapisane, 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 skonstruowanych 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 pakuje instancję CSSStyleSheet i wyodrębnia dodatkowe metadane wymagane przez Narzędzia deweloperskie:

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().

Szczególnym przypadkiem są arkusze stylów powiązane z tagiem <style>, które zostały zmienione przy użyciu interfejsu CSSOM API. 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ą zsynchronizowane z wersją tekstową, edycja nie zadziała. 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.

Mając wszystkie te elementy, już działa edycja CSS-in-JS, ale chcieliśmy ulepszyć interfejs użytkownika, by wskazać, czy utworzono arkusz stylów. 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

Aby je podsumować, przeanalizowaliśmy przypadki użycia kodu CSS-in-JS, których Narzędzia deweloperskie nie były obsługiwane, i omówiliśmy odpowiednie rozwiązanie, żeby je wykorzystać. Co ciekawe, udało nam się wykorzystać istniejące funkcje, dzięki czemu reguły CSSOM CSSOM miały standardowy tekst źródłowy, co eliminowało konieczność zmiany architektury w Narzędziach deweloperskich.

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 przeglądarki Chrome Canary, Dev lub Beta jako domyślnej przeglądarki deweloperskiej. 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.