Skip to content

模板語法與指令

重新認識 Vue.js: 008 天絕對看不完的 Vue.js 3.x 指南

上一節我們用 {{ }} 把資料顯示到畫面上。不過實際開發時,畫面不只需要顯示文字,也會需要綁定屬性、切換 class、套用 style,甚至在少數情境中輸出 HTML。這些需求都會透過 Vue 的模板語法與指令完成。

什麼是指令?

Vue 指令是寫在模板上的特殊屬性,通常以 v- 開頭。它們會把 DOM 行為和響應式資料連起來。

vue
<script setup>
import { ref } from 'vue'

const imageUrl = ref('https://vuejs.org/images/logo.png')
const imageAlt = ref('Vue logo')
</script>

<template>
  <main class="app">
    <img :src="imageUrl" :alt="imageAlt" class="logo">
  </main>
</template>

<style scoped>
.app {
  padding: 2rem;
}

.logo {
  width: 96px;
  height: 96px;
}
</style>

這裡的 :src:altv-bind:srcv-bind:alt 的縮寫。當 imageUrlimageAlt 改變時,對應的 HTML 屬性也會跟著更新。

指令的基本形狀

指令通常可以拆成幾個部分:

html
<a v-bind:href="url">官方網站</a>
<button v-on:click="submit">送出</button>
  • v-bindv-on 是指令名稱。
  • hrefclick 是參數,代表這個指令作用在哪個屬性或事件上。
  • urlsubmit 是 JavaScript 表達式,會在目前元件的資料範圍內求值。

常用縮寫如下:

完整寫法縮寫用途
v-bind:href="url":href="url"綁定 HTML 屬性
v-on:click="submit"@click="submit"監聽 DOM 事件

綁定 HTML 屬性

文字插值 {{ }} 只能放在標籤內容裡,不能塞進 HTML 屬性。下面前兩行看起來像是在使用變數,其實都不會得到想要的結果:

html
<!-- 錯誤:屬性裡不能使用文字插值 -->
<img src="{{ imageUrl }}">

<!-- 固定字串:瀏覽器會真的請求名為 imageUrl 的路徑 -->
<img src="imageUrl">

<!-- 完整寫法:讀取 JavaScript 的 imageUrl -->
<img v-bind:src="imageUrl">

<!-- 縮寫:與上一行完全相同 -->
<img :src="imageUrl">

v-bind:src="imageUrl" 可以拆成三個部分:v-bind 是指令、src 是要綁定的屬性、imageUrl 是 JavaScript 表達式。縮寫時,v-bind: 整段濃縮成一個冒號,因此 :src 不是另一個新指令,而是同一件事的短寫法。

引號裡也不只限於變數名稱,可以放一段會算出值的 JavaScript 表達式。更重要的是,只要表達式讀到的響應式資料改變,Vue 就會重新計算,並把結果同步到元素的屬性。下面用庫存狀態同時控制一般屬性與布林屬性:

vue
<script setup>
import { ref } from 'vue'

const isSoldOut = ref(false)

function toggleStock() {
  isSoldOut.value = !isSoldOut.value
}
</script>

<template>
  <main class="app">
    <p :title="isSoldOut ? '這本書目前缺貨' : null">
      庫存狀態:{{ isSoldOut ? '已售完' : '尚有庫存' }}
    </p>

    <div class="actions">
      <button type="button" @click="toggleStock">
        切換庫存狀態
      </button>
      <button type="button" :disabled="isSoldOut">
        加入購物車
      </button>
    </div>
  </main>
</template>

<style scoped>
.app {
  padding: 2rem;
}

.actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.75rem;
}

.actions button:disabled {
  cursor: not-allowed;
  opacity: 0.5;
}
</style>
試一試

庫存狀態:尚有庫存

切換成「已售完」後,isSoldOut 變成 true,Vue 會同時完成兩件事:

  • :title 的表達式得到字串,因此元素會出現 title="這本書目前缺貨";切回 null 時,Vue 會移除這個屬性。綁定值是 undefined 時也一樣會移除。
  • disabled 是布林屬性。:disabled 得到真值時,Vue 會保留 disabled;得到假值時則移除,所以「加入購物車」按鈕會跟著啟用或停用。

這個範例也把 v-bindv-on 的方向放在一起了:

語法資料流向範例
v-bind:JavaScript 狀態 → 元素屬性:disabled="isSoldOut"
v-on@元素事件 → JavaScript 函式@click="toggleStock"

元件標籤也沿用同一組符號::prop 把資料傳入、@event 接收事件;第三章會直接建立在這個讀法上。

同名縮寫(Vue 3.4 新增)

如果屬性名稱與變數名稱相同,Vue 3.4 起可以再省略等號右邊的值:

vue
<script setup>
import { ref } from 'vue'

const id = ref('featured-book')
</script>

