本文 AI 產出,尚未審核

Vue3 - 指令(Directives)

全域與區域指令註冊


簡介

在 Vue3 中,指令是直接作用於 DOM 的特殊屬性,讓我們可以在元素的生命週期內插入自訂的行為。雖然 Vue 已內建如 v-ifv-showv-model 等常用指令,但在實務開發中,常會需要自己寫指令來解決 UI 細節(例如自動聚焦、懸浮提示、權限控制等)。

指令的註冊方式分為 全域註冊區域(局部)註冊,兩者各有適用情境。了解何時使用全域、何時使用區域,能讓專案的可維護性、載入效能與命名衝突管理都更佳。本文將從概念說明、實作範例、常見陷阱與最佳實踐,帶你完整掌握 Vue3 指令的註冊與使用。


核心概念

1. 指令的生命週期鉤子

Vue3 的指令提供了以下生命週期鉤子(每個鉤子都會接收到 elbindingvnodeprevNode 四個參數):

鉤子 說明
created 指令被綁定到元素時執行,尚未掛載到實際的 DOM。
beforeMount 即將掛載到 DOM 前呼叫。
mounted 元素已插入 DOM,最常使用的鉤子。
beforeUpdate 來源資料更新前呼叫,可用於比較新舊值
updated 來源資料更新後呼叫,常用來同步 UI
beforeUnmount 元素即將被移除前呼叫,可做清理工作
unmounted 元素已從 DOM 移除,最後的清理時機

Tip:在 Vue3 中,bindinsertedupdatecomponentUpdatedunbind 已被上述鉤子取代,保持 API 統一。


2. 全域指令註冊

全域指令在 整個應用程式 都可以直接使用,註冊方式在 main.js(或 main.ts)中透過 app.directive 完成。

// main.js
import { createApp } from 'vue'
import App from './App.vue'

const app = createApp(App)

// -------------------
// 1. 註冊全域指令 v-focus
// -------------------
app.directive('focus', {
  // 當指令綁定的元素插入到 DOM 時,聚焦該元素
  mounted(el) {
    el.focus()
  }
})

// 2. 註冊全域指令 v-tooltip
app.directive('tooltip', {
  mounted(el, binding) {
    // 建立 tooltip 元素
    const tooltip = document.createElement('div')
    tooltip.className = 'vue-tooltip'
    tooltip.textContent = binding.value
    document.body.appendChild(tooltip)

    // 位置計算
    const show = () => {
      const rect = el.getBoundingClientRect()
      tooltip.style.top = `${rect.bottom + 4}px`
      tooltip.style.left = `${rect.left}px`
      tooltip.style.display = 'block'
    }
    const hide = () => (tooltip.style.display = 'none')

    el.addEventListener('mouseenter', show)
    el.addEventListener('mouseleave', hide)

    // 把 tooltip 實例掛在元素上,方便 later cleanup
    el._tooltip = tooltip
    el._show = show
    el._hide = hide
  },
  unmounted(el) {
    // 清理事件與 DOM
    el.removeEventListener('mouseenter', el._show)
    el.removeEventListener('mouseleave', el._hide)
    document.body.removeChild(el._tooltip)
  }
})

app.mount('#app')

重點:全域指令會在 每一次 createApp 時註冊一次,若套件需要在多個入口點(如微前端)使用,務必確保指令只註冊一次,避免重複覆寫。


3. 區域(局部)指令註冊

區域指令僅在 單一組件 內有效,適合 僅在特定頁面或元件 使用、或是避免全域命名衝突的情況。區域指令寫在組件的 directives 選項裡。

// components/PasswordInput.vue
<template>
  <div class="pwd-wrapper">
    <input
      type="password"
      v-model="password"
      v-show-toggle="isVisible"
      placeholder="請輸入密碼"
    />
    <button @click="isVisible = !isVisible">
      {{ isVisible ? '隱藏' : '顯示' }}
    </button>
  </div>
</template>

<script>
export default {
  name: 'PasswordInput',
  data() {
    return { password: '', isVisible: false }
  },

  // -------------------
  // 1. 區域指令 v-show-toggle
  // -------------------
  directives: {
    // 用來切換 input 的 type 屬性
    showToggle: {
      mounted(el, binding) {
        // 初始狀態
        el.type = binding.value ? 'text' : 'password'
      },
      updated(el, binding) {
        // 當綁定值改變時更新 type
        el.type = binding.value ? 'text' : 'password'
      }
    }
  }
}
</script>

<style scoped>
.pwd-wrapper { display: flex; gap: 8px; }
</style>

Tip:區域指令的名稱 不需要加 v- 前綴,在 template 中仍以 v- 使用(如 v-show-toggle)。


4. 何時使用全域 vs 區域?

