iT邦幫忙

2022 iThome 鐵人賽

DAY 14
1
Modern Web

真的好想離開 Vue 3 新手村 feat. CompositionAPI系列 第 14

真的好想離開 Vue 3 新手村 - Day 14: style scoped 原理與特殊選擇器 :deep()&:slotted()

  • 分享至 

  • xImage
  •  

Outline

  • <style scoped> 的作用方式
  • 突破 <style scoped> 的特殊選擇器
    a. 深層選擇器 (Deep Selectors)
    b. 插槽選擇器 (Slotted Selectors)
    c. 全域選擇器 (Global Selector)
  • 總結

<style scoped> 的作用方式

在使用 SFC 檔進行開發的狀況下,我們很常用 scoped 來鎖住 CSS style 影響的區域,讓樣式只應用在這個元件(或頁面)上,不會影響到其他元件(或頁面)的樣式。

那 Vue 是怎麼做到把樣式 scoped 住的呢?

Vue 會透過 PostCSS 來做 CSS 的後處理,至於處理規則為何?
直接到瀏覽器觀察看看,一般 <style> 和使用 scoped 兩種狀況下,轉換出來的 CSS 內容有什麼差別?

畫面結構(後續的範例觀察都使用相同的畫面結構)

  • 父層:App.vue
<template>
    <div class="wrapper">
        <h1>APP 層的 h1</h1>
        <HelloWorld/>
    </div>
<template/>
  • 子層:HelloWorld.vue
<template>
    <div class="greetings">
        <h1>子元件層的 h1</h1>
    </div>
<template>

1. 不加 scoped

  • 父層:App.vue
<style>
h1 {
  color: darksalmon;
}
</style>

結果

HTML 架構

<div class="wrapper">
    <h1>APP 層的 h1</h1>
    <div class="greetings">
        <h1>子元件層的 h1</h1>
    </div>
</div>

CSS 樣式

h1 {
    color: darksalmon;
}
  • 生成的 CSS style 跟在 <style> 內寫選擇器方式一模一樣,也就代表,整個專案 App 內,只要有 <h1>,就會吃到這個樣式。
  • 同理,將這個樣式寫在子元件的 <style> 內,也會有一樣的效果,可以預想樣式管理會變得比較混亂。

scope 的樣式會影響到全域,包含祖先和後代

2. 加 scoped

  • 父層:App.vue
<style scoped>
h1 {
  color: darksalmon;
}
</style>

結果

HTML 架構

<div class="wrapper" data-v-7a7a37b1>
    <h1 data-v-7a7a37b1>APP 層的 h1</h1>
    <div class="greetings" data-v-7a7a37b1>
        <h1>子元件層的 h1</h1>
    </div>
</div>

CSS 樣式

h1[data-v-7a7a37b1]{
    color: darksalmon;
}
  • <style> 加上 scoped 屬性後,從渲染後的 HTML 可以發現,該元件 template 內的所有 HTML 元素,都會被加上 data-v-xxxxx 屬性,這個屬性可以想成是 Vue 給每個元件的 id
  • 生成的 CSS 樣式會被加上對應的屬性選擇器([data-v-xxxxx]),這樣產生的 CSS 樣式,就只會套用在特定元件上。

scoped 的樣式只會影響到同個元件(或頁面)的 HTML 元素


Vue style 特殊選擇器

1. :deep()

在特定情況下,會希望從父元件影響子元件的樣式,但又不希望將樣式影響到全域,這時候就可以用 :deep() 深度選擇器,來突破 scoped 的限制。
:deep()在要修改 UI framework 元件的樣式時很好用。(這個等到引入 Quasar 篇章時再來示範)

  • 父層:App.vue
<style scoped>
:deep(h1) {
  color: darksalmon;
}
</style>

經過轉換後會生成 CSS 樣式如下:

[data-v-7a7a37b1] h1 {
    color: darksalmon;
}

轉換結果為後代選擇器(descendant selector),會在該 CSS 樣式前,加上屬性選擇器 (屬性為元件的 id - data-v-xxxxx)。
從上面的 CSS 程式碼可以理解到,所有後代元件<h1> 都會被這個樣式影響到,包括兒子、孫子、曾孫...,也就是說,使用 :deep() 所有後代都會被影響,使用上要特別注意!

2. :slotted()

Vue 預設傳入元件的 <slot/> 內容樣式,不會被當層元件的 scoped style 影響,因為 <slot/> 內容屬於他的父元件,那如果想在當前的元件做樣式管理,可以用:slotted()

