Edited at

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

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

Vue.js で Single File Components を利用する場合、 Scoped CSS は手軽に利用できますが CSS Modules についても手軽に利用ができることがわかったので、比較をしてみました。


Scoped CSS, CSS Modules の利用方法

実際に手を動かして検証されたい方は、以前に書いた記事2を参照してプロジェクトを作成してください。


Scoped CSS

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


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名を指定します:


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>


ここで :classv-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 に対応する要素を確認してみます:

<div data-v-509622e6>

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

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

また、インスペクタで確認すると <head> タグの中に以下のような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 モジュールのページに書いてある通り、 <style module> を用いることで $style という名前の算出プロパティが自動的に注入されます。

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


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

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

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

<div>

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

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

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

.SmallComponent_title_1UHoE {

border: dashed 2px #5b8bd0;
border-radius: 5px;
padding: 3px;
}

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


CSS Modules が Scoped CSS に比べてベターな理由

Scoped CSS では、グローバルに定義されたclassセレクタが適用されることがあるという落とし穴があり、コンポーネントの外側から影響を受けやすい仕組みとなっています。

さらに、 Scoped CSS では親コンポーネントと子コンポーネントで同じclass名を用いると予期せぬ装飾がなされることがあるという落とし穴もあります。

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


CSS Modules にも気をつけるべきことがあるらしい

例として以下のような BlueButton を考えてみます:


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>


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

<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_casecamelCase を用いる







  1. Shadow DOM が主要ブラウザで問題なく使える時代になったら廃れる気がしています 



  2. Vue.jsのScoped CSSがいかにしてスコープを実現しているのか検証してみた 



  3. クラスとスタイルのバインディング 



  4. 減算演算子としての - とコンフリクトするため 



  5. css-loader の設定で localIdentName を変更することで対応する文字列の規則を変更することができます 



  6. 元のコンポートの色を上書きするような使い方は微妙かもしれませんが、わかりやすい例として例示します 



  7. 参考: css-modulesを止めようとしている話(具体的な解決編), AbemaTVにおけるCSS is too fragile問題に対する解 



  8. Scoped CSS においても仕組み上、同様の問題が発生する可能性はあります 



  9. 手元の環境で試したところ、ボタンは赤くなりました。色々試しても青くすることはできなかったのですが、どちらの色になるかわからない不安は払拭できませんでした。