本文 AI 產出,尚未審核

Vue3 Composition API(核心)— 在 setup 中使用 Template Refs


簡介

在 Vue 3 中,setup 是 Composition API 的入口點,所有的響應式邏輯都從這裡開始。雖然大多數狀態可以透過 refreactive 直接在 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. 使用 shallowRefcustomRef(進階)

有時候我們只想取得 一次 的引用,且不希望 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.valuenull,直接呼叫方法會拋錯。 使用 ?.if (ref.value) 或將操作放在 onMountedonMounted 之後的回呼中。
v-for 中的 Ref 變成單一值 若同時使用 refkey,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(如聚焦、測量尺寸)。

最佳實踐小結

  1. 先聲明、後使用:在 setup 中先用 const xxx = ref(null) 宣告,再在模板或生命週期裡使用。
  2. 安全存取if (xxx.value) { … }xxx.value?.method(),避免 null 錯誤。
  3. 限制作用域:只在需要的地方使用 ref,不要把所有 DOM 都掛載到 setup,保持程式碼乾淨。
  4. 型別安全:配合 TypeScript 時,使用 Ref<HTMLElement | null>Ref<ComponentPublicInstance | null>,並在 defineExpose 中聲明公開方法。

實際應用場景

  1. 表單驗證與焦點導向
    使用 ref 在驗證失敗時自動聚焦到錯誤欄位,提高使用者體驗。

  2. 彈出式對話框(Modal)
    父層透過 ref 呼叫子組件的 open/close 方法,實現集中管理的 UI。

  3. 第三方圖表或地圖套件
    如 Chart.js、ECharts、Leaflet 等,都需要一個實際的 DOM 容器作為掛載點。

  4. 自訂動畫與過渡
    取得元素尺寸或位置,配合 requestAnimationFrame 實作精細動畫。

  5. 動態列表的尺寸測量
    例如虛擬滾動(virtual scrolling),需要即時知道每筆資料的高度,以計算滾動位置。


總結

在 Vue 3 的 Composition API 中,setup 為我們提供了更彈性、可組合的開發方式。Template refs 則是連接模板與 JavaScript 的橋樑,讓我們能在保持響應式思維的同時,安全且直接地操作 DOM 或子組件實例。

本文從概念、基本語法、實務範例、常見陷阱與最佳實踐,最後延伸到真實開發情境,完整說明了:

  • 為何需要 Template refs
  • 如何在模板與 setup 中正確宣告與使用
  • 多種實務範例(聚焦、子組件方法、第三方套件、v-for 多 Ref)
  • 防止常見錯誤的技巧與最佳實踐
  • 典型的應用場景

掌握這些要點後,你就能在 Vue 3 專案中自如地結合 響應式邏輯DOM 操作,寫出更具可讀性、可維護性的程式碼。祝你在 Vue 的世界裡玩得開心,開發出更優秀的使用者介面! 🚀