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 ルールを編集できるようにすることでした。CSS-in-JS スタイルは、Web API を使用して作成されたことを示すために、「コンストラクト」とも呼ばれます。

DevTools でのスタイル編集の詳細を見てみましょう。

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

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

DevTools で要素を選択すると、[スタイル] ペインが表示されます。[スタイル] ペインは、CSS.getMatchedStylesForNode という CDP コマンドを実行して、要素に適用される CSS ルールを取得します。CDP は Chrome DevTools プロトコルの略で、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 データ構造に存在します

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

このスニペットでは、内部で 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.getMatchedStylesForNode の次回の呼び出し時に同期されます。

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

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

まとめ

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

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

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

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

Chrome DevTools チームに問い合わせる

次のオプションを使用して、DevTools の新機能、アップデート、その他のトピックについて話し合います。