【Vue.js】Scoped CSSよりCSS Modulesの方がベターだった件

コンポーネント内で閉じた装飾の手法として、 Scoped CSS([vue-loader](https://vue-loader.vuejs.org/ja/) の機能)や CSS Modules, CSS in JS などが流行っています[^1]。

Vue.js で [Single File Components](https://jp.vuejs.org/v2/guide/single-file-components.html) を利用する場合、 Scoped CSS は手軽に利用できますが CSS Modules についても手軽に利用ができることがわかったので、比較をしてみました。

# Scoped CSS, CSS Modules の利用方法
実際に手を動かして検証されたい方は、以前に書いた記事[^2]を参照してプロジェクトを作成してください。

## Scoped CSS
`*.vue` ファイルにおいて `<style scoped>` のように `scoped` を入れ、装飾したい対象に `class` を指定します:

```vue:src/components/SmallComponent.vue
<template>
  <div>
    <h4 class="title">SmallComponent!</h4>
  </div>
</template>

<script>
import Vue from 'vue';

export default Vue.extend({
  name: 'SmallComponent',
});
</script>

<style scoped>
.title {
    border: dashed 2px #5b8bd0;
    border-radius: 5px;
    padding: 3px;
}
</style>
```

## CSS Modules
CSS Modules でも Scoped CSS 同様追加パッケージは不要です。

`*.vue` ファイルにおいて `<style module>` のように `module` を入れ、装飾したい対象に `:class="$style.class_name"` のようにしてclass名を指定します:

```vue:src/components/SmallComponent.vue
<template>
  <div>
    <h4 :class="$style.title">SmallComponent!</h4>
  </div>
</template>

<script>
import Vue from 'vue';

export default Vue.extend({
  name: 'SmallComponent',
});
</script>

<style module>
.title {
    border: dashed 2px #5b8bd0;
    border-radius: 5px;
    padding: 3px;
}
</style>
```

ここで `:class` は `v-bind:class` の省略記法であることに気をつけてください[^3]。
そのため、複数のclassを指定したい場合には `:class="[$style.class_name_1, $style.class_name_2]"` のように指定し、class名には `-` を用いない方が良いです[^4]。

# Scoped CSS, CSS Modules の仕組み
## Scoped CSS
Scoped CSS の詳細な仕組みについては、以前に書いた記事[^2]などを参照してください。

ブラウザのインスペクタで `SmallComponent` に対応する要素を確認してみます:

```html
<div data-v-509622e6>
    <h4 data-v-509622e6 class="title">SmallComponent!</h4>
</div>
```

`data-v-509622e6` のようなカスタムデータ属性が `div` タグと `h4` タグに付与されます。

また、インスペクタで確認すると `<head>` タグの中に以下のようなCSSが埋め込まれています:

```css
.title[data-v-509622e6] {
    border: dashed 2px #5b8bd0;
    border-radius: 5px;
    padding: 3px;
}
```

このように、 Scoped CSS はカスタムデータ属性 `data-v-xxxxxxxx` を追加で付与し、**元のclass名は変更しない**ような仕組みとなっています。

## CSS Modules
[vue-loader の CSS モジュールのページ](https://vue-loader-v14.vuejs.org/ja/features/css-modules.html)に書いてある通り、 `<style module>` を用いることで `$style` という名前の算出プロパティが自動的に注入されます。

以下のようにして `console.log` で `$style` の中身を表示してみます:

```vue:src/components/SmallComponent.vue
<template>
  <div>
    <h4 :class="$style.title">SmallComponent!</h4>
  </div>
</template>

<script>
import Vue from 'vue';

export default Vue.extend({
  name: 'SmallComponent',
  created() {
    console.log(this.$style);
  },
});
</script>

<style module>
.title {
    border: dashed 2px #5b8bd0;
    border-radius: 5px;
    padding: 3px;
}
</style>
```

ブラウザのインスペクタのJavaScriptコンソールで確認すると、以下のオブジェクトが表示されます:
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/137045/3cd6a747-075e-5830-dbb2-6b12d9a24c2f.png)

`$style` は `元のclass名` を `コンポーネント名_元のclass名_ランダムな記号`[^5] に対応させるオブジェクト(辞書)であることがわかります。

インスペクタで `SmallComponent` に対応する要素を確認してみます:

```html
<div>
    <h4 class="SmallComponent_title_1UHoE">SmallComponent!</h4>
</div>
```

`h4` タグに `title` に対応する文字列 `SmallComponent_title_1UHoE` のclassが設定されていることがわかります。

また、 `<head>` タグの中に以下のようなCSSが埋め込まれています:

```css
.SmallComponent_title_1UHoE {
    border: dashed 2px #5b8bd0;
    border-radius: 5px;
    padding: 3px;
}
```

このように、 CSS Modules は**class名を衝突しづらいものに上書きする**仕組みとなっています。

# CSS Modules が Scoped CSS に比べてベターな理由
Scoped CSS では、[グローバルに定義されたclassセレクタが適用されることがある](https://qiita.com/mascii/items/623d4c97c3f95e6984c0#%E3%82%B0%E3%83%AD%E3%83%BC%E3%83%90%E3%83%AB%E3%81%AB%E5%AE%9A%E7%BE%A9%E3%81%95%E3%82%8C%E3%81%9F%E3%82%BB%E3%83%AC%E3%82%AF%E3%82%BF%E3%81%8C%E9%81%A9%E7%94%A8%E3%81%95%E3%82%8C%E3%82%8B%E3%81%93%E3%81%A8%E3%81%8C%E3%81%82%E3%82%8B)という落とし穴があり、コンポーネントの外側から影響を受けやすい仕組みとなっています。

さらに、 Scoped CSS では[親コンポーネントと子コンポーネントで同じclass名を用いると予期せぬ装飾がなされることがある](https://qiita.com/mascii/items/623d4c97c3f95e6984c0#2%E5%80%8B%E3%81%AE-data-v-xxxxxxxx-%E5%B1%9E%E6%80%A7%E3%81%AE%E4%BB%98%E4%B8%8E)という落とし穴もあります。

上記2点の問題については、 CSS Modules は先述の通り**class名を衝突しづらいものに上書きする**仕組みのため、発生しにくくなっています。

# CSS Modules にも気をつけるべきことがあるらしい
例として以下のような `BlueButton` を考えてみます:

```vue:BlueButton.vue
<template>
  <button :class="$style.button" @click="e => $emit('click', e)">
    <slot></slot>
  </button>
</template>

<script>
import Vue from 'vue';

export default Vue.extend({
  name: 'BlueButton',
});
</script>

<style module>
.button {
    background: blue;
    padding: 5px 10px;
}
</style>
```

さらに親コンポーネントにおいて

```vue
<style module>
.red {
  background: red;
}
</style>
```

のように定義し、 `<BlueButton :class="$style.red">Click!</BlueButton>` のように用いる[^6]と、このボタンは赤くなるでしょうか？それとも青くなるでしょうか？

答えは、**どちらになるかビルドしてみるまで確定しない**とのことです[^7] [^8] [^9]。

この問題は**コンポーネントの内側の装飾を外側から上書きしない**こと、**外側からは `margin` などコンポーネントの外側についての装飾だけをする**ことを守るようにすれば発生しないのではないかと思いました。
特に、デザイナーさんがマークアップをする場合、守ってほしいことをきちんと伝えると良さそうです。

# まとめ
- Scoped CSS で発生し得る問題が CSS Modules の方が発生しにくい
- コンポーネントの内側の装飾を外側から上書きしないことを守るべきである
- template を書く際に `class="class-name"` ではなく `:class="$style.class_name"` を用いるため、人によっては書きづらさを感じるかもしれない
    - class名には `snake_case` か `camelCase` を用いる

# Tips を書きました
こちらの記事もあわせてご覧ください: [【Vue.js】CSS Modulesで書き始めるときに見るべきTips](https://qiita.com/mascii/items/1d43f5e5529d3beb433b)

[^1]: [Shadow DOM](https://developer.mozilla.org/ja/docs/Web/Web_Components/Using_shadow_DOM) が主要ブラウザで問題なく使える時代になったら廃れる気がしています
[^2]: [Vue.jsのScoped CSSがいかにしてスコープを実現しているのか検証してみた](https://qiita.com/mascii/items/623d4c97c3f95e6984c0)
[^3]: [クラスとスタイルのバインディング](https://jp.vuejs.org/v2/guide/class-and-style.html)
[^4]: 減算演算子としての `-` とコンフリクトするため
[^5]: css-loader の設定で `localIdentName` を変更することで対応する文字列の規則を変更することができます
[^6]: 元のコンポートの色を上書きするような使い方は微妙かもしれませんが、わかりやすい例として例示します
[^7]: 参考: [css-modulesを止めようとしている話（具体的な解決編）](https://saneyukis.hatenablog.com/entry/2019/03/14/023446), [AbemaTVにおけるCSS is too fragile問題に対する解](https://speakerdeck.com/kubosho/solution-of-css-is-too-fragile-by-abematv?slide=9)
[^8]: Scoped CSS においても仕組み上、同様の問題が発生する可能性はあります
[^9]: 手元の環境で試したところ、ボタンは赤くなりました。色々試しても青くすることはできなかったのですが、どちらの色になるかわからない不安は払拭できませんでした。