vue-loader では <style scoped> を用いることで、 各コンポーネントで scoped CSS をシミュレートすることが可能 です。
この機能は手軽に利用できるものの、どういった仕組みでスコープを実現しているのか、注意すべき点は何なのかが気になったため、実際にコードを書いて検証してみました。
動作環境
- macOS Mojave
- Node.js 10
- @vue/cli 3.8.4
- Vue 2.6.10
- vue-loader 15.7.0
- Chrome 75
プロジェクトを作成
vue create scoped-css-test
# 今回はプリセットに default (babel, eslint) を選択
cd scoped-css-test
(vue-cli をグローバルにインストールしたくない場合は vue コマンドの代わりに npx @vue/cli でもOKです)
手を動かしながら理解してみる
コンポーネントを作成
以下の SmallComponent, MediumComponent, LargeComponent を作成します:
(名前はなんでも良いのですが、入れ子構造がわかりやすいようにこのような名前にします)
<template>
<div>
<h4>SmallComponent!</h4>
</div>
</template>
<script>
import Vue from 'vue';
export default Vue.extend({
name: 'SmallComponent',
});
</script>
<template>
<div>
<h3>MediumComponent!</h3>
<SmallComponent />
</div>
</template>
<script>
import Vue from 'vue';
import SmallComponent from './SmallComponent.vue';
export default Vue.extend({
name: 'MediumComponent',
components: {
SmallComponent,
},
});
</script>
<template>
<div>
<h2>LargeComponent!</h2>
<MediumComponent />
<MediumComponent />
<MediumComponent />
</div>
</template>
<script>
import Vue from 'vue';
import MediumComponent from './MediumComponent.vue';
export default Vue.extend({
name: 'LargeComponent',
components: {
MediumComponent,
},
});
</script>
コンポーネントをマウントする
src/main.js を <div id="app"></div> に対して LargeComponent をマウントするように変更します:
import Vue from 'vue'
import LargeComponent from './components/LargeComponent.vue'
Vue.config.productionTip = false
new Vue({
render: h => h(LargeComponent),
}).$mount('#app')
npm run serve でアプリケーションを起動させ、ブラウザから http://localhost:8080/ にアクセスすると、以下のようなページが表示されます:
ブラウザのインスペクタで確認すると、以下のようなDOMツリーが表示されます:
<div>
<h2>LargeComponent!</h2>
<div>
<h3>MediumComponent!</h3>
<div>
<h4>SmallComponent!</h4>
</div>
</div>
<div>
<h3>MediumComponent!</h3>
<div>
<h4>SmallComponent!</h4>
</div>
</div>
<div>
<h3>MediumComponent!</h3>
<div>
<h4>SmallComponent!</h4>
</div>
</div>
</div>
<MediumComponent /> や <SmallComponent /> は各コンポーネント(*.vue ファイル)の <template> の中身で置換されていることがわかるかと思います。
上記のDOMツリーを図示するとこんな感じです:
Scoped CSS で装飾する
SmallComponent から装飾してみます:
<template>
<div>
<h4 class="title">SmallComponent!</h4>
</div>
</template>
<script>
// 省略
</script>
<style scoped>
.title {
border: dashed 2px #5b8bd0;
border-radius: 5px;
padding: 3px;
}
</style>
ページをリロードしてブラウザのインスペクタで確認すると、以下のようなDOMツリーが表示されます:
<div>
<h2>LargeComponent!</h2>
<div>
<h3>MediumComponent!</h3>
<div data-v-509622e6>
<h4 data-v-509622e6 class="title">SmallComponent!</h4>
</div>
</div>
<div>
<h3>MediumComponent!</h3>
<div data-v-509622e6>
<h4 data-v-509622e6 class="title">SmallComponent!</h4>
</div>
</div>
<div>
<h3>MediumComponent!</h3>
<div data-v-509622e6>
<h4 data-v-509622e6 class="title">SmallComponent!</h4>
</div>
</div>
</div>
<SmallComponent /> に対応する <div> タグとそこに含まれる <h4> タグに data-v-509622e6 属性が付与されていることがわかります。
また、 <head> タグの中に以下のCSSが埋め込まれます:
.title[data-v-509622e6] {
border: dashed 2px #5b8bd0;
border-radius: 5px;
padding: 3px;
}
.title のセレクタが .title[data-v-509622e6] に変換されていることがわかるかと思います。
これが何に役立っているものかまだわかりにくいので、 MediumComponent, LargeComponent にも装飾してみます:
<template>
<div>
<h3 class="title">MediumComponent!</h3>
<SmallComponent />
</div>
</template>
<script>
// 省略
</script>
<style scoped>
.title {
border: solid 2px #309468;
border-radius: 5px;
padding: 3px;
}
</style>
<template>
<div>
<h2 class="title">LargeComponent!</h2>
<MediumComponent />
<MediumComponent />
<MediumComponent />
</div>
</template>
<script>
// 省略
</script>
<style scoped>
.title {
border: double 4px #b72629;
border-radius: 5px;
padding: 3px;
}
</style>
ページをリロードしてブラウザのインスペクタで確認すると、以下のようなDOMツリーが表示されます:
<div data-v-34f8b132>
<h2 data-v-34f8b132 class="title">LargeComponent!</h2>
<div data-v-21e071f0 data-v-34f8b132>
<h3 data-v-21e071f0 class="title">MediumComponent!</h3>
<div data-v-509622e6 data-v-21e071f0>
<h4 data-v-509622e6 class="title">SmallComponent!</h4>
</div>
</div>
<div data-v-21e071f0 data-v-34f8b132>
<h3 data-v-21e071f0 class="title">MediumComponent!</h3>
<div data-v-509622e6 data-v-21e071f0>
<h4 data-v-509622e6 class="title">SmallComponent!</h4>
</div>
</div>
<div data-v-21e071f0 data-v-34f8b132>
<h3 data-v-21e071f0 class="title">MediumComponent!</h3>
<div data-v-509622e6 data-v-21e071f0>
<h4 data-v-509622e6 class="title">SmallComponent!</h4>
</div>
</div>
</div>
また、 <head> タグの中に以下のCSSが埋め込まれます:
.title[data-v-509622e6] {
border: dashed 2px #5b8bd0;
border-radius: 5px;
padding: 3px;
}
.title[data-v-21e071f0] {
border: solid 2px #309468;
border-radius: 5px;
padding: 3px;
}
.title[data-v-34f8b132] {
border: double 4px #b72629;
border-radius: 5px;
padding: 3px;
}
title というclass名が各コンポーネントで使われており名前が被ってしまっていますが、 data-v-xxxxxx が自動的に設定されていることで 各コンポーネントで同じclass名が使用されていても意図した装飾がなされている ことがわかります(Scoped CSS を使うメリット)。
data-v-34f8b132, data-v-21e071f0, data-v-509622e6 をそれぞれ赤、緑、青として先ほどの図を色付けするとこんな感じです:
このように、コンポーネントごとに data-v-xxxxxxxx が通常のHTML要素すべてと、子コンポーネントのルート要素(<template> の次の子要素)に付与されます。
scoped属性を付け忘れるとどうなるのか
各コンポーネントの <style scoped> を <style> にしてみます:
LargeComponent.vue で定義した装飾が他のコンポーネントにも適用されてしまっていることがわかります。
ブラウザのインスペクタで確認すると、以下のようなDOMツリーが表示されます:
<div>
<h2 class="title">LargeComponent!</h2>
<div>
<h3 class="title">MediumComponent!</h3>
<div>
<h4 class="title">SmallComponent!</h4>
</div>
</div>
<div>
<h3 class="title">MediumComponent!</h3>
<div>
<h4 class="title">SmallComponent!</h4>
</div>
</div>
<div>
<h3 class="title">MediumComponent!</h3>
<div>
<h4 class="title">SmallComponent!</h4>
</div>
</div>
</div>
data-v-xxxxxxxx の属性がすべて無くなってしまいました。
<head> タグの中に以下のCSSが埋め込まれます:
.title {
border: dashed 2px #5b8bd0;
border-radius: 5px;
padding: 3px;
}
.title {
border: solid 2px #309468;
border-radius: 5px;
padding: 3px;
}
.title {
border: double 4px #b72629;
border-radius: 5px;
padding: 3px;
}
.title のセレクタが3個あることがわかります。
このため、色が #b72629 のものだけが適用されてしまったようです。
注意点
グローバルに定義されたセレクタが適用されることがある
例えば public/index.html の <head> タグの中で Font Awesome などの CSS を読み込ませることで、 *.vue ファイルの <template> の中でも読み込んだ CSS のセレクタが適用されるようになります。
このため、 *.vue ファイルの中であってもグローバルに定義されたclassセレクタのclass名と被らないよう気をつける必要があります。
2個の data-v-xxxxxxxx 属性の付与
上にある図のように、コンポーネントに対応する要素については2個の data-v-xxxxxxxx 属性が付くことがあります。
これに起因し、親コンポーネントと子コンポーネントで同じclass名を用いると予期せぬ装飾がなされることがあります。
詳しくはこちらの記事を参照ください: vue-loaderのScoped CSSのスタイルが子コンポーネントのルート要素に効いてしまって辛い
コンポーネント(<SmallComponent /> など)と、コンポーネントのルート要素にclass名を付けて装飾するときには、名前に気をつける必要がありそうです。
コンポーネントのルート要素のclass名には small-component-container を付ける、といった名前の衝突を避けるためのルールを設けると良いのかもしれません。
まとめ
- 各コンポーネントで同じclass名を用いていても意図した装飾がなされることを確認できた
- ただし
data-v-xxxxxxxx属性が2個付く要素はclass名の付け方に気をつけなければならない
- ただし
-
*.vueファイルの<template>の中においても、グローバルに定義されたセレクタが適用されることに注意しなければならない



