コンポーネント内で閉じた装飾の手法として、 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
を指定します:
<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名を指定します:
<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
に対応する要素を確認してみます:
<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
の中身を表示してみます:
<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コンソールで確認すると、以下のオブジェクトが表示されます:
$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
を考えてみます:
<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_case
かcamelCase
を用いる
- class名には
Tips を書きました
こちらの記事もあわせてご覧ください: 【Vue.js】CSS Modulesで書き始めるときに見るべきTips
-
Shadow DOM が主要ブラウザで問題なく使える時代になったら廃れる気がしています ↩
-
減算演算子としての
-
とコンフリクトするため ↩ -
css-loader の設定で
localIdentName
を変更することで対応する文字列の規則を変更することができます ↩ -
元のコンポートの色を上書きするような使い方は微妙かもしれませんが、わかりやすい例として例示します ↩
-
参考: css-modulesを止めようとしている話(具体的な解決編), AbemaTVにおけるCSS is too fragile問題に対する解 ↩
-
Scoped CSS においても仕組み上、同様の問題が発生する可能性はあります ↩
-
手元の環境で試したところ、ボタンは赤くなりました。色々試しても青くすることはできなかったのですが、どちらの色になるかわからない不安は払拭できませんでした。 ↩