Vue3 - 指令(Directives)
全域與區域指令註冊
簡介
在 Vue3 中,指令是直接作用於 DOM 的特殊屬性,讓我們可以在元素的生命週期內插入自訂的行為。雖然 Vue 已內建如 v-if、v-show、v-model 等常用指令,但在實務開發中,常會需要自己寫指令來解決 UI 細節(例如自動聚焦、懸浮提示、權限控制等)。
指令的註冊方式分為 全域註冊 與 區域(局部)註冊,兩者各有適用情境。了解何時使用全域、何時使用區域,能讓專案的可維護性、載入效能與命名衝突管理都更佳。本文將從概念說明、實作範例、常見陷阱與最佳實踐,帶你完整掌握 Vue3 指令的註冊與使用。
核心概念
1. 指令的生命週期鉤子
Vue3 的指令提供了以下生命週期鉤子(每個鉤子都會接收到 el、binding、vnode、prevNode 四個參數):
| 鉤子 | 說明 |
|---|---|
created |
指令被綁定到元素時執行,尚未掛載到實際的 DOM。 |
beforeMount |
即將掛載到 DOM 前呼叫。 |
mounted |
元素已插入 DOM,最常使用的鉤子。 |
beforeUpdate |
來源資料更新前呼叫,可用於比較新舊值。 |
updated |
來源資料更新後呼叫,常用來同步 UI。 |
beforeUnmount |
元素即將被移除前呼叫,可做清理工作。 |
unmounted |
元素已從 DOM 移除,最後的清理時機。 |
Tip:在 Vue3 中,
bind、inserted、update、componentUpdated、unbind已被上述鉤子取代,保持 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 直接互動 的行為抽成指令。 - 複雜邏輯建議使用 Composable 或 Vuex。 |
| 指令內部直接操作 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)或傳入函式。 |
最佳實踐清單
- 命名規則:全域指令建議使用
v-app-或v-global-前綴,區域指令則保持簡潔。 - 保持單一職責:指令只負責「DOM 操作」與「事件綁定」,業務邏輯交給組件或 composable。
- 清理工作:所有在
mounted中新增的資源(事件、Timer、Observer、DOM)必須在unmounted中釋放。 - 參數傳遞:使用
binding.value、binding.arg、binding.modifiers,避免硬編碼。 - 類型安全(TS 專案):為指令建立 interface,確保
binding結構正確。 - 單元測試:指令可以用 Vue Test Utils 的
mount測試mounted、updated、unmounted行為。 - 文件化:在專案的
docs或README中列出所有自訂指令、使用方式與注意事項,提升團隊可讀性。
實際應用場景
| 場景 | 適合的指令 | 為何選擇指令 |
|---|---|---|
| 表單首位自動聚焦 | v-focus(全域) |
所有表單頁面皆需要,重複使用且簡單。 |
| 商品列表懶載入圖片 | v-lazy(全域) |
效能提升,集中管理 IntersectionObserver,避免每個元件自行寫。 |
| 搜尋框防抖 | v-debounce(全域) |
多處搜尋框都需要相同防抖行為,統一管理可減少錯誤。 |
| 權限按鈕顯隱 | v-permission(區域) |
僅在特定管理介面使用,避免全局權限判斷過於複雜。 |
| 即時 Tooltip | v-tooltip(全域) |
多個元件都會顯示說明文字,統一樣式與行為。 |
| 複製到剪貼簿 | v-clipboard(區域) |
只在「分享」或「設定」頁面使用,保持指令輕量。 |
| 自訂滾動偵測(Infinite Scroll) | v-infinite-scroll(全域) |
整站大量列表皆需要,封裝成全域指令最適合。 |
案例示範:假設我們有一個「商品搜尋」頁面,需要同時使用
v-focus、v-debounce、v-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-focus、v-tooltip),但要注意 命名衝突與 一次性註冊。 - 區域指令則適合僅在單一元件使用的特殊需求(如權限顯示、局部 debounce),能避免全域汙染並提升可維護性。
- 正確使用 生命週期鉤子、清理資源、參數化,並遵守 單一職責 原則,才能寫出 高效、可測、易維護 的自訂指令。
掌握了全域與區域指令的註冊與最佳實踐後,你就能在 Vue3 專案中自由地擴充 UI 行為,讓程式碼既乾淨又功能強大。祝開發順利 🚀!