Appearance
模板語法與指令
上一節我們用 {{ }} 把資料顯示到畫面上。不過實際開發時,畫面不只需要顯示文字,也會需要綁定屬性、切換 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 與 :alt 是 v-bind:src、v-bind:alt 的縮寫。當 imageUrl 或 imageAlt 改變時,對應的 HTML 屬性也會跟著更新。
指令的基本形狀
指令通常可以拆成幾個部分:
html
<a v-bind:href="url">官方網站</a>
<button v-on:click="submit">送出</button>v-bind、v-on是指令名稱。href、click是參數,代表這個指令作用在哪個屬性或事件上。url、submit是 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-bind 與 v-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>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 補齊。
