【Vue.js】Scoped CSSのscoped具合を検証してみた

Vue.jsの[Scoped CSS](https://vue-loader-v14.vuejs.org/ja/features/scoped-css.html)は完全にはscopedじゃない、という話を聞いたので検証してみました。
結果、コンポーネント最上位の要素と、slotについては気をつける必要があることが分かりました。

## 参考記事
親コンポーネントのstyleが子コンポーネントに影響を与える仕組みは、以下が詳しいです。

* https://qiita.com/wintyo/items/dfc232255ad45fdf376f
* https://www.dkrk-blog.net/javascript/vue_scopedcss


## 検証環境
* OS: macOS Catalina 10.15.5
* ブラウザ: Google Chrome 83.0.4103.116（Official Build）(64bit)
* Vue.js: 2.6.11


<br>
## 検証1: 素朴にstyleを当てた場合
親、子、孫コンポーネントにそれぞれ違った色を割り当てました。
すると、各コンポーネントの最上位の要素には親コンポーネントのスタイルが適用されてしまいました。
![1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/162371/e5e31450-3b85-6cbc-80a6-0127f657f37a.png)

```Parent.vue
<template>
  <div class="root-div">
    this is root div of parent
    <h1 class="my-title">this is h1 of parent</h1>
    <div  class="h2-container">
      <h2>this is h2 of parent</h2>
    </div>
    <Child />
  </div>
</template>

<script lang="ts">
import Vue from "vue";
import Child from "@/components/Child.vue";

export default Vue.extend({
  name: "Parent",
  components: {
    Child,
  },
});
</script>

<style scoped lang="scss">
.root-div {
  color: red;
}
.my-title {
  color: red;
}
h2 {
  color: red;
}
</style>
```

```Child.vue
<template>
  <div class="root-div">
    this is root div of child
    <h1 class="my-title">this is h1 of child</h1>
    <div  class="h2-container">
      <h2>this is h2 of child</h2>
    </div>
    <GrandChild />
  </div>
</template>

<script lang="ts">
import Vue from "vue";
import GrandChild from "@/components/GrandChild.vue";

export default Vue.extend({
  name: "Child",
  components: {
    GrandChild,
  },
});
</script>

<style scoped lang="scss">
.root-div {
  color: blue; // こいつが効いていない
}
.my-title {
  color: blue;
}
h2 {
  color: blue;
}
</style>
```

```GrandChild.vue
<template>
  <div class="root-div">
    this is root div of grandchild
    <h1 class="my-title">this is h1 of grandchild</h1>
    <div  class="h2-container">
      <h2>this is h2 of grandchild</h2>
    </div>
  </div>
</template>

<script lang="ts">
import Vue from "vue";

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

<style scoped lang="scss">
.root-div {
  color: green;
}
.my-title {
  color: green;
}
h2 {
  color: green;
}
</style>
```

<br>
## 検証2: 全称セレクタ(*)を使った場合
`*`を使って乱暴にstyleを指定した場合どうなるかを見てみました。
結果、検証1と同じく、各コンポーネント最上位の要素だけは親コンポーネントのstyleの影響が出ました。
※検証1のコードから、Child.vueだけ変更しました。
![1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/162371/f42d3974-1134-884b-00ce-263a9c804efd.png)

```Child.vue
<template>
  <div class="root-div">
    this is root div of child
    <h1 class="my-title">this is h1 of child</h1>
    <div  class="h2-container">
      <h2>this is h2 of child</h2>
    </div>
    <GrandChild />
  </div>
</template>

<script lang="ts">
import Vue from "vue";
import GrandChild from "@/components/GrandChild.vue";

export default Vue.extend({
  name: "Child",
  components: {
    GrandChild,
  },
});
</script>

<style scoped lang="scss">
.root-div * {
  color: blue;
}
* {
  color: blue;
}
* * {
  color: blue;
}
</style>
```

<br>
## 検証3: globalのcssがある場合
当たり前ですが、ページ全体に適用されているcssがある場合、競合します。
例えば、ページ全体に`* { color: black!important }`を当てたら、すべて黒になります。
![1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/162371/647bdd46-1679-a8ee-86a8-7546a23e5cc7.png)

```App.vue
<style lang="scss">
* {
  color: black!important; // プロダクトコードで書いたら殴られるやつ
}
</style>
```

<br>
もちろん、`!important`を付けなければ、コンポーネント内のcssが優先的に適用されます。
(この辺はScoped CSSというよりはCSSそのものの話)
![1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/162371/da7e1b8f-91b6-00f0-fa1b-7d72e92d4944.png)

```App.vue
<style lang="scss">
* {
  color: black;
}
</style>
```

<br>
## 検証4: slotを使った場合
slotを使って、親から子へタグを渡した場合、渡したタグには親コンポーネントのstyleが適用されました。
![1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/162371/fd18c235-5cbc-5c67-564d-86a7d3ba982f.png)

```Parent.vue
<template>
  <div class="root-div">
    this is root div of parent
    <h1 class="my-title">this is h1 of parent</h1>
    <div  class="h2-container">
      <h2>this is h2 of parent</h2>
    </div>
    <Child>
      <!-- 親からタグごとslotを挟むと、親のstyleが適用される -->
      <template #my-slot>
        <p>slot: this is p of parent</p>
        <div>
          <h2>slot: this is h2 of parent</h2>
        </div>
      </template>
    </Child>
  </div>
</template>

<script lang="ts">
import Vue from "vue";
import Child from "@/components/Child.vue";

export default Vue.extend({
  name: "Parent",
  components: {
    Child,
  },
});
</script>

<style scoped lang="scss">
.root-div {
  color: red;
}
.my-title {
  color: red;
}
h2 {
  color: red;
}
p {
  color: red;
}
</style>
```

```Child.vue
<template>
  <div class="root-div">
    this is root div of child
    <h1 class="my-title">this is h1 of child</h1>
    <div  class="h2-container">
      <h2>this is h2 of child</h2>
    </div>
    <slot name="my-slot">
        <p>slot: this is p of child</p>
        <div>
          <h2>slot: this is h2 of child</h2>
        </div>
    </slot>
    <GrandChild />
  </div>
</template>

<script lang="ts">
import Vue from "vue";
import GrandChild from "@/components/GrandChild.vue";

export default Vue.extend({
  name: "Child",
  components: {
    GrandChild,
  },
});
</script>

<style scoped lang="scss">
.root-div {
  color: blue;
}
.my-title {
  color: blue;
}
h2 {
  color: blue;
}
p {
  color: blue;
}
</style>
```

<br>
一方、親から子へ文字列だけを渡した場合、子のstyleが適用されました。
![1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/162371/f7fedb5e-9783-7cf4-1d6d-e9ddc7897ede.png)

```Parent.vue
<template>
  <div class="root-div">
    this is root div of parent
    <h1 class="my-title">this is h1 of parent</h1>
    <div  class="h2-container">
      <h2>this is h2 of parent</h2>
    </div>
    <Child>
      <!-- 親から文字列だけを渡すようにすれば、親のstyleは適用されない -->
      <template #my-slot-p>slot: this is p of parent</template>
      <template #my-slot-h2>slot: this is h2 of parent</template>
    </Child>
  </div>
</template>

<script lang="ts">
import Vue from "vue";
import Child from "@/components/Child.vue";

export default Vue.extend({
  name: "Parent",
  components: {
    Child,
  },
});
</script>

<style scoped lang="scss">
.root-div {
  color: red;
}
.my-title {
  color: red;
}
h2 {
  color: red;
}
p {
  color: red;
}
</style>
```

```Child.vue
<template>
  <div class="root-div">
    this is root div of child
    <h1 class="my-title">this is h1 of child</h1>
    <div  class="h2-container">
      <h2>this is h2 of child</h2>
    </div>
    <p><slot name="my-slot-p">slot: this is p of child</slot></p>
    <div>
      <h2><slot name="my-slot-h2">slot: this is h2 of child</slot></h2>
    </div>
    <GrandChild />
  </div>
</template>

<script lang="ts">
import Vue from "vue";
import GrandChild from "@/components/GrandChild.vue";

export default Vue.extend({
  name: "Child",
  components: {
    GrandChild,
  },
});
</script>

<style scoped lang="scss">
.root-div {
  color: blue;
}
.my-title {
  color: blue;
}
h2 {
  color: blue;
}
p {
  color: blue;
}
</style>
```

<br>
## で、結局何に気をつければいいのか
* コンポーネント最上位の要素になるべくstyleを当てない
* 当てざるを得ない場合、`.component-name-root { display: flex }`のように、重複しないクラス名を付けて、styleを指定する 
* 自前でslotを作る場合、極力DOM構造は子コンポーネント側で定義する
* デザインフレームワーク(vuetifyとか)の制約上slotにDOMを渡さざるを得ない場合は、予期せぬstyleが適用されないよう気をつける

## 所感
完全でないとはいえ、知らずに使っていてもそんなに気にならない程度にはscopedにできていると感じました。
個人的にはslotを使わないため、各コンポーネントの最上位のクラス名だけ気をつければ問題なし! という印象です。
Scoped CSS便利。