DevTools での CSS-in-JS サポート

Alex Rudenko
Alex Rudenko

この記事では、Chrome 85 以降に追加された DevTools での CSS-in-JS のサポートと、一般的には CSS-in-JS の意味と、DevTools で長年サポートされてきた通常の CSS との違いについて説明します。

CSS-in-JS とは

CSS-in-JS の定義はあまりあいまいです。広義には、JavaScript を使用して CSS コードを管理するためのアプローチです。たとえば、CSS コンテンツが JavaScript を使用して定義され、最終的な CSS 出力がアプリによってその場で生成される場合などです。

DevTools の「CSS-in-JS」とは、CSSOM API を使用して CSS コンテンツがページに挿入されることを意味します。通常の CSS は <style> 要素または <link> 要素を使用して挿入され、静的なソース(DOM ノード、ネットワーク リソースなど)を持ちます。これに対して、CSS-in-JS には静的ソースがないことがよくあります。特殊なケースとして、<style> 要素のコンテンツが CSSOM API を使用して更新され、ソースが実際の CSS スタイルシートと同期しなくなる場合があります。

CSS-in-JS ライブラリ(styled-componentEmotionJSS など)を使用する場合、開発モードやブラウザによっては、ライブラリが内部で CSSOM API を使用してスタイルを挿入することがあります。

CSS-in-JS ライブラリと同様に、CSSOM API を使用してスタイルシートを挿入する方法の例をいくつか見てみましょう。

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

DevTools での CSS サポート

DevTools で CSS を扱うときに最もよく使用される機能は、[スタイル] ペインです。[スタイル] ペインでは、特定の要素に適用されるルールを確認できます。また、ルールを編集したり、ページの変更内容をリアルタイムで確認したりできます。

昨年までは、CSSOM API を使用して変更された CSS ルールのサポートはかなり限定されていました。適用されたルールを表示するだけで、編集することはできませんでした。昨年の主な目的は、[スタイル] ペインを使用して CSS-in-JS ルールを編集できるようにすることでした。ウェブ API を使用して構築されたことを示すために、CSS-in-JS スタイルを「コンストラクト」と呼ぶこともあります。

DevTools でのスタイル編集作業について詳しく見ていきましょう。

DevTools のスタイル編集メカニズム

DevTools のスタイル編集メカニズム

DevTools で要素を選択すると、[Styles] ペインが表示されます。[Styles] ペインは、CSS.getMatchedStylesForNode という CDP コマンドを発行して、要素に適用される CSS ルールを取得します。CDP は Chrome DevTools Protocol の略で、DevTools のフロントエンドが検査されたページに関する追加情報を取得できるようにする API です。

CSS.getMatchedStylesForNode が呼び出されると、ドキュメント内のすべてのスタイルシートが識別され、ブラウザの CSS パーサーを使用して解析されます。次に、すべての CSS ルールをスタイルシート ソース内の位置に関連付けるインデックスを作成します。

なぜ再度 CSS を解析する必要があるのかと疑問に思われるかもしれません。ここでの問題は、パフォーマンス上の理由から、ブラウザ自体が CSS ルールのソースの位置を考慮せず、そのため、ルールを保存しないことです。ただし、DevTools では、CSS の編集をサポートするためにソースの位置が必要です。通常の Chrome ユーザーがパフォーマンスを低下させるのは避けたいと考えていますが、DevTools ユーザーがソースの位置にアクセスできるようにする必要があります。この再解析アプローチにより、両方のユースケースに最小限のデメリットで対処できます。

次に、CSS.getMatchedStylesForNode の実装はブラウザのスタイル エンジンに対し、指定された要素に一致する CSS ルールを提供するよう求めます。最後に、このメソッドはスタイル エンジンから返されたルールをソースコードに関連付けて、CSS ルールに関する構造化されたレスポンスを提供します。これにより、DevTools はルールのどの部分がセレクタかプロパティかを認識します。これにより、DevTools でセレクタとプロパティを個別に編集できます。

次は編集についてですCSS.getMatchedStylesForNode はルールごとにソースの位置を返すことを思い出してください。これは編集にとって非常に重要です。ルールを変更すると、DevTools は実際にページを更新する別の CDP コマンドを発行します。このコマンドには、更新するルールのフラグメントの元の位置と、フラグメントの更新に使用する新しいテキストが含まれます。

