Vue3 Composition API(核心)— 在 setup 中使用 Template Refs
簡介
在 Vue 3 中,setup 是 Composition API 的入口點,所有的響應式邏輯都從這裡開始。雖然大多數狀態可以透過 ref、reactive 直接在 JavaScript 中管理,但在實務開發中,我們仍會需要直接操作 DOM 元素或子組件的實例。這時 template refs(模板引用)就派上用場。
template refs 讓我們能夠在模板 (template) 中為元素或組件標註 ref,之後在 setup 中取得對應的引用(Ref<HTMLElement | Component>)。掌握這項技巧,不僅能解決焦點控制、動畫觸發、第三方套件整合等需求,也能讓組件的行為更具彈性與可測試性。
本文將從概念說明、實作範例、常見陷阱與最佳實踐,逐步帶你在 Vue 3 的 setup 中熟練使用 template refs。
核心概念
1. 為什麼需要 Template Refs?
- 直接操作 DOM:如聚焦 (
focus)、滾動 (scrollIntoView)、取得尺寸 (getBoundingClientRect)。 - 呼叫子組件方法:父組件可以透過
ref取得子組件實例,進而呼叫公開的 method。 - 與第三方庫整合:許多 UI 套件(如 Chart.js、Swiper)需要傳入實際的 DOM 節點。
注意:在 Vue 3 中,
ref仍然是「響應式」的概念,而 template refs 則是「非響應式」的 DOM/組件引用。兩者的用途不同,切勿混淆。
2. 在模板中宣告 Ref
<!-- 為原生元素加 ref -->
<input ref="nameInput" type="text" />
<!-- 為子組件加 ref -->
<ChildComponent ref="childRef" />
ref的字串名稱會自動在setup中產生同名的 Ref 物件(Ref<...>)。- 如果同一個模板中出現多個相同名稱的
ref(如v-for),Vue 會把它們收集成 Array of Ref,在setup中會得到Ref<Array<...>>。
3. 在 setup 中取得 Ref
import { ref, onMounted } from 'vue'
export default {
setup() {
// 1. 先宣告同名的 ref 變數(型別必須與實際引用相符)
const nameInput = ref(null) // HTMLElement | null
const childRef = ref(null) // ChildComponent | null
// 2. 在生命週期中使用(例如 onMounted)
onMounted(() => {
// 直接存取 DOM 方法
nameInput.value?.focus()
// 呼叫子組件公開方法
childRef.value?.doSomething()
})
// 3. 回傳給模板使用(若不需要在模板中再次引用,可不回傳)
return {
nameInput,
childRef,
}
},
}
小技巧:
ref(null)的型別推斷會是null,所以在使用前務必加上?.或檢查if (ref.value),避免在 SSR 或未掛載時拋出錯誤。
4. 取得多個相同 Ref(v-for)
<ul>
<li v-for="(item, i) in items" :key="i" ref="listItem">{{ item }}</li>
</ul>
import { ref, onMounted } from 'vue'
export default {
setup() {
const listItem = ref([]) // 會自動變成 Array<HTMLElement>
onMounted(() => {
// 取得所有 li 元素的高度
const heights = listItem.value.map(el => el.getBoundingClientRect().height)
console.log('每個 li 的高度:', heights)
})
return { listItem }
},
}
提醒:
listItem.value在第一次渲染前是空陣列,之後才會被 Vue 填入實際的 DOM 元素。
5. 使用 shallowRef 與 customRef(進階)
有時候我們只想取得 一次 的引用,且不希望 Vue 追蹤其變化。此時可以使用 shallowRef:
import { shallowRef, onMounted } from 'vue'
export default {
setup() {
const chartContainer = shallowRef(null) // 不會深度追蹤
onMounted(() => {
// 假設我們使用 Chart.js
const chart = new Chart(chartContainer.value, { /* config */ })
})
return { chartContainer }
},
}
如果需要自訂 getter / setter 行為(例如在取得時自動 log),可以使用 customRef:
import { customRef, onMounted } from 'vue'
function useLogRef(initialValue = null) {
return customRef((track, trigger) => ({
get() {
track()
console.log('ref 被讀取')
return initialValue
},
set(value) {
console.log('ref 被設定為', value)
initialValue = value
trigger()
},
}))
}
程式碼範例
以下提供 4 個實務中常見的範例,從最基礎到稍微進階,幫助你快速上手。
範例 1:自動聚焦與清除輸入框
<template>
<div>
<input ref="searchInput" placeholder="請輸入關鍵字" @keyup.enter="onSearch" />
<button @click="clear">清除</button>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
const searchInput = ref(null)
// 元件掛載後自動聚焦
onMounted(() => {
searchInput.value?.focus()
})
function onSearch() {
alert(`搜尋關鍵字:${searchInput.value?.value}`)
}
function clear() {
if (searchInput.value) {
searchInput.value.value = ''
searchInput.value.focus()
}
}
</script>
重點:
searchInput.value?.focus()必須在onMounted後才能取得真實的 DOM。
範例 2:呼叫子組件方法(Modal 範例)
<!-- Parent.vue -->
<template>
<button @click="openModal">開啟 Modal</button>
<ModalDialog ref="modalRef" />
</template>
<script setup>
import { ref } from 'vue'
import ModalDialog from './ModalDialog.vue'
const modalRef = ref(null)
function openModal() {
// 子組件必須公開 `open` 方法
modalRef.value?.open()
}
</script>
<!-- ModalDialog.vue -->
<template>
<div v-if="visible" class="modal">我是 Modal <button @click="close">關閉</button></div>
</template>
<script setup>
import { ref, defineExpose } from 'vue'
const visible = ref(false)
function open() {
visible.value = true
}
function close() {
visible.value = false
}
// 讓父層可以透過 ref 呼叫
defineExpose({ open, close })
</script>
技巧:使用
defineExpose明確聲明子組件要公開的 API,避免意外暴露內部實作。
範例 3:整合第三方套件(Swiper 輪播)
<template>
<div class="swiper-container" ref="swiperEl">
<div class="swiper-wrapper">
<div class="swiper-slide" v-for="img in images" :key="img">
<img :src="img" />
</div>
</div>
<!-- 如果需要導航按鈕 -->
<div class="swiper-button-next"></div>
<div class="swiper-button-prev"></div>
</div>
</template>
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue'
import Swiper from 'swiper/bundle'
import 'swiper/css/bundle'
const swiperEl = ref(null)
let swiperInstance = null
const images = [
'https://picsum.photos/id/1015/600/400',
'https://picsum.photos/id/1016/600/400',
'https://picsum.photos/id/1018/600/400',
]
onMounted(() => {
swiperInstance = new Swiper(swiperEl.value, {
loop: true,
navigation: {
nextEl: '.swiper-button-next',
prevEl: '.swiper-button-prev',
},
})
})
onBeforeUnmount(() => {
swiperInstance?.destroy()
})
</script>
要點:
swiperEl使用shallowRef也可以,因為我們只需要一次引用,不必讓 Vue 追蹤其變化。
範例 4:v-for 中的多 Ref(列表動畫)
<template>
<ul>
<li
v-for="(item, idx) in list"
:key="item.id"
ref="listItem"
@click="remove(idx)"
>{{ item.text }}</li>
</ul>
<button @click="add">新增項目</button>
</template>
<script setup>
import { ref, onMounted, nextTick } from 'vue'
const list = ref([
{ id: 1, text: '第一項' },
{ id: 2, text: '第二項' },
])
const listItem = ref([]) // 收集所有 li
function add() {
const newId = Date.now()
list.value.push({ id: newId, text: `第 ${list.value.length + 1} 項` })
// 新增後等待 DOM 更新,再取得最新的高度
nextTick(() => {
console.log('最新 li 高度:', listItem.value.at(-1).offsetHeight)
})
}
function remove(index) {
list.value.splice(index, 1)
}
</script>
說明:
listItem.value會自動同步為所有<li>的陣列,配合nextTick可以在 DOM 完全渲染後取得正確資訊。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解法 |
|---|---|---|
在 setup 中直接使用 ref 而未回傳 |
若在模板中使用 ref="xxx",但 setup 沒有回傳同名的 ref,Vue 仍會自動創建,但 TypeScript 會失去型別提示。 |
最佳實踐:在 setup 中宣告並回傳同名 ref,確保型別安全與 IDE 補全。 |
在 onMounted 前存取 ref.value |
SSR 或組件尚未掛載時,ref.value 為 null,直接呼叫方法會拋錯。 |
使用 ?.、if (ref.value) 或將操作放在 onMounted、onMounted 之後的回呼中。 |
v-for 中的 Ref 變成單一值 |
若同時使用 ref 與 key,Vue 會把所有元素收集成陣列;但若忘記 ref 重名,可能只得到最後一個元素。 |
確認 ref 名稱唯一,並在 setup 中宣告為 ref([]),必要時使用 listRef.value[index] 取得對應元素。 |
忘記 defineExpose |
子組件若未使用 defineExpose,父層仍能取得實例,但無法保證方法可用,且 TypeScript 會失去提示。 |
在子組件中 defineExpose({ methodA, methodB }),讓 API 明確且安全。 |
| 過度依賴 DOM Ref | 把大量 UI 邏輯寫在 DOM 操作裡,會失去 Vue 的響應式優勢,導致維護困難。 | 儘量將狀態放在 ref/reactive 中,僅在需要直接操作 DOM 時才使用 Template Ref(如聚焦、測量尺寸)。 |
最佳實踐小結:
- 先聲明、後使用:在
setup中先用const xxx = ref(null)宣告,再在模板或生命週期裡使用。 - 安全存取:
if (xxx.value) { … }或xxx.value?.method(),避免null錯誤。 - 限制作用域:只在需要的地方使用
ref,不要把所有 DOM 都掛載到setup,保持程式碼乾淨。 - 型別安全:配合 TypeScript 時,使用
Ref<HTMLElement | null>或Ref<ComponentPublicInstance | null>,並在defineExpose中聲明公開方法。
實際應用場景
表單驗證與焦點導向
使用ref在驗證失敗時自動聚焦到錯誤欄位,提高使用者體驗。彈出式對話框(Modal)
父層透過ref呼叫子組件的open/close方法,實現集中管理的 UI。第三方圖表或地圖套件
如 Chart.js、ECharts、Leaflet 等,都需要一個實際的 DOM 容器作為掛載點。自訂動畫與過渡
取得元素尺寸或位置,配合requestAnimationFrame實作精細動畫。動態列表的尺寸測量
例如虛擬滾動(virtual scrolling),需要即時知道每筆資料的高度,以計算滾動位置。
總結
在 Vue 3 的 Composition API 中,setup 為我們提供了更彈性、可組合的開發方式。Template refs 則是連接模板與 JavaScript 的橋樑,讓我們能在保持響應式思維的同時,安全且直接地操作 DOM 或子組件實例。
本文從概念、基本語法、實務範例、常見陷阱與最佳實踐,最後延伸到真實開發情境,完整說明了:
- 為何需要 Template refs
- 如何在模板與
setup中正確宣告與使用 - 多種實務範例(聚焦、子組件方法、第三方套件、
v-for多 Ref) - 防止常見錯誤的技巧與最佳實踐
- 典型的應用場景
掌握這些要點後,你就能在 Vue 3 專案中自如地結合 響應式邏輯 與 DOM 操作,寫出更具可讀性、可維護性的程式碼。祝你在 Vue 的世界裡玩得開心,開發出更優秀的使用者介面! 🚀