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

[vue-loader](https://vue-loader.vuejs.org/ja/) では `<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-cli でプロジェクトを作成](https://cli.vuejs.org/guide/creating-a-project.html)します:

```bash
vue create scoped-css-test
# 今回はプリセットに default (babel, eslint) を選択
cd scoped-css-test
```

(vue-cli をグローバルにインストールしたくない場合は `vue` コマンドの代わりに `npx @vue/cli` でもOKです)

# 手を動かしながら理解してみる
## コンポーネントを作成
以下の `SmallComponent`, `MediumComponent`, `LargeComponent` を作成します:
(名前はなんでも良いのですが、入れ子構造がわかりやすいようにこのような名前にします)

```vue:src/components/SmallComponent.vue
<template>
  <div>
    <h4>SmallComponent!</h4>
  </div>
</template>

<script>
import Vue from 'vue';

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

```vue:src/components/MediumComponent.vue
<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>
```

```vue:src/components/LargeComponent.vue
<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` をマウントするように変更します:

```javascript:src/main.js
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/ にアクセスすると、以下のようなページが表示されます:

<img width="289" alt="largecomponent01.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/137045/fba54b2e-f215-efde-10c0-7f2db7ac5251.png">

ブラウザのインスペクタで確認すると、以下のようなDOMツリーが表示されます:

```html
<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ツリーを図示するとこんな感じです:

![dombox-no-color.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/137045/b2056f4c-7b64-8692-38dd-6f8bbc4cbd33.png)

## Scoped CSS で装飾する
`SmallComponent` から装飾してみます:

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

<script>
// 省略
</script>

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

ページリロードすると、以下のようなページが表示されます:
<img width="495" alt="largecomponent02.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/137045/e33c158b-53cc-c6bf-ac3c-4ddcf68bfde0.png"> 

ページをリロードしてブラウザのインスペクタで確認すると、以下のようなDOMツリーが表示されます:

```html
<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が埋め込まれます:

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

`.title` のセレクタが `.title[data-v-509622e6]` に変換されていることがわかるかと思います。

これが何に役立っているものかまだわかりにくいので、 `MediumComponent`, `LargeComponent` にも装飾してみます:

```vue:src/components/MediumComponent.vue
<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>
```

```vue:src/components/LargeComponent.vue
<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>
```

ページリロードすると、以下のようなページが表示されます:
<img width="495" alt="largecomponent03.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/137045/7ef472d8-499f-5fe7-c780-b27bde4136f6.png">

ページをリロードしてブラウザのインスペクタで確認すると、以下のようなDOMツリーが表示されます:

```html
<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が埋め込まれます:

```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` をそれぞれ赤、緑、青として先ほどの図を色付けするとこんな感じです:

![dombox.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/137045/e8d38c64-66b2-785a-bd7b-962d135bafa2.png)

このように、コンポーネントごとに `data-v-xxxxxxxx` が通常のHTML要素**すべて**と、子コンポーネントの**ルート要素**(`<template>` の次の子要素)に付与されます。

# scoped属性を付け忘れるとどうなるのか
各コンポーネントの `<style scoped>` を `<style>` にしてみます:

<img width="494" alt="largecomponent04.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/137045/95d7e8cb-c28e-caaf-1c07-9a4f84796621.png">

`LargeComponent.vue` で定義した装飾が他のコンポーネントにも適用されてしまっていることがわかります。

ブラウザのインスペクタで確認すると、以下のようなDOMツリーが表示されます:

```html
<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が埋め込まれます:

```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名と被らないよう気をつける必要があります。

<img width="495" alt="largecomponent05.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/137045/45c0292a-111f-cb3c-9be5-c3d961bfb6ce.png">

## 2個の `data-v-xxxxxxxx` 属性の付与
上にある図のように、コンポーネントに対応する要素については2個の `data-v-xxxxxxxx` 属性が付くことがあります。
これに起因し、親コンポーネントと子コンポーネントで同じclass名を用いると予期せぬ装飾がなされることがあります。

詳しくはこちらの記事を参照ください: [vue-loaderのScoped CSSのスタイルが子コンポーネントのルート要素に効いてしまって辛い](https://qiita.com/yassh/items/7fb75904de19ff3bd3e8)

コンポーネント(`<SmallComponent />` など)と、コンポーネントのルート要素にclass名を付けて装飾するときには、名前に気をつける必要がありそうです。
コンポーネントのルート要素のclass名には `small-component-container` を付ける、といった名前の衝突を避けるためのルールを設けると良いのかもしれません。

# まとめ
- 各コンポーネントで同じclass名を用いていても意図した装飾がなされることを確認できた
    - ただし `data-v-xxxxxxxx` 属性が2個付く要素はclass名の付け方に気をつけなければならない
- `*.vue` ファイルの `<template>` の中においても、グローバルに定義されたセレクタが適用されることに注意しなければならない