バックエンドで編集呼び出しを処理するときに、DevTools がターゲット スタイルシートを更新します。また、保持しているスタイルシートのソースのコピーを更新し、更新されたルールのソースの位置を更新します。編集呼び出しに応答して、DevTools フロントエンドは、更新されたテキスト フラグメントの更新された位置を取得します。

これが、DevTools での CSS-in-JS の編集が、すぐには機能しなかった理由です。CSS-in-JS には実際のソースがどこにも保存されておらずCSS ルールは、CSSOM データ構造内のブラウザのメモリ内に存在します

JavaScript による CSS のサポートを追加した方法

そこで、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 インスタンスで見つかったルールを反復処理し、そこから 1 つの文字列を作成します。このメソッドは、InspectStyleSheet クラスのインスタンスが作成されると呼び出されます。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);
}

このスニペットでは、CollectStyleSheetRules を内部で呼び出す CSSOMStyleSheetText が示されています。CSSOMStyleSheetText は、スタイルシートがインラインでもリソース スタイルシートでもない場合に呼び出されます。基本的に、これら 2 つのスニペットでは、new CSSStyleSheet() コンストラクタを使用して作成されたスタイルシートの基本的な編集がすでに可能です。

特殊なケースとして、CSSOM API を使用して変更された <style> タグに関連付けられたスタイルシートがあります。この場合、スタイルシートにはソーステキストと、ソースには存在しない追加のルールが含まれます。このケースに対処するために、これらの追加のルールをソーステキストに統合するメソッドを導入します。この場合、元のソーステキストの途中に CSS ルールを挿入できるため、順序が重要です。たとえば、元の <style> 要素に次のテキストが含まれていたとします。

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

次に、JS API を使用して新しいルールが挿入され、ルールの順序は次のようになります。 .rule0、.rule1、.rule2、.rule3、.rule4マージ操作の後のソーステキストは次のようになります。

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

ルールのソーステキストの位置は正確にする必要があるため、編集プロセスにおいては元のコメントやインデントを保持しておくことが重要です。

CSS-in-JS スタイルシートのもう 1 つの特色は、ページによっていつでも変更できることです。実際の CSSOM ルールがテキスト バージョンと同期していない場合、編集は機能しません。そのために、いわゆる「プローブ」を導入しました。これにより、スタイルシートが変更されたときに、ブラウザが DevTools のバックエンド部分に通知できるようになりました。その後、変更されたスタイルシートは、次回の CSS.getMatchesStylesForNode の呼び出し時に同期されます。

以上の要素がすべて揃ったので、JS 内での CSS 編集は正常に機能しますが、スタイルシートが構築されたかどうかを示す UI を改善したいと考えました。CDP の CSS.CSSStyleSheetHeaderisConstructed という新しい属性を追加しました。この属性は、CSS ルールのソースを適切に表示するためにフロントエンドで使用されます。

構成可能なスタイルシート

まとめ

ここで話の内容をまとめるために、CSS-in-JS に関連するユースケースのうち、DevTools でサポートされていなかったユースケースと、それらのユースケースをサポートするソリューションについて説明しました。この実装の興味深い点は、CSSOM CSS ルールに通常のソーステキストを含めることで、既存の機能を活用できたことです。これにより、DevTools でスタイル編集を完全に再設計する必要がなくなりました。

詳しい背景情報については、Google の設計案、または関連するすべてのパッチが参照されている Chromium のトラッキング バグをご覧ください。

プレビュー チャンネルをダウンロードする

デフォルトの開発ブラウザとして Chrome の CanaryDev、または Beta を使用することを検討してください。これらのプレビュー チャンネルを使用すると、DevTools の最新機能にアクセスしたり、最先端のウェブ プラットフォーム API をテストしたり、ユーザーに先駆けてサイトの問題を検出したりできます。

Chrome DevTools チームへのお問い合わせ

以下のオプションを使用して、投稿の新機能や変更点、または DevTools に関連するその他のことについて話し合います。

  • ご提案やフィードバックは、crbug.com からお送りください。
  • DevTools の問題を報告するには、その他のオプションもっと見る) >ヘルプ >DevTools で DevTools の問題を報告します。
  • @ChromeDevTools でツイートしてください。
  • DevTools の新機能に関する YouTube 動画または DevTools のヒントの YouTube 動画にコメントを残してください。