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 には静的なソースがないことがよくあります。ここでの特殊なケースは、CSSOM API を使用して <style> 要素のコンテンツを更新する場合です。これにより、ソースと実際の 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 を扱うときに最もよく使用される機能は、[Styles] ペインです。[スタイル] ペインでは、特定の要素に適用されているルールを確認できます。ルールを編集することで、ページの変更をリアルタイムで確認できます。

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

では、DevTools でのスタイル編集の仕組みを詳しく見ていきましょう。

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

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

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

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

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

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

次に編集について見ていきましょう。CSS.getMatchedStylesForNode はすべてのルールのソース位置を返すことを思い出してください。これは編集に不可欠です。ルールを変更すると、DevTools は別の CDP コマンドを発行し、実際にページを更新します。このコマンドには、更新するルールのフラグメントの元の位置と、フラグメントを更新する必要がある新しいテキストが含まれます。

バックエンドでは、edit 呼び出しを処理する際に、対象のスタイルシートを更新します。また、保持しているスタイルシートのソースのコピーを更新し、更新したルールのソースの位置を更新します。編集の呼び出しに応じて、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 インスタンス内のルールを反復処理して、そこから 1 つの文字列を作成します。このメソッドは、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.getMatchesStylesForNode の呼び出し時に同期されます。

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

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

まとめ

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

詳しくは、Google の設計案または Chromium のトラッキング バグをご覧ください(関連するすべてのパッチが掲載されています)。

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

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

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

投稿内の新機能や変更点、または DevTools に関するその他の事項について議論するには、以下のオプションを使用します。

  • crbug.com からご提案やフィードバックをお寄せください。
  • DevTools の [その他のオプション] その他   > [ヘルプ] > [DevTools の問題を報告する] を使用して、DevTools の問題を報告する。
  • @ChromeDevTools でツイートします。
  • DevTools の新機能の YouTube 動画または DevTools のヒントの YouTube 動画で、コメントを記入してください。