畫面結構

  • 父層:App.vue
<template>
    <div class="wrapper">
      <h1>APP 層的 h1</h1>
      <HelloWorld><h1>子元件層的 h1</h1> </HelloWorld>
    </div>
<template/>
  • 子層:HelloWorld.vue
<template>
    <slot></slot>
<template>

結果

  1. 先來看使用 <slot> 後的渲染結果。
<div class="wrapper">
    <h1>APP 層的 h1</h1>
    <div class="greetings" data-v-e17ea971>
        <h1 data-v-e17ea971-s>子元件層的 h1</h1>
    </div>
</div>
  1. 在兩個元件檔的 <style> 都加上 scoped 屬性後的渲染結果
<div class="wrapper" data-v-7a7a37b1>
    <h1 data-v-7a7a37b1>APP 層的 h1</h1>
    <div class="greetings" data-v-e17ea971 data-v-7a7a37b1>
        <h1 data-v-7a7a37b1 data-v-e17ea971-s>子元件層的 h1</h1>
    </div>
</div>

觀察渲染結果可以發現:

  • data-v-7a7a 是父層(App)的識別屬性
  • data-v-e17e 是子層元件(HelloWorld)的識別屬性。

而從父層傳入子層元件的 slot (<h1>) 元素上,會帶有父層的識別屬性(data-v-7a7a),所以會被父層的 scoped style 影響。

除了父層的識別屬性之外, slot 內的元素還帶有 data-v-e17e-s,是子層元件的識別屬性加上 -s 字符,表示這個元素是子層元件的 slot。

那要如何在子層影響外來的 slot 內容?
可以在子層(HelloWorld元件)的<style> 中使用 :slotted() 選擇器,使用方式如下:

  • 子層:HelloWorld.vue
<style scoped>
:slotted(h1) {
  color: darksalmon;
}
</style>

所生成的 CSS 樣式為:

h1[data-v-e17ea971-s] {
    color: darksalmon;
}

:slotted() 選擇器就是利用 slot 特有的識別屬性去選到他。

3. :global()

Vue 還有提供 :global() 選擇器,可以在 <style scope> 內使用,指定樣式轉換為全域樣式。

  • 父層:App.vue
<style scoped>
:global(h1) {
  color: darksalmon;
}
</style>

經過轉換會生成 CSS 樣式如下:

h1 {
    color: darksalmon;
}

但是,如果真的要在同一個 SFC 檔內,定義全域和區域樣式,與其使用 :global() 選擇器,不如合併使用 <style><style scoped>,將全域和區域樣式分開管理!

總結

  • <style>
    • 轉換前
    h1 {
        color: blue;
    }
    
    • 轉換後
      轉換後的樣式會套用到全域,不論是父代或子代
    h1 {
        color: blue;
    }
    
  • <style scoped>
    • 轉換前
    h1 {
        color: blue;
    }
    
    • 轉換後
      加上屬性選擇器後,該元件內的所有 HTML 標籤上會加入 [data-v-元件特殊識別符] 屬性,利用元件的特殊識別符,讓樣式只套用到指定元件。
    h1[data-v-元件特殊識別符] {
        color: blue;
    }
    
  • :deep():指定樣式會影響所有後代元件
    • 轉換前
    :deep(h1) {
        color: blue;
    }
    
    • 轉換後
      結合元件的屬性選擇器,組成後代選擇器,讓樣式只套用到指定元件的後代元件。(所有後代元件都會被影響)
    [data-v-元件特殊識別符] h1 {
        color: blue;
    }
    
  • :slot():指定樣式可以影響到父層傳進來的 slot 結構
    • 轉換前
    <style scoped>
    :slotted(h1) {
      color: darksalmon;
    }
    </style>
    
    • 轉換後
      加上屬性選擇器,利用 slot 的特殊識別符 (${父元件識別符}-s),讓樣式只套用到外層傳入的 slot 結構上。
    h1[data-v-e17ea971-s] {
        color: darksalmon;
    }
    
  • 同一份 SFC 檔中要定義全域和區域樣式
    • 可以在 <style scope> 內使用 :global() 選擇器
    • 更推薦將樣式分 <style scope><style> 兩個區塊管理

參考資料


上一篇
真的好想離開 Vue 3 新手村 - Day 13: v-on 語法、修飾符與找不到的 console.log
下一篇
Day 15: 在 Vue 專案使用 Sass/SCSS +共用變數 (feat. Vite)
系列文
真的好想離開 Vue 3 新手村 feat. CompositionAPI31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言