判斷條件 建議使用
跨多頁、跨功能共用(如自動聚焦、全站 Tooltip) 全域
僅在單一或少數元件 使用,且可能與其他套件的同名指令衝突 區域
需要根據組件的 props/狀態 動態變化(例如依賴組件內部資料) 區域(或使用 Composable + onMounted
指令較為複雜,需要依賴外部服務或 Vuex 全域(搭配 plugin)或 區域(封裝在 plugin 中)

5. 實作範例彙總

以下提供 5 個常見且實用的指令範例,每個範例皆示範 全域或區域註冊,並附上完整註解說明。

5.1. v-focus(全域)— 自動聚焦

// main.js
app.directive('focus', {
  // 元素插入 DOM 後自動聚焦
  mounted(el) {
    // 若是表單元素才執行
    if (['INPUT', 'TEXTAREA', 'SELECT'].includes(el.tagName)) {
      el.focus()
    }
  }
})

使用方式<input v-focus />
應用情境:登入頁、搜尋框的首位自動聚焦。


5.2. v-debounce(全域)— 防抖輸入

// debounce.js
function debounce(fn, delay) {
  let timer = null
  return function (...args) {
    clearTimeout(timer)
    timer = setTimeout(() => fn.apply(this, args), delay)
  }
}

// main.js
app.directive('debounce', {
  // 只在 input、textarea 上使用
  mounted(el, binding) {
    const delay = Number(binding.arg) || 300 // v-debounce:500
    const event = binding.modifiers.keyup ? 'keyup' : 'input'
    const handler = debounce(event => {
      // 觸發自訂事件,讓父組件可接收
      el.dispatchEvent(new CustomEvent('debounced', { detail: el.value }))
    }, delay)

    el._debounceHandler = handler
    el.addEventListener(event, handler)
  },
  unmounted(el) {
    const event = el._debounceHandler ? 'input' : null
    if (event) el.removeEventListener(event, el._debounceHandler)
  }
})

使用方式

<input v-debounce:500.keyup @debounced="search" />

應用情境:搜尋欄位、即時驗證,減少 API 呼叫次數。


5.3. v-permission(區域)— 權限顯示

// components/PermissionButton.vue
<template>
  <button v-permission="'admin'" @click="doSomething">
    只有管理員可見
  </button>
</template>

<script>
export default {
  directives: {
    permission: {
      // 假設全局有一個 auth 模組
      mounted(el, binding) {
        const required = binding.value // e.g., 'admin'
        const userRoles = window.$store.state.auth.roles // ['user', 'admin']
        if (!userRoles.includes(required)) {
          // 隱藏或移除元素
          el.style.display = 'none'
        }
      }
    }
  },
  methods: {
    doSomething() {
      console.log('執行管理員功能')
    }
  }
}
</script>

說明:此指令僅在 該元件 內生效,避免全站權限邏輯互相干擾。


5.4. v-lazy(全域)— 圖片懶載入

// main.js
app.directive('lazy', {
  // 使用 IntersectionObserver 實作懶載入
  mounted(el, binding) {
    const loadImg = () => {
      el.src = binding.value
      el.classList.add('loaded')
    }

    if ('IntersectionObserver' in window) {
      const observer = new IntersectionObserver((entries, obs) => {
        entries.forEach(entry => {
          if (entry.isIntersecting) {
            loadImg()
            obs.unobserve(el) // 只載入一次
          }
        })
      })
      observer.observe(el)
      el._lazyObserver = observer
    } else {
      // 老舊瀏覽器直接載入
      loadImg()
    }
  },
  unmounted(el) {
    // 清理 observer
    if (el._lazyObserver) {
      el._lazyObserver.unobserve(el)
    }
  }
})

使用方式<img v-lazy="'/imgs/hero.jpg'" alt="Hero">
應用情境:長列表、新聞牆、大圖牆,提升首屏渲染速度。


5.5. v-clipboard(區域)— 點擊複製文字

// components/CopyButton.vue
<template>
  <button v-clipboard="text" @click="onCopy">
    複製
  </button>
</template>

<script>
export default {
  props: { text: { type: String, required: true } },
  directives: {
    clipboard: {
      mounted(el, binding) {
        // 建立隱藏的 textarea 作為 copy 來源
        const textarea = document.createElement('textarea')
        textarea.style.position = 'fixed' // avoid scrolling to bottom
        textarea.style.opacity = '0'
        document.body.appendChild(textarea)
        el._clipboardTarget = textarea
      },
      updated(el, binding) {
        // 每次文字變更時更新 textarea 內容
        el._clipboardTarget.value = binding.value
      },
      unmounted(el) {
        document.body.removeChild(el._clipboardTarget)
      }
    }
  },
  methods: {
    onCopy() {
      const textarea = this.$el._clipboardTarget
      textarea.select()
      document.execCommand('copy')
      this.$emit('copied')
    }
  }
}
</script>

說明:此指令只在 CopyButton 內使用,避免全域污染 document.execCommand 的行為。


常見陷阱與最佳實踐

陷阱 說明 解決方式 / 最佳實踐
命名衝突 多個套件或自行開發的指令使用相同名稱,會互相覆寫。 - 使用 前綴(如 v-app-v-ui-
- 盡量把指令封裝成 plugin,在 install 時統一管理。
記憶體泄漏 mounted 中註冊事件、IntersectionObserver 等,卻忘記在 unmounted 清理。 - 必須在 unmounted(或 beforeUnmount移除所有監聽、Observer、Timeout。
過度使用指令 把大量業務邏輯寫入指令,導致難以追蹤、測試。 - 只把 與 DOM 直接互動 的行為抽成指令。
- 複雜邏輯建議使用 ComposableVuex
指令內部直接操作 Vuex/Pinia 使指令與狀態管理緊耦合,降低重用性。 - 透過 binding.value 傳入需要的函式或資料,保持指令純粹。
忘記 binding.modifiers 想要支援 v-xxx.modifier 卻未檢查 binding.modifiers,導致行為不符。 - 在指令內部 判斷 binding.modifiers,如 if (binding.modifiers.stop)
使用 this 取值錯誤 指令函式不是 Vue 實例方法,this 會是 undefined - 不要在指令內使用 this,改用 binding.instance(Vue3)或傳入函式。

最佳實踐清單

  1. 命名規則:全域指令建議使用 v-app-v-global- 前綴,區域指令則保持簡潔。
  2. 保持單一職責:指令只負責「DOM 操作」與「事件綁定」,業務邏輯交給組件或 composable。
  3. 清理工作:所有在 mounted 中新增的資源(事件、Timer、Observer、DOM)必須在 unmounted 中釋放。
  4. 參數傳遞:使用 binding.valuebinding.argbinding.modifiers,避免硬編碼。
  5. 類型安全(TS 專案):為指令建立 interface,確保 binding 結構正確。
  6. 單元測試:指令可以用 Vue Test Utilsmount 測試 mountedupdatedunmounted 行為。
  7. 文件化:在專案的 docsREADME 中列出所有自訂指令、使用方式與注意事項,提升團隊可讀性。

實際應用場景

場景 適合的指令 為何選擇指令
表單首位自動聚焦 v-focus(全域) 所有表單頁面皆需要,重複使用且簡單。
商品列表懶載入圖片 v-lazy(全域) 效能提升,集中管理 IntersectionObserver,避免每個元件自行寫。
搜尋框防抖 v-debounce(全域) 多處搜尋框都需要相同防抖行為,統一管理可減少錯誤。
權限按鈕顯隱 v-permission(區域) 僅在特定管理介面使用,避免全局權限判斷過於複雜。
即時 Tooltip v-tooltip(全域) 多個元件都會顯示說明文字,統一樣式與行為。
複製到剪貼簿 v-clipboard(區域) 只在「分享」或「設定」頁面使用,保持指令輕量。
自訂滾動偵測(Infinite Scroll) v-infinite-scroll(全域) 整站大量列表皆需要,封裝成全域指令最適合。

案例示範:假設我們有一個「商品搜尋」頁面,需要同時使用 v-focusv-debouncev-tooltip。全域註冊讓我們只在 main.js 中寫一次,頁面模板則保持乾淨:

<!-- SearchPage.vue -->
<template>
  <div class="search-page">
    <input
      v-focus
      v-debounce:400
      v-tooltip="'輸入商品關鍵字'"
      v-model="keyword"
      @debounced="fetchProducts"
      placeholder="搜尋商品..."
    />
    <ul>
      <li v-for="item in list" :key="item.id">{{ item.name }}</li>
    </ul>
  </div>
</template>

<script>
export default {
  data() {
    return { keyword: '', list: [] }
  },
  methods: {
    fetchProducts() {
      // 呼叫 API,依據 this.keyword 取得結果
    }
  }
}
</script>

總結

  • 指令是 Vue3 中直接操作 DOM 的利器,能把 UI 細節抽離成可重用的模組。
  • 全域指令適合跨頁面、跨功能的共用行為(如 v-focusv-tooltip),但要注意 命名衝突一次性註冊
  • 區域指令則適合僅在單一元件使用的特殊需求(如權限顯示、局部 debounce),能避免全域汙染並提升可維護性。
  • 正確使用 生命週期鉤子清理資源參數化,並遵守 單一職責 原則,才能寫出 高效、可測、易維護 的自訂指令。

掌握了全域與區域指令的註冊與最佳實踐後,你就能在 Vue3 專案中自由地擴充 UI 行為,讓程式碼既乾淨功能強大。祝開發順利 🚀!