本文 AI 產出,尚未審核
Vue3 教學:指令(Directives)與 DOM 互動範例(focus、lazyload)
簡介
在 Vue 3 中,**指令(Directives)**是連接 Vue 實例與真實 DOM 的橋樑。雖然大多數情況下我們只會使用內建指令(如 v-if、v-for、v-model),但在實務開發中,常會需要自行建立或使用自訂指令來處理 UI 細節,如自動聚焦(focus)或圖片懶加載(lazyload)等。
透過指令,我們可以把 DOM 操作的邏輯抽離,保持模板的簡潔,同時提升程式碼的可重用性與可測試性。本文將以 focus 與 lazyload 為例,介紹如何在 Vue 3 中撰寫、使用與最佳化自訂指令,並說明常見的坑與實務應用場景。
核心概念
1. 指令的生命週期鉤子
Vue 3 為每個指令提供了四個主要的鉤子(hook):
| 鉤子 | 說明 |
|---|---|
created |
指令第一次被綁定到元素上時呼叫,尚未掛載到實際 DOM。 |
beforeMount |
元素即將插入父節點前呼叫。 |
mounted |
元素已插入真實 DOM,最常用於操作 DOM。 |
beforeUpdate / updated |
當綁定值變更時呼叫,可用於重新計算。 |
beforeUnmount / unmounted |
元素即將被移除,適合做清理工作(如解除事件監聽)。 |
Tip:在大多數情況下,我們只需要在
mounted與unmounted兩個階段完成指令的核心功能。
2. 註冊方式
- 全域指令:在
app實例上app.directive('my-directive', definition)。全域指令在整個應用中皆可使用。 - 局部指令:在組件的
directives選項中註冊,只在該組件內可用。
// main.js(全域指令)
import { createApp } from 'vue'
import App from './App.vue'
import focusDirective from './directives/focus'
const app = createApp(App)
app.directive('focus', focusDirective) // <input v-focus />
app.mount('#app')
// MyComponent.vue(局部指令)
<script setup>
import lazyloadDirective from '@/directives/lazyload'
</script>
<template>
<img v-lazyload="imageSrc" alt="Lazy Image" />
</template>
<script>
export default {
directives: {
lazyload: lazyloadDirective
}
}
</script>
3. 取得指令參數
指令的 binding 參數提供了以下資訊:
value:指令綁定的值(v-my-directive="xxx"中的xxx)。oldValue:上一次的值(僅在updated時可用)。arg:參數(v-my-directive:foo中的foo)。modifiers:修飾符(v-my-directive.foo.bar中的foo、bar)。
程式碼範例
以下提供 3 個 focus 範例 與 2 個 lazyload 範例,每個範例都包含完整註解與使用方式。
1️⃣ 自動聚焦指令(簡易版)
// directives/focus.js
export default {
// 元素掛載完成後立刻取得焦點
mounted(el) {
// 若元素是可聚焦的(input、textarea、select)
if (['INPUT', 'TEXTAREA', 'SELECT'].includes(el.tagName)) {
el.focus()
}
}
}
使用方式
<input v-focus placeholder="自動聚焦" />
2️⃣ 帶條件的聚焦指令(可控制)
// directives/focus-if.js
export default {
// 當指令值為 true 時聚焦,false 時失焦
mounted(el, binding) {
if (binding.value) {
el.focus()
}
},
updated(el, binding) {
// 當值從 false 變成 true 時才聚焦
if (binding.value && !binding.oldValue) {
el.focus()
}
}
}
使用方式
<input v-focus-if="shouldFocus" /> <!-- 在組件中: --> <script setup> import { ref } from 'vue' const shouldFocus = ref(false) setTimeout(() => { shouldFocus.value = true }, 1000) // 1 秒後自動聚焦 </script>
3️⃣ 聚焦並選取文字(常見需求)
// directives/focus-select.js
export default {
mounted(el, binding) {
// 先聚焦,再全選文字(適用於 input[type="text"])
el.focus()
// 若傳入 true,則自動全選;否則僅聚焦
if (binding.value) {
el.select()
}
}
}
使用方式
<input v-focus-select="true" value="預設文字" />
4️⃣ 圖片懶加載指令(IntersectionObserver 版)
// directives/lazyload.js
export default {
mounted(el, binding) {
// 建立 IntersectionObserver 實例
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
// 替換 src 為實際圖片網址
el.src = binding.value
// 加載完成後解除監聽
observer.unobserve(el)
}
},
{
rootMargin: '0px 0px 200px 0px' // 提前 200px 加載
}
)
// 先設定占位圖或空白
el.src = 'data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg"/>' // 透明占位
observer.observe(el)
// 把 observer 存在 element 上,供 unmounted 時清理
el.__vueLazyObserver__ = observer
},
unmounted(el) {
// 防止記憶體泄漏
if (el.__vueLazyObserver__) {
el.__vueLazyObserver__.unobserve(el)
delete el.__vueLazyObserver__
}
}
}
使用方式
<img v-lazyload="imageUrl" alt="商品圖" />
5️⃣ 支援 <picture> 與多圖來源的懶加載
// directives/lazyload-picture.js
export default {
mounted(el, binding) {
// <picture> 裡的 <source> 需要分別設定 data-srcset
const sources = el.querySelectorAll('source')
sources.forEach(source => {
source.dataset.srcset = source.getAttribute('srcset')
source.removeAttribute('srcset')
})
// 觀察 <img> 本身
const img = el.querySelector('img')
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
// 設定 <source> 的 srcset
sources.forEach(source => {
source.setAttribute('srcset', source.dataset.srcset)
})
// 設定 <img> 的 src
img.src = binding.value
observer.unobserve(el)
}
},
{ rootMargin: '0px 0px 150px 0px' }
)
observer.observe(el)
el.__vueLazyObserver__ = observer
},
unmounted(el) {
if (el.__vueLazyObserver__) {
el.__vueLazyObserver__.unobserve(el)
delete el.__vueLazyObserver__
}
}
}
使用方式
<picture v-lazyload-picture="fallbackImg"> <source data-srcset="small.webp" type="image/webp" /> <source data-srcset="small.jpg" type="image/jpeg" /> <img alt="懶加載圖" /> </picture>
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解法 / Best Practice |
|---|---|---|
| 指令內部直接操作全域變數 | 會破壞組件的可預測性,導致測試困難。 | 把所有依賴都透過 binding.value 或 el.dataset 注入。 |
忘記在 unmounted 清理 |
IntersectionObserver、事件監聽器未解除,會造成記憶體泄漏。 |
必須在 unmounted 中 unobserve、removeEventListener。 |
在 SSR 環境使用 document |
伺服器端渲染時 document 為 undefined,會拋錯。 |
使用 if (typeof window !== 'undefined') 包裹 DOM 相關程式。 |
| 指令值變更未重新觸發 | 例如 v-lazyload 的 src 變更但圖片已載入,無法更新。 |
在 updated 鉤子裡檢查 binding.value !== binding.oldValue,必要時重新觀察。 |
| 過度使用自訂指令 | 把過多的業務邏輯塞進指令會讓程式碼難以維護。 | 只把 純粹的 DOM 操作 放在指令,業務邏輯仍建議放在組件或 composable。 |
其他最佳實踐
- 指令命名:使用
v-前綴的動詞式名稱(如v-focus、v-lazyload),易於閱讀。 - 提供 fallback:懶加載時,提供占位圖或
loading="lazy"作為備援。 - 支援修飾符:如
v-lazyload.once表示只加載一次,可在指令內根據binding.modifiers判斷。 - 型別檢查:在 TypeScript 專案中,為指令的
binding加上介面(DirectiveBinding<any>),提升 IDE 提示。 - 測試:使用
@vue/test-utils搭配 JSDOM 測試指令的掛載與解除行為。
實際應用場景
| 場景 | 為何需要指令? | 範例 |
|---|---|---|
| 表單自動聚焦 | 使用者在表單切換時希望焦點自動移到第一個輸入框,提高使用體驗。 | v-focus-if="isLoginPage" |
| 搜尋框即時聚焦 | 點擊搜尋圖示後,彈出搜尋欄自動取得焦點,避免額外的 ref 操作。 |
v-focus-select="true" |
| 長列表圖片懶加載 | 電商或社群平台的商品/貼文列表,圖片數量龐大,若一次載入會造成卡頓。 | v-lazyload="item.imageUrl" |
| 多媒體播放器的預載 | 音訊或影片列表只在即將播放前才載入,節省頻寬。 | 自訂 v-preload 指令結合 IntersectionObserver。 |
Responsive <picture> |
不同螢幕尺寸需要載入不同解析度的圖片,懶加載同時避免不必要的下載。 | v-lazyload-picture 配合 srcset。 |
總結
- 指令是 Vue 3 中將 DOM 操作 與 元件邏輯 分離的利器,讓模板保持乾淨、程式碼可重用。
- 透過
mounted、unmounted等生命週期鉤子,我們可以安全地 聚焦、懶加載,同時在updated裡處理值變更。 - focus 指令的實作示例展示了從最簡單的自動聚焦到可條件控制、文字選取的不同需求。
- lazyload 指令則利用
IntersectionObserver實現高效的圖片懶加載,並提供支援<picture>的進階寫法。 - 針對常見陷阱(如記憶體泄漏、SSR 錯誤)與最佳實踐(命名、清理、型別檢查),只要遵守這些原則,就能寫出 可維護、效能佳 的自訂指令。
掌握了這兩個核心範例後,你可以更自在地在 Vue 3 專案中擴展自訂指令,解決各種 UI 互動需求,提升使用者體驗與開發效率。祝你寫程式快樂,指令玩得開心! 🎉