<template>
  <!-- 等同於 :id="id" -->
  <article :id>本週精選</article>
</template>

這跟 JavaScript 物件的同名屬性簡寫很像。第一次閱讀別人的程式碼時若看到 :id 後面沒有值,不是漏寫,而是把同名變數 id 綁上去。

一次綁定多個屬性

如果多個屬性已經整理在同一個物件裡,可以使用沒有參數的 v-bind 一次綁定:

vue
<script setup>
const linkAttrs = {
  href: 'https://vuejs.org/',
  target: '_blank',
  rel: 'noreferrer'
}
</script>

<template>
  <a v-bind="linkAttrs">Vue 官方網站</a>
</template>

這裡不能縮寫成單獨的 :,因為冒號後面沒有單一屬性名稱。linkAttrs 的每個 key 會成為屬性名稱,value 則是對應的屬性值。

綁定 class

class 本身也是 HTML 屬性,所以 :class 就是 v-bind:class。由於 class 常需要同時切換多個名稱,Vue 另外提供物件與陣列語法;先看物件寫法:

vue
<script setup>
import { ref } from 'vue'

const isActive = ref(true)
const hasError = ref(false)

function toggleActive() {
  isActive.value = !isActive.value
}
</script>

<template>
  <main class="app">
    <p
      class="message"
      :class="{ active: isActive, error: hasError }"
    >
      這段文字會依狀態切換 class。
    </p>

    <button type="button" @click="toggleActive">
      切換 active
    </button>
  </main>
</template>

<style scoped>
.app {
  padding: 2rem;
}

.message {
  color: inherit;
}

.active {
  font-weight: 700;
  text-decoration: underline;
  text-decoration-thickness: 0.15em;
}

.error {
  color: #b91c1c;
}
</style>
試一試

這段文字會依狀態切換 class。

active: isActive 的意思是:當 isActive 為真,就加上 active class;否則移除它。

綁定 style

style 同樣是屬性,:style 就是 v-bind:style。行內樣式可以透過物件綁定,建議使用 camelCase 屬性名稱,和 JavaScript 物件一致:

vue
<script setup>
import { ref } from 'vue'

const fontSize = ref(18)
const themeColor = ref('#2563eb')

function enlarge() {
  fontSize.value += 2
}
</script>

<template>
  <main class="app">
    <p :style="{ fontSize: `${fontSize}px`, color: themeColor }">
      這段文字會變大。
    </p>

    <button type="button" @click="enlarge">
      放大
    </button>
  </main>
</template>

<style scoped>
.app {
  padding: 2rem;
}
</style>

若 class 或 style 的邏輯開始變複雜,通常會在 <script setup> 裡用 computed 整理,避免把判斷塞滿模板。computed 會在本章最後一節介紹。

動態參數

有時候,連要綁定的屬性名稱也可能來自資料。這時可以使用動態參數:

vue
<script setup>
import { ref } from 'vue'

const attributeName = ref('href')
const linkValue = ref('https://vuejs.org/')
</script>

<template>
  <main class="app">
    <a :[attributeName]="linkValue" target="_blank" rel="noreferrer">
      Vue 官方網站
    </a>
  </main>
</template>

<style scoped>
.app {
  padding: 2rem;
}
</style>

:[attributeName] 會把 attributeName 的值當作屬性名稱。這種寫法平常用不太到,但在寫彈性較高的元件或工具時會派上用場。

輸出 HTML:v-html

一般文字插值會自動跳脫 HTML,避免字串被當成真正的標籤執行。若確定內容來源可信,可以用 v-html 輸出 HTML:

vue
<script setup>
import { ref } from 'vue'

const trustedHtml = ref('<strong>這段文字會以粗體顯示</strong>')
</script>

<template>
  <main class="app">
    <p v-html="trustedHtml"></p>
  </main>
</template>

<style scoped>
.app {
  padding: 2rem;
}
</style>

注意:不要用 v-html 顯示未信任內容

v-html 會把字串當作 HTML 插進頁面。如果內容來自使用者輸入、外部 API 或任何未經清理的來源,就可能造成 XSS 風險。

一般情況下,優先使用 {{ }} 顯示文字;只有在來源可信且確定需要 HTML 時,才使用 v-html

其他內建指令

下面幾個指令不一定一開始就會用到,但先知道它們的定位即可:

指令用途
v-once只渲染一次,後續資料變化不再更新該區塊。
v-pre跳過該元素與子元素的 Vue 編譯,常用於顯示原始 Mustache 語法。
v-cloak搭配 CSS 隱藏尚未完成編譯的模板,較常見於非建置工具情境。
v-memo記憶模板子樹,只有指定依賴改變時才重新更新。

模板語法先介紹到這裡。下一節,我們回頭處理前面已經出現好幾次的 @click,再把表單需要的 v-model 補齊。