本文 AI 產出,尚未審核

Vue3 教學:指令(Directives)與 DOM 互動範例(focus、lazyload)


簡介

在 Vue 3 中,**指令(Directives)**是連接 Vue 實例與真實 DOM 的橋樑。雖然大多數情況下我們只會使用內建指令(如 v-ifv-forv-model),但在實務開發中,常會需要自行建立或使用自訂指令來處理 UI 細節,如自動聚焦(focus)或圖片懶加載(lazyload)等。

透過指令,我們可以把 DOM 操作的邏輯抽離,保持模板的簡潔,同時提升程式碼的可重用性與可測試性。本文將以 focuslazyload 為例,介紹如何在 Vue 3 中撰寫、使用與最佳化自訂指令,並說明常見的坑與實務應用場景。


核心概念

1. 指令的生命週期鉤子

Vue 3 為每個指令提供了四個主要的鉤子(hook):

鉤子 說明
created 指令第一次被綁定到元素上時呼叫,尚未掛載到實際 DOM。
beforeMount 元素即將插入父節點前呼叫。
mounted 元素已插入真實 DOM,最常用於操作 DOM。
beforeUpdate / updated 當綁定值變更時呼叫,可用於重新計算。
beforeUnmount / unmounted 元素即將被移除,適合做清理工作(如解除事件監聽)。

Tip:在大多數情況下,我們只需要在 mountedunmounted 兩個階段完成指令的核心功能。

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 中的 foobar)。

程式碼範例

以下提供 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.valueel.dataset 注入。
忘記在 unmounted 清理 IntersectionObserver、事件監聽器未解除,會造成記憶體泄漏。 必須在 unmountedunobserveremoveEventListener
在 SSR 環境使用 document 伺服器端渲染時 document 為 undefined,會拋錯。 使用 if (typeof window !== 'undefined') 包裹 DOM 相關程式。
指令值變更未重新觸發 例如 v-lazyloadsrc 變更但圖片已載入,無法更新。 updated 鉤子裡檢查 binding.value !== binding.oldValue,必要時重新觀察。
過度使用自訂指令 把過多的業務邏輯塞進指令會讓程式碼難以維護。 只把 純粹的 DOM 操作 放在指令,業務邏輯仍建議放在組件或 composable。

其他最佳實踐

  1. 指令命名:使用 v- 前綴的動詞式名稱(如 v-focusv-lazyload),易於閱讀。
  2. 提供 fallback:懶加載時,提供占位圖或 loading="lazy" 作為備援。
  3. 支援修飾符:如 v-lazyload.once 表示只加載一次,可在指令內根據 binding.modifiers 判斷。
  4. 型別檢查:在 TypeScript 專案中,為指令的 binding 加上介面(DirectiveBinding<any>),提升 IDE 提示。
  5. 測試:使用 @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 操作元件邏輯 分離的利器,讓模板保持乾淨、程式碼可重用。
  • 透過 mountedunmounted 等生命週期鉤子,我們可以安全地 聚焦懶加載,同時在 updated 裡處理值變更。
  • focus 指令的實作示例展示了從最簡單的自動聚焦到可條件控制、文字選取的不同需求。
  • lazyload 指令則利用 IntersectionObserver 實現高效的圖片懶加載,並提供支援 <picture> 的進階寫法。
  • 針對常見陷阱(如記憶體泄漏、SSR 錯誤)與最佳實踐(命名、清理、型別檢查),只要遵守這些原則,就能寫出 可維護、效能佳 的自訂指令。

掌握了這兩個核心範例後,你可以更自在地在 Vue 3 專案中擴展自訂指令,解決各種 UI 互動需求,提升使用者體驗與開發效率。祝你寫程式快樂,指令玩得開心! 🎉