iT邦幫忙

2026 iThome 鐵人賽

DAY 9
0
Modern Web

重新認識Vue 走過路過不要錯過系列 第 9

Vue 走過路過不要錯過 Day09 - 從 {{ }} 到 v-bind:動態綁定 class 與 style

  • 分享至 

  • xImage
  •  

前幾天我們用 Proxy 做出迷你 reactive,也認識了 ref、reactive、watch。資料已經會「變」了,但使用者看不到資料,只看得到畫面。今天要講的就是資料和畫面之間的那座橋:模板語法。

一、{{ }}:它只負責「文字」

<script setup>
import { ref } from 'vue'
const name = ref('Fabio')
const user = ref({ age: 30 })
</script>
 
<template>
  <p>哈囉,{{ name }}</p>
  <p>{{ name.toUpperCase() }}</p>
  <p>{{ user }}</p>
</template>

大括號裡可以放「一個 JavaScript 表達式」。只要能算出一個值就行,例如三元運算、呼叫函式、陣列方法;但 iffor、宣告變數這類陳述句不行。

有幾個細節容易被忽略:

  1. 物件會被 JSON.stringify。上面的 {{ user }} 會顯示 { "age": 30 };而 nullundefined 會顯示成空字串,不會真的印出 "undefined"。
  2. 它是 textContent,不是 innerHTML。放進去的 <b>粗體</b> 會原封不動顯示成文字,這是 Vue 預設幫你擋掉 XSS。真的要塞 HTML 得改用 v-html,而且只能用在你信任的內容上。
  3. 模板裡不是什麼全域變數都能用MathDate 這類白名單可以用,但你自己掛在 window 上的東西不行。
    最關鍵的一點是:{{ }} 不能用在屬性上。
<!-- ❌ 不會動,id 就是字面上的 "{{ dynamicId }}" -->
<div id="{{ dynamicId }}"></div>

這就是 v-bind 登場的原因。

二、v-bind:把資料接到屬性上

<img v-bind:src="imgUrl" />
<img :src="imgUrl" />          <!-- 縮寫 -->
<img :src />                   <!-- 3.4+ 同名縮寫,等於 :src="src" -->

冒號後面那串,Vue 會當成 JavaScript 表達式來求值,而不是字串。所以 :count="1" 拿到的是數字 1,count="1" 拿到的是字串 "1"。傳 props 時,這個差別很重要。

值是 false、null 時會發生什麼?

這是最常被忽略的地方。Vue 3 的規則是:

綁定的值 一般屬性(id、aria-*) 布林屬性(disabled、checked)
null / undefined 移除屬性 移除屬性
false 保留,值為 "false" 移除屬性
"" 空字串 保留,值為空 保留(也就是啟用)

所以想「拿掉」一個屬性時,請給 null,不要給 false。像 :aria-hidden="false" 會真的輸出 aria-hidden="false",這對無障礙來說反而是正確的行為。

一次綁一整包

<script setup>
import { ref } from 'vue'
const inputAttrs = { type: 'email', placeholder: '請輸入 Email', required: true }
const attrName = ref('title')
</script>
 
<template>
  <input v-bind="inputAttrs" />
  <div :[attrName]="'我是動態屬性名'"></div>
</template>

v-bind="物件" 在包裝元件、轉傳一堆屬性時非常好用。:[attrName] 則是動態參數,連「要綁哪個屬性」都可以是變數。

三、:class,Vue 幫你特別加強的綁定

一般屬性綁的是字串,但 class 和 style 太常需要「條件式組合」,所以 Vue 對這兩個屬性額外支援物件和陣列。

物件語法:key 是 class 名,value 決定要不要加

<div class="btn" :class="{ active: isActive, 'is-error': hasError }"></div>

這裡有兩件事要注意:靜態的 class:class 可以並存,Vue 會把兩者合併;含 - 的 class 名要加引號。

陣列語法:直接列出要加的 class

<div :class="[sizeClass, isActive ? 'active' : '']"></div>
<div :class="[sizeClass, { active: isActive }]"></div>  <!-- 陣列裡也能放物件 -->

條件一多,就搬進 computed

<script setup>
import { computed } from 'vue'
const props = defineProps({ type: String, disabled: Boolean })
 
const btnClass = computed(() => ({
  'btn-primary': props.type === 'primary',
  'btn-danger': props.type === 'danger',
  'is-disabled': props.disabled
}))
</script>
 
<template>
  <button :class="btnClass"><slot /></button>
</template>

這樣模板保持乾淨,邏輯集中在一個地方,還能享有 computed 的快取。

在元件上寫 class,會跑去哪?

<MyButton class="mt-4" />

如果 MyButton 只有一個根元素,mt-4 會自動加到那個根元素上,並和元件內部原本的 class 合併,這叫屬性透傳(fallthrough attributes)。如果元件有多個根元素,Vue 不知道要給誰,就得自己用 $attrs.class 指定。

⚠️ 搭配 Tailwind 的陷阱

<!-- ❌ Tailwind 掃描原始碼時看不到完整 class 名,這些樣式可能根本沒被產生 -->
<div :class="`bg-${color}-500`"></div>

Tailwind 在建置時是用文字掃描找出你用到的 class,它不會執行你的 JavaScript。所以 class 名必須以完整字串出現在原始碼裡:

// ✅ 用對照表,每個 class 都完整寫出來
const colorMap = {
  red: 'bg-red-500',
  blue: 'bg-blue-500'
}
<div :class="colorMap[color]"></div>

這個問題不是 Vue 造成的,但在 Vue + Tailwind 專案裡非常常見:開發時看起來正常(剛好別處有用到同一個 class),上線後樣式卻消失了。

四、:style,行內樣式的綁定

<div :style="{ color: textColor, fontSize: size + 'px' }"></div>
<div :style="{ 'font-size': size + 'px' }"></div>  <!-- kebab-case 要加引號 -->

幾個重點:

  1. 單位不會自動補fontSize: 16 不會變成 16px,要自己加上去。
  2. 可以用陣列合併多個樣式物件:style="[baseStyle, overrideStyle]",後面的會蓋掉前面的。
  3. Vue 會自動補瀏覽器前綴。需要的話也能給多個值,讓瀏覽器挑自己支援的那個:{ display: ['-webkit-box', 'flex'] }
  4. 可以設定 CSS 變數,這個很實用:
<div class="card" :style="{ '--theme-color': themeColor }"></div>
 
<style>
.card {
  border-color: var(--theme-color);
}
</style>

JS 負責提供動態的值,CSS 負責決定怎麼使用這個值,分工更清楚。

五、另一條路:在 <style> 裡用 v-bind()

SFC 還有一個很多人沒注意到的功能:

<script setup>
import { ref } from 'vue'
const themeColor = ref('#42b883')
</script>
 
<template>
  <p class="title">Hello</p>
</template>
 
<style scoped>
.title {
  color: v-bind(themeColor);
}
</style>

它背後的原理正是上一段的 CSS 變數:Vue 會把 themeColor 轉成一個帶 hash 的 CSS 變數,掛在元件根元素的 inline style 上,而且是響應式的。樣式寫在 CSS 裡,值由 JS 控制,兩邊都不用妥協。

六、打開 SFC Playground 看一眼

還記得 Day 2 提到的 SFC Playground 嗎?把這段貼進去:

<div :class="{ active: isActive }">hi</div>

在右邊的 JS 分頁,會看到類似這樣的編譯結果:

_createElementVNode("div", {
  class: _normalizeClass({ active: $setup.isActive })
}, "hi", 2 /* CLASS */)

這裡有兩個值得一看的地方:

  1. normalizeClass:不管你給的是字串、物件還是陣列,最後都會被整理成一個普通的 class 字串。:style 則對應 normalizeStyle
  2. 2 /* CLASS */:這是 patchFlag。編譯器在編譯階段就知道「這個節點只有 class 是動態的」,所以資料變動時,Vue 只比對 class,其他部分都跳過。
    把它跟 Day 3 串起來:isActive 被 Proxy 追蹤 → 值改變 → 觸發元件重新渲染 → patchFlag 告訴 Vue 只需要更新 class。從資料到畫面的整條路,到這裡就接起來了。

小結

  • {{ }} 負責文字內容,是安全的 textContent,不能用在屬性上
  • v-bind 負責屬性,值是 JS 表達式;想移除屬性請給 null,而不是 false
  • :class:style 支援物件與陣列,條件一多就搬進 computed
  • 動態值可以透過 CSS 變數,或 <style> 裡的 v-bind() 交給 CSS 處理
  • 編譯器會用 patchFlag 標記動態部分,讓更新只做必要的事

上一篇
Vue 走過路過不要錯過 Day08 - 改完資料,DOM 為什麼還是舊的?認識 nextTick
系列文
重新認識Vue 走過路過不要錯過9
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言