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 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 データ構造内のブラウザのメモリ内に存在します

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 の新機能、更新、その他のトピックについて話し合います。