CSS Modules の問題点をミックスインで解決する

はじめまして、よこけんです。(２回目)
今日は、CSS Modules の問題点を解決する方法について検討したのでそれを共有してみます。

# CSS Modules の問題点
CSS Modules ではクラスセレクタによるスタイル適用が基本です。
そしてクラス名は一つのコンポーネントに複数指定することができます。
しかし、複数指定した場合の適用順序は保証されません。クラス名の指定順序ではなく、クラスセレクタが読み込まれた順序に依存します。

例えば下記の場合、背景色は赤色ではなく青色になります。

```sass:a.styl
.a
    background-color: red
```

```sass:b.styl
.b
    background-color: blue
```

```tsx:Hoge.tsx
import * as aStyles from "./a.styl";
import * as bStyles from "./b.styl";
const Hoge = () => <div className={`${bStyles.b} ${aStyles.a}`}>Hoge</div>;
```

サンプルコードでは `Hoge.tsx` がインポート順序を `b.styl` -> `a.styl` に変えれば意図した結果を得ることができますが、例えば `Hoge.tsx` から参照される別のコンポーネントで `b.styl` をインポートしている場合にはやはり `.b` が優先されて青色になってしまいます。
この問題は非常に厄介な上に、直接的な解決方法というものはありません。

# ミックスインによる解決

本記事では、この問題に対する比較的扱いやすい解決方法として、Stylus や Sass の**ミックスイン機能**を利用した解決方法を紹介します。
ミックスインはクラス継承と違い、適用位置にプロパティを全てコピーします。全てのプロパティが集約されることにより、単純にミックスインの指定順序に従ってプロパティが適用されます。ミックスイン本体の読み込み順序は関係ありません。

次のサンプルコードでは、`mixHoge()` が `mixB()` の後に `mixA()` を呼んでいるため、背景色は必ず赤色になります。

```sass:a.styl
mixA()
    background-color: red

.a
    mixA()
```

```sass:b.styl
mixB()
    background-color: blue

.b
    mixB()
```

```sass:Hoge.styl
@import "a.styl"
@import "b.styl"

mixHoge()
    mixB()
    mixA()

.hoge
    mixHoge()
```

```tsx:Hoge.tsx
import * as styles from "./Hoge.styl";
const Hoge = () => <div className={styles.hoge}>Hoge</div>;
```

# シンプルなルール

前述のコードをルール化すると次のようになります。

* **一つのコンポーネントに複数のクラスを指定してはいけない**
* **クラスセレクタを用意する場合、対になる単一のミックスインを必ず用意する**
* **クラスセレクタでは常に、対になる単一のミックスインの適用のみを行い、スタイル記述はミックスイン内で行う**
* **クラス継承 (`@extends`) は一律禁止とし、代わりにミックスインを使用する**

クラス継承 (`@extends`) は一律禁止ということに気を付けてください。
単一継承であれば大丈夫のように思うかもしれませんが、クラス継承を使ってしまうと、そこから先をミックスインで派生させても全てのプロパティが一箇所に集約されなくなってしまい、読み込み順序に再び依存するようになってしまいます。

この方式の欠点は、トランスパイルされた CSS ファイルのサイズが肥大化するリスクです。
前述の通り、ミックスインはクラス継承と違い、適用位置にプロパティを全てコピーします。だから読み込み順序に一切依存しなくなるわけですが、これはトランスパイル後の CSS ファイルのサイズに影響します。
スタイルの継承を多用するようなプロジェクトではリスクが顕在化するかもしれません。
リスクが顕在化してきた場合には、プロパティ数が多く継承も多く行われる特定のスタイルに対してのみクラス継承 (`@extends`) を許可し、それらのクラスセレクタだけは読み込み順序を慎重に管理します。


# 追加ルールでリスクを軽減
先ほどはクラス継承を使用すると問題に繋がるとしていましたが、厳密には、**派生を許可しないクラス**からであればクラス継承を使用しても問題には繋がりません。
合法的にクラス継承を使用できるケースが発生すると、ファイルサイズを抑える効果が期待できます。

次のサンプルコードは、`.hoge` クラスの派生を禁止することで安全を確保できます。

```sass:a.styl
mixA()
    background-color: red

.a
    mixA()
```

```sass:b.styl
mixB()
    background-color: blue

.b
    mixB()
```

```sass:Hoge.styl
@import "a.styl"
@import "b.styl"

.hoge
    @extends .b
    mixA()
```

```tsx:Hoge.tsx
import * as styles from "./Hoge.styl";
const Hoge = () => <div className={styles.hoge}>Hoge</div>;
```


これをルール化すると次のようになります。

* 一つのコンポーネントに複数のクラスを指定してはいけない
* **派生を許可する**クラスセレクタを用意する場合、対になる単一のミックスインを必ず用意する
* **派生を許可する**クラスセレクタでは常に対になる単一のミックスインの適用のみを行い、スタイル記述はミックスイン内で行う
* **ミックスイン内での**クラス継承 (`@extends`) は一律禁止とし、代わりにミックスインを使用する
* **派生を許可しないクラスセレクタではスタイルを直接記述して良い**
* **派生を許可しないクラスセレクタではクラスを一つだけ継承 (`@extends`) して良い (２つ以上のクラスを継承したい場合はミックスインを併用する)**

この方式の欠点は二つあります。

一つはルールが少し複雑になるために混乱を招いたりルール違反が発生しやすくなることです。
ただし、派生を許可するクラスセレクタがあまり多くない (整理されていて見通しが良い) プロジェクトなら、追加ルールを適用しても混乱やルール違反は最低限に抑えられると思います。

もう一つは、リスクの低減はできても完全に回避することはできないということです。
リスクが顕在化してきたら最初の解決方法と同様、特定のスタイルに対してクラス継承 (`@extends`) を許可し、それらのクラスセレクタだけは読み込み順序を慎重に管理します。
しかし、元々少し複雑なルールにこの例外措置が加わることになりますので、混乱やルール違反をより招きやすくなる恐れがあります。


# 結論

* リスクが顕在化する可能性が低そうであればシンプルなルールを採用する
* 追加ルールを採用する場合、混乱やルール違反を招かないよう工夫する

なお、そもそも複数クラスの継承をしようとしなければ問題は起きません。ただし、そのためのルールは結局必要になります。 (そして恐らく、そのルールによって新たなリスクも発生します。)

根本的には、クラスセレクタの読み込み順序ではなくクラス指定順序で結果が決まってくれれば良いんですが、CSS の仕様のようなので。