CSS 変数のスコープについての問題、そしてその問題に対処するツールの提案
こんにちは。
今回は CSS 変数のスコープについての問題と対処法について語ろうと思います。
CSS 変数のスコープ問題とは
まず、「CSS 変数のスコープについての問題」の解説ですが、現在の CSS Custom Properties には以下のような問題があると私は考えています。
・特定の変数がどのスコープで有効なのかが軽く読むだけでは分かりづらい
例えば、以下のような単純なコード例で考えてみましょう。
.scope-1 {
--text-color: red;
}
.scope-2 {
/* 変数 `--text-color` はどこから来ている?本当にここで使える? */
color: var(--text-color);
}
コメントにあるように、「この変数はどこで使えるのか」を CSS ファイルだけを読んで把握するのは非常に難しいです。
HTML の構造に左右されるのはもちろん、JS や JSX で動的に書き換えられたりすることが多々あるから、なのは当然として、そもそも CSS の構文自体が変数のスコープを把握しづらい構造となっているのも1つの理由だと私は考えています。
CSS Nesting が登場したことで以前よりはかなりわかりやすく書けるようにはなりましたが、それでも「CSS ファイルや CSS ルールブロックを細かく分割して書くのが当たり前」でありながら「どの変数がどこで定義されているのか、どこで継承されているのかが暗黙的」な状態なので、変数の数が増えると全てを把握するのが難しくなっていきます。
対処法は?
とはいえ、このような問題は何も CSS 特有の問題という訳ではありません。
「現在のブロックの外で定義されている変数を読み込んで使用する」なんて他のプログラム言語では当たり前のように行われています。
では、CSS とは何が違うのか?なぜ CSS のように問題にならないのか?
そう、それは明示的に必要な変数を import / 引数に指定 しているからです。
提案する解決方法: 使用する変数スコープをコメントで明示する
先ほどのコード例をもう一度見てみましょう。
.scope-1 {
--text-color: red;
}
.scope-2 {
/* 変数 `--text-color` はどこから来ている?本当にここで使える? */
color: var(--text-color);
}
このコード例の .scope-2 ブロックの中に、以下のような特殊なコメントを追加してみましょう。
/* @var-scope .scope-1 */
.scope-1 {
--text-color: red;
}
.scope-2 {
/* @var-scope .scope-1 */
color: var(--text-color);
}
これで変数 --text-color がどこから来ているものなのか、とてもわかりやすくなりましたね。
もちろんこれはただのコメントなので、以前のように .scope-2 ブロック内で変数 --text-color を使えるように HTML / JS / JSX で設定しておかなければいけないという制約は依然として残っています。
それでも、CSS ファイル内に書き残すことができる最大限の情報・意図を書いておくことで、人間が後で読み返した時に把握できるまでの時間や労力の軽減、そして機械的に静的解析ツールで読み込めるようになるという大きなメリットがあると、私は考えています。
ここからは、この問題を解決する為に提案する特殊コメントの書式の説明と、これを静的解析と共に使用することでどのようなメリットがあるのかを説明していきます。
提案する特殊コメントの種類
@var-scope
まず、先ほどの例で出てきたもの、@var-scope [セレクタ] は「同一ファイル内に書かれている変数スコープを継承するのか」を示すように使用できます。
とはいえ、CSS ファイルをコンポーネント単位などで分割していると外部のファイルで定義されている変数を使用することになるのがほとんどでしょう。その場合は、@var-scope [セレクタ] from "./ファイルパス" という風に「どのファイルにある変数スコープから継承するのか」を示すことができます。
場合によっては、:root ブロック内の変数さえ読み込めれば良いというケースも多いでしょう。その場合、@var-scope "./ファイルパス" と書くことで「どのファイルから :root の変数スコープを継承するのか」を示すことができます。
以下がまとめです。
/* 同一ファイル内 */
/* @var-scope .block */
/* 外部ファイルの特定スコープから継承する場合 */
/* @var-scope .block from "./style.css" */
/* 外部ファイルの `:root` スコープから継承する場合 */
/* @var-scope "./style.css" */
@var-use
実際の開発では、全ての CSS 変数が予め CSS ファイル内で定義されているとは限りません。
HTML / JS / JSX などで CSS の外部から挿入することができます。
その場合、当然ながら CSS ファイルからは何も読み取ることができません。これでは先ほど提起した問題と全く同じ事が起きてしまいます。
それに対処する為に、@var-use コメントを導入します。
:root {
--color-1: red;
}
.card {
/* この --color-2 はどこから? --color-1 の書き間違い? */
color: var(--color-2);
}
.button {
/* 定義先を明記しなくとも、変数の存在を開発者が保証する */
/* @var-use --color-3 */
color: var(--color-3);
}
もちろん、このコメントも実際にはなんの効力もありません。
あくまでも、「開発者としてこの変数が存在することを保証する」ということを示す以外の意味はありません。
それでも、このコード例のように「変数の定義忘れや変数名の書き間違えなどではない」と示すことができます。
静的解析について
さて、ここからは「これらの特殊コメントを静的解析と組み合わせて使うことでどのようなメリットが得られるのか」を説明します。
1. 未定義変数の検知
まずはこれですね。
変数スコープを明示しておくことで、どの変数が未定義のまま使われているかを検知しやすくなり、typo やリファクタリング後の修正忘れに気付きやすくなります。
2. 現在のスコープ内で使用可能な変数の列挙
これまではどの変数がどこで使えるのか、を完全に把握するのは難しいことでしたが、変数スコープを明示することでスコープ外の変数を検知した時に警告を出したり、入力補完機能の恩恵を受けることが可能になります。
3. 複雑な CSS の継承関係を意識せず済む
静的解析を行えば、セレクタから既存の変数スコープを自動で読み取ることも可能になります。
例えば、a > span というセレクタは、a セレクタの変数スコープを継承している、というようなものです。
このような単純なセレクタであれば理解も簡単ですが、セレクタが複雑になってくると理解も難しくなってきます。
静的解析を行えば、これを機械的に解決することが可能になります。
4. 依存する外部ファイル・セレクタが存在しない時に警告を表示
リファクタリングなどでファイルを編集した後、現状では全ての影響範囲を把握するのが難しいです。
静的解析を行うことで、変数スコープの依存関係が壊れた時に検知することができます。
5. 未使用変数の検知
変数が多くなってくると、変数使用の追跡が難しくなってきます。
この問題も静的解析で解決できます。
実装について: どうやって?
実装についてですが、このコード上の問題を検知する方法として Stylelint や eslint/css を利用する方法が使えると思います。
また、リンタツールだけではなく、エディタの拡張機能としても実装すると DX が向上するでしょう。
現在、eslint/css 用カスタムルールとしての実装・開発に取り組んでいます。
実装時の課題
実装には1つの大きな課題があります。それは、「セレクタ間の継承関係を完璧に静的解析のみで把握することが難しい」ということです。
例えば、以下のようなコードを考えてみましょう。
main .card {
--text: red;
}
body > main > div > .card > .link {
/* !!! */
color: var(--text);
}
main .card スコープ内で定義された変数を body > main > div > .card > .link で使用しています。
これが valid であるか invalid であるかと聞かれれば、valid であるべきですが、body > main > div > .card > .link というセレクタは見ての通り main .card セレクタと大きく変わっています。
セレクタの最初・最後も違いますし、正規表現で見つけるのも難しいです。.some-class と [class="some-class"] は同じ要素を指している、ということも正規表現では読み取れません。
しかし、変数の継承の流れをすべて手動で書いていくことになるのは利便性の点でよろしくないので、やはりこの辺りの「セレクタ A はセレクタ B にもマッチするか」を検証するロジック・ツールも開発する必要があります。
提案は以上です。
今後、実装を進めていく中で、仕様の見直しや拡張などが行われる可能性はありますが、基本的な考え方は本記事に記した通りです。
この提案について、ご意見・ご感想などがございましたら、ぜひお聞かせいただけると嬉しいです。
最後までお読みいただき、ありがとうございました。
おしまい。
Discussion