欢迎光临
我们一直在努力

Vue3+TypeScript 自定义指令

 全局注册示例

1、创建指令 + 统一导出

src\\directives\\totalInputDirective.ts

import type { Directive } from "vue";

// 使用 WeakMap 存储事件处理器,避免直接在 DOM 元素上添加自定义属性
const handlerMap = new WeakMap<HTMLInputElement, (e: Event) => void>();

// 格式化输入的金额,需定义在指令外部
const formatTotalInput = (value: string): string => {
let v = value;

// 如果值为空,设置为空字符串
if (!v) {
return "";
}

// 1. 只允许数字、小数点、负号
v = v.replace(/[^\\d.-]/g, "");

// 2. 处理负号:只能出现在开头,且只出现一次
let hasNegative = false;
if (v.includes("-")) {
// 检查负号是否在开头
if (v[0] === "-") {
hasNegative = true;
// 保留开头的负号,移除其他位置的负号
v = "-" + v.slice(1).replace(/-/g, "");
} else {
// 负号不在开头,移除所有负号
v = v.replace(/-/g, "");
}
}

// 3. 处理单独的小数点
if (v === ".") v = "0.";
if (v === "-.") v = "-0.";

// 4. 处理前导零:去除整数部分的前导零,但保留小数点和负数的情况
if (hasNegative) {
v = v.replace(/^(-?)0+(\\d)/, "$1$2");
} else {
v = v.replace(/^0+(\\d)/, "$1");
}

// 5. 去除多余的小数点(只保留第一个)
const firstDot = v.indexOf(".");
if (firstDot !== -1) {
const beforeDot = v.substring(0, firstDot + 1);
const afterDot = v.substring(firstDot + 1).replace(/\\./g, "");
v = beforeDot + afterDot;
}

// 6. 限制小数点后最多两位
const dotIndex = v.indexOf(".");
if (dotIndex !== -1) {
const integerPart = v.substring(0, dotIndex);
const decimalPart = v.substring(dotIndex + 1, dotIndex + 3);
v = integerPart + "." + decimalPart;
}

// 7. 特殊情况处理:如果只有负号或负号加0,保留它们

// 8. 处理以0开头的非小数情况
if (v.length > 1 && v[0] === "0" && v[1] !== ".") {
v = v.substring(1);
}

// 9. 返回格式化后的输入内容
return v;
};

/**
* 自定义指令:金额输入指令,只能输入数字、负号和小数点,最多两位小数,不能0开头,不能小数点开头,只能开头输入负号,只能输入一次负号和小数点
*/
export const totalInputDirective: Directive = {
/**
* 指令挂载到元素上时的钩子函数
* @param el – 指令绑定的DOM元素
*/
mounted(el: HTMLElement | HTMLInputElement) {
// 找到 input 元素
let input: HTMLInputElement | null = null;

if (el.tagName === "INPUT") {
input = el as HTMLInputElement;
} else {
input = el.querySelector && el.querySelector("input");
}

if (!input) return;

// 处理输入事件的函数
const handler = (e: Event) => {
const target = e.target as HTMLInputElement;

// 获取输入的值
const inputValue = target.value;

const value = formatTotalInput(inputValue);

if (value !== target.value) {
const oldValue = target.value;
target.value = value;

// 如果值发生了变化,触发input事件以确保Vue的数据绑定更新
if (oldValue !== value) {
target.dispatchEvent(new Event("input", { bubbles: true }));
}
}
};

// 监听输入事件
input.addEventListener("input", handler);

// 使用 WeakMap 存储处理器引用,以便后续移除
handlerMap.set(input, handler);
},

/**
* 指令从元素解绑时的钩子函数
* 清理事件监听器以避免内存泄漏
* @param el – 指令绑定的DOM元素
*/
unmounted(el: HTMLElement | HTMLInputElement) {
let input: HTMLInputElement | null = null;

if (el.tagName === "INPUT") {
input = el as HTMLInputElement;
} else {
input = el.querySelector && el.querySelector("input");
}

if (input) {
const storedHandler = handlerMap.get(input);
if (storedHandler) {
// 移除事件监听器以防止内存泄漏
input.removeEventListener("input", storedHandler);
handlerMap.delete(input);
}
}
}
};

src\\directives\\index.ts

export * from "./totalInputDirective";

2、注册指令

2.1、全局注册指令(导入指令 + 安装指令)

src\\main.ts

// 使用命名导入,全局引入自定义指令:金额输入指令
import { totalInputDirective } from "@/directives";

// 创建 Vue 应用实例
const app = createApp(App);

// 安装自定义指令,金额输入指令 totalInput,在模板中使用 v-total-input 指令
app.directive("totalInput", totalInputDirective);

// 将 Vue 应用实例挂载到 DOM 中的一个指定容器上,从而启动 Vue 应用
app.mount("#app");

2.2、局部注册指令(导入指令 + 定义指令)

src\\views\\capital\\CapitalInfo.vue

import { totalInputDirective } from "@/directives";

// 定义指令:金额输入指令,规范以v开头,在模板中使用 v-total-input 指令
const vTotalInput = totalInputDirective ;

3、使用指令

src\\views\\capital\\CapitalInfo.vue

<!– 方法4:自定义指令 + @blur –>
<el-input v-model="localCapitalInfo.capitalTotal" v-total-input @blur="handleTotalBlur" clearable />


局局注册示例

代码:

1、创建指令 + 统一导出

src\\directives\\yearInputDirective.ts

import type { Directive } from "vue";

// 使用 WeakMap 存储事件处理器,避免直接在 DOM 元素上添加自定义属性
const handlerMap = new WeakMap<HTMLInputElement, (e: Event) => void>();

/**
* 自定义指令:年份输入指令,最多只能输入四位数字,不能输入其他符号
* 该指令限制输入框只能输入最多4位数字字符,通常用于年份输入场景
*/
export const yearInputDirective: Directive = {
/**
* 指令挂载到元素上时的钩子函数
* @param el – 指令绑定的DOM元素
*/
mounted(el: HTMLElement | HTMLInputElement) {
// 找到 input 元素
let input: HTMLInputElement | null = null;

if (el.tagName === "INPUT") {
input = el as HTMLInputElement;
} else {
input = el.querySelector && el.querySelector("input");
}

if (!input) return;

// 设置最大长度为4位数字
const maxLength = 4;

// 处理输入事件的函数,过滤掉非数字字符并限制长度
const handler = (e: Event) => {
const target = e.target as HTMLInputElement;

// 移除非数字字符(\\D表示非数字),然后截取前maxLength个字符
const value = target.value.replace(/\\D/g, "").slice(0, maxLength);

if (value !== target.value) {
const oldValue = target.value;
target.value = value;

// 如果值发生了变化,触发input事件以确保Vue的数据绑定更新
if (oldValue !== value) {
target.dispatchEvent(new Event("input", { bubbles: true }));
}
}
};

// 监听输入事件
input.addEventListener("input", handler);

// 使用 WeakMap 存储处理器引用,以便后续移除
handlerMap.set(input, handler);
},

/**
* 指令从元素解绑时的钩子函数
* 清理事件监听器以避免内存泄漏
* @param el – 指令绑定的DOM元素
*/
unmounted(el: HTMLElement | HTMLInputElement) {
let input: HTMLInputElement | null = null;

if (el.tagName === "INPUT") {
input = el as HTMLInputElement;
} else {
input = el.querySelector && el.querySelector("input");
}

if (input) {
const storedHandler = handlerMap.get(input);
if (storedHandler) {
// 移除事件监听器以防止内存泄漏
input.removeEventListener("input", storedHandler);
handlerMap.delete(input);
}
}
}
};

src\\directives\\index.ts

export * from "./yearInputDirective";

 2、注册指令

2.1、全局注册指令(导入指令 + 安装指令)

src\\main.ts

// 使用命名导入,全局引入自定义指令:年份输入指令
import { yearInputDirective } from "@/directives";

// 创建 Vue 应用实例
const app = createApp(App);

// 安装自定义指令,年份输入指令 yearInput,在模板中使用 v-year-input 指令
app.directive("yearInput", yearInputDirective);

// 将 Vue 应用实例挂载到 DOM 中的一个指定容器上,从而启动 Vue 应用
app.mount("#app");

2.2、局部注册指令(导入指令 + 定义指令)

src\\views\\capital\\CapitalInfo.vue

import { yearInputDirective } from "@/directives";

// 定义指令:年份输入指令,规范以v开头,在模板中使用 v-year-input 指令
const vYearInput = yearInputDirective;

3、使用指令

src\\views\\capital\\CapitalInfo.vue

<el-row :gutter="10">
<el-col :span="24">
<el-form-item label="资金年份" label-position="right">
<el-input v-model="capitalInfoStore.queryDTO.capitalYear" clearable v-year-input />
</el-form-item>
</el-col>
</el-row>


Vue3 + TypeScript 自定义指令完整教程

一、基础环境设置

1. 确保 TypeScript 配置正确

json

// tsconfig.json
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"types": ["vue", "vite/client"],
"strict": true,
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"esModuleInterop": true,
"noEmit": true,
"skipLibCheck": true,
"noUnusedLocals": true,
"noUnusedParameters": true
},
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.vue"]
}

二、创建自定义指令的完整流程

1. 创建基础指令(带完整类型)

typescript

// src/directives/NumberOnlyDirective.ts
import type { Directive, DirectiveBinding } from 'vue'

// 1. 定义指令参数类型
interface NumberOnlyOptions {
maxLength?: number
min?: number
max?: number
allowNegative?: boolean
allowDecimal?: boolean
}

// 2. 定义指令存储的扩展类型
declare global {
interface HTMLElement {
_numberOnlyHandler?: (e: Event) => void
_numberOnlyOptions?: NumberOnlyOptions
}
}

// 3. 创建指令
export const NumberOnlyDirective: Directive<HTMLElement, NumberOnlyOptions | number | undefined> = {
/**
* 元素挂载时调用
* @param el – 绑定的元素
* @param binding – 指令绑定信息
*/
mounted(el: HTMLElement, binding: DirectiveBinding<NumberOnlyOptions | number | undefined>) {
// 解析配置
const options = parseOptions(binding.value)

// 找到 input 元素
const input = findInputElement(el)
if (!input) {
console.warn('NumberOnlyDirective: 未找到 input 元素')
return
}

// 创建处理函数
const handler = createInputHandler(input, options)

// 存储引用
el._numberOnlyHandler = handler
el._numberOnlyOptions = options

// 绑定事件
input.addEventListener('input', handler)
input.addEventListener('blur', () => validateValue(input, options))

// 初始格式化
formatInputValue(input, options)
},

/**
* 参数更新时调用
*/
updated(el: HTMLElement, binding: DirectiveBinding<NumberOnlyOptions | number | undefined>) {
const newOptions = parseOptions(binding.value)
const input = findInputElement(el)

if (input && el._numberOnlyHandler) {
// 更新选项
el._numberOnlyOptions = newOptions

// 重新格式化当前值
formatInputValue(input, newOptions)
}
},

/**
* 元素卸载时调用
*/
unmounted(el: HTMLElement) {
const input = findInputElement(el)

if (input && el._numberOnlyHandler) {
// 移除事件监听
input.removeEventListener('input', el._numberOnlyHandler)

// 清理存储
delete el._numberOnlyHandler
delete el._numberOnlyOptions
}
}
}

// 辅助函数
function parseOptions(value: NumberOnlyOptions | number | undefined): NumberOnlyOptions {
const defaultOptions: NumberOnlyOptions = {
maxLength: 4,
min: 0,
max: 9999,
allowNegative: false,
allowDecimal: false
}

if (typeof value === 'number') {
return { …defaultOptions, maxLength: value }
}

if (typeof value === 'object' && value !== null) {
return { …defaultOptions, …value }
}

return defaultOptions
}

function findInputElement(el: HTMLElement): HTMLInputElement | null {
// 处理原生 input
if (el.tagName.toLowerCase() === 'input') {
return el as HTMLInputElement
}

// 处理 Element Plus 的 el-input
const input = el.querySelector('input')
if (input) return input

// 处理 Element Plus 的特殊类名
const elInput = el.querySelector('.el-input__inner')
return elInput as HTMLInputElement
}

function createInputHandler(input: HTMLInputElement, options: NumberOnlyOptions): (e: Event) => void {
return (e: Event) => {
const target = e.target as HTMLInputElement
const cursorPos = target.selectionStart || 0
const oldValue = target.value

// 根据配置过滤值
let newValue = filterValue(target.value, options)

// 限制长度
if (options.maxLength) {
newValue = newValue.slice(0, options.maxLength)
}

// 如果值发生变化
if (newValue !== oldValue) {
// 更新值
target.value = newValue

// 调整光标位置
const newCursorPos = calculateNewCursorPosition(oldValue, newValue, cursorPos)
target.setSelectionRange(newCursorPos, newCursorPos)

// 触发 Vue 更新
triggerVueUpdate(target)
}
}
}

function filterValue(value: string, options: NumberOnlyOptions): string {
// 构建正则表达式
let pattern = '0-9'

if (options.allowNegative) {
pattern = '-0-9'
}

if (options.allowDecimal) {
pattern += '.'
}

const regex = new RegExp(`[^${pattern}]`, 'g')
return value.replace(regex, '')
}

function calculateNewCursorPosition(oldValue: string, newValue: string, cursorPos: number): number {
const lengthDiff = newValue.length – oldValue.length
return Math.max(0, Math.min(cursorPos + lengthDiff, newValue.length))
}

function formatInputValue(input: HTMLInputElement, options: NumberOnlyOptions) {
const currentValue = input.value
const filteredValue = filterValue(currentValue, options)

if (filteredValue !== currentValue) {
input.value = filteredValue
triggerVueUpdate(input)
}
}

function validateValue(input: HTMLInputElement, options: NumberOnlyOptions) {
const value = input.value.trim()

if (!value) return

const numValue = parseFloat(value)

if (isNaN(numValue)) {
input.value = ''
triggerVueUpdate(input)
return
}

if (options.min !== undefined && numValue < options.min) {
input.value = options.min.toString()
triggerVueUpdate(input)
}

if (options.max !== undefined && numValue > options.max) {
input.value = options.max.toString()
triggerVueUpdate(input)
}
}

function triggerVueUpdate(input: HTMLInputElement) {
input.dispatchEvent(new Event('input', { bubbles: true }))
input.dispatchEvent(new Event('change', { bubbles: true }))
}

// 导出类型,方便在其他地方使用
export type { NumberOnlyOptions }

2. 创建指令管理器(可选)

typescript

// src/directives/index.ts
import type { App } from 'vue'
import { NumberOnlyDirective, type NumberOnlyOptions } from './NumberOnlyDirective'
import { AutoFocusDirective } from './AutoFocusDirective'
import { ClickOutsideDirective } from './ClickOutsideDirective'

// 指令集合
const directives = {
'number-only': NumberOnlyDirective,
'auto-focus': AutoFocusDirective,
'click-outside': ClickOutsideDirective
} as const

// 导出类型
export type DirectiveName = keyof typeof directives

// 全局安装函数
export function installDirectives(app: App) {
Object.entries(directives).forEach(([name, directive]) => {
app.directive(name, directive)
})
}

// 导出单个指令(用于局部注册)
export {
NumberOnlyDirective,
AutoFocusDirective,
ClickOutsideDirective
}

// 导出类型
export type { NumberOnlyOptions }

3. 全局注册指令

typescript

// src/main.ts
import { createApp } from 'vue'
import App from './App.vue'
import { installDirectives } from './directives'

const app = createApp(App)

// 安装所有指令
installDirectives(app)

app.mount('#app')

三、局部注册和使用

1. 组件内局部注册

vue

<!– src/components/YearInput.vue –>
<template>
<div>
<!– 使用自定义指令 –>
<el-input
v-number-only="numberOnlyOptions"
v-model="yearValue"
placeholder="请输入年份"
clearable
/>

<!– 使用修饰符 –>
<el-input
v-number-only.debounce="numberOnlyOptions"
v-model="codeValue"
placeholder="请输入验证码"
/>
</div>
</template>

<script setup lang="ts">
import { ref, computed } from 'vue'
import { NumberOnlyDirective, type NumberOnlyOptions } from '@/directives'

// 局部注册指令
const vNumberOnly = NumberOnlyDirective

// 指令配置
const numberOnlyOptions = computed<NumberOnlyOptions>(() => ({
maxLength: 4,
min: 1900,
max: 2100,
allowNegative: false,
allowDecimal: false
}))

// 数据
const yearValue = ref('')
const codeValue = ref('')
</script>

<style scoped>
/* 组件样式 */
</style>

2. 使用指令参数和修饰符

typescript

// src/directives/AdvancedNumberOnlyDirective.ts
import type { Directive, DirectiveBinding } from 'vue'

// 定义修饰符类型
interface NumberOnlyModifiers {
debounce?: boolean
trim?: boolean
lazy?: boolean
immediate?: boolean
}

// 创建支持修饰符的指令
export const AdvancedNumberOnlyDirective: Directive<
HTMLElement,
number | undefined,
NumberOnlyModifiers
> = {
mounted(el, binding) {
const { value, modifiers } = binding

console.log('指令参数:', value)
console.log('修饰符:', modifiers)

const input = el.querySelector('input')
if (!input) return

// 根据修饰符应用不同逻辑
let handler = createBaseHandler(input, value || 4)

if (modifiers.debounce) {
handler = createDebouncedHandler(handler, 300)
}

if (modifiers.trim) {
handler = createTrimmedHandler(handler)
}

if (modifiers.lazy) {
// 使用 change 事件而不是 input 事件
input.addEventListener('change', handler)
el._handlerType = 'change'
} else {
input.addEventListener('input', handler)
el._handlerType = 'input'
}

el._handler = handler

// 如果设置了 immediate,立即格式化
if (modifiers.immediate) {
formatInput(input, value || 4)
}
},

unmounted(el) {
const input = el.querySelector('input')
if (input && el._handler && el._handlerType) {
input.removeEventListener(el._handlerType, el._handler)
}
}
}

// 辅助函数
function createBaseHandler(input: HTMLInputElement, maxLength: number): (e: Event) => void {
return (e: Event) => {
const target = e.target as HTMLInputElement
target.value = target.value.replace(/\\D/g, '').slice(0, maxLength)
triggerUpdate(target)
}
}

function createDebouncedHandler(handler: Function, delay: number): Function {
let timer: NodeJS.Timeout | null = null

return (…args: any[]) => {
if (timer) clearTimeout(timer)

timer = setTimeout(() => {
handler(…args)
timer = null
}, delay)
}
}

function createTrimmedHandler(handler: Function): Function {
return (e: Event) => {
const target = e.target as HTMLInputElement
target.value = target.value.trim()
handler(e)
}
}

function formatInput(input: HTMLInputElement, maxLength: number) {
input.value = input.value.replace(/\\D/g, '').slice(0, maxLength)
triggerUpdate(input)
}

function triggerUpdate(input: HTMLInputElement) {
input.dispatchEvent(new Event('input', { bubbles: true }))
}

// 扩展类型
declare global {
interface HTMLElement {
_handler?: Function
_handlerType?: 'input' | 'change'
}
}

四、创建更多实用指令示例

1. 自动聚焦指令

typescript

// src/directives/AutoFocusDirective.ts
import type { Directive, DirectiveBinding } from 'vue'

interface AutoFocusOptions {
delay?: number
selectAll?: boolean
}

export const AutoFocusDirective: Directive<HTMLElement, AutoFocusOptions | boolean | undefined> = {
mounted(el, binding) {
const options = parseAutoFocusOptions(binding.value)

const focusElement = el.tagName.toLowerCase() === 'input'
? el as HTMLInputElement
: el.querySelector('input, textarea, [tabindex]')

if (!focusElement) return

const focusFn = () => {
focusElement.focus()

if (options.selectAll && 'select' in focusElement) {
;(focusElement as HTMLInputElement).select()
}
}

if (options.delay && options.delay > 0) {
setTimeout(focusFn, options.delay)
} else {
// 使用 nextTick 确保 DOM 已更新
setTimeout(focusFn, 0)
}
}
}

function parseAutoFocusOptions(value: AutoFocusOptions | boolean | undefined): AutoFocusOptions {
if (typeof value === 'boolean') {
return { delay: 0, selectAll: false }
}

if (typeof value === 'object' && value !== null) {
return {
delay: value.delay || 0,
selectAll: value.selectAll || false
}
}

return { delay: 0, selectAll: false }
}

2. 点击外部关闭指令

typescript

// src/directives/ClickOutsideDirective.ts
import type { Directive, DirectiveBinding } from 'vue'

export const ClickOutsideDirective: Directive<HTMLElement, (e: MouseEvent) => void> = {
mounted(el, binding) {
const handler = (e: MouseEvent) => {
if (!el.contains(e.target as Node) && el !== e.target) {
binding.value(e)
}
}

// 使用捕获阶段确保先执行
document.addEventListener('click', handler, true)

// 存储引用以便清理
el._clickOutsideHandler = handler
},

unmounted(el) {
if (el._clickOutsideHandler) {
document.removeEventListener('click', el._clickOutsideHandler, true)
delete el._clickOutsideHandler
}
}
}

declare global {
interface HTMLElement {
_clickOutsideHandler?: (e: MouseEvent) => void
}
}

3. 防抖指令

typescript

// src/directives/DebounceDirective.ts
import type { Directive, DirectiveBinding } from 'vue'

type DebounceFunction = (…args: any[]) => void

interface DebounceOptions {
delay?: number
events?: string
immediate?: boolean
}

export const DebounceDirective: Directive<HTMLElement, DebounceFunction, DebounceOptions> = {
mounted(el, binding) {
const { value: handler, modifiers, arg } = binding

if (typeof handler !== 'function') {
console.warn('DebounceDirective: 绑定值必须是函数')
return
}

const delay = parseInt(arg || '300', 10) || 300
const events = modifiers.events ? modifiers.events.split(',') : ['input']
const immediate = modifiers.immediate || false

// 创建防抖函数
let timeout: NodeJS.Timeout | null = null
let isImmediateCalled = false

const debouncedHandler = (…args: any[]) => {
if (timeout) {
clearTimeout(timeout)
timeout = null
}

if (immediate && !isImmediateCalled) {
handler(…args)
isImmediateCalled = true
} else {
timeout = setTimeout(() => {
handler(…args)
isImmediateCalled = false
}, delay)
}
}

// 绑定事件
events.forEach(eventName => {
el.addEventListener(eventName.trim(), debouncedHandler)
})

// 存储引用
el._debounceHandler = debouncedHandler
el._debounceEvents = events
},

unmounted(el) {
if (el._debounceHandler && el._debounceEvents) {
el._debounceEvents.forEach(eventName => {
el.removeEventListener(eventName, el._debounceHandler!)
})

delete el._debounceHandler
delete el._debounceEvents
}
}
}

declare global {
interface HTMLElement {
_debounceHandler?: Function
_debounceEvents?: string[]
}
}

五、在组件中使用指令

1. 使用多个指令

vue

<!– src/components/AdvancedForm.vue –>
<template>
<div class="form-container">
<!– 使用多个指令 –>
<el-input
v-number-only="{ maxLength: 4, min: 1900, max: 2100 }"
v-auto-focus="{ delay: 100, selectAll: true }"
v-model="form.year"
placeholder="请输入年份"
/>

<!– 使用防抖指令 –>
<el-input
v-model="form.search"
v-debounce:500="handleSearch"
placeholder="搜索(防抖500ms)"
/>

<!– 点击外部关闭 –>
<div v-click-outside="closeDropdown" class="dropdown">
<button @click="toggleDropdown">打开菜单</button>
<div v-if="isDropdownOpen" class="dropdown-content">
<!– 菜单内容 –>
</div>
</div>
</div>
</template>

<script setup lang="ts">
import { ref } from 'vue'

// 局部注册指令
import { NumberOnlyDirective } from '@/directives/NumberOnlyDirective'
import { AutoFocusDirective } from '@/directives/AutoFocusDirective'
import { ClickOutsideDirective } from '@/directives/ClickOutsideDirective'
import { DebounceDirective } from '@/directives/DebounceDirective'

const vNumberOnly = NumberOnlyDirective
const vAutoFocus = AutoFocusDirective
const vClickOutside = ClickOutsideDirective
const vDebounce = DebounceDirective

// 数据
const form = ref({
year: '',
search: ''
})

const isDropdownOpen = ref(false)

// 方法
const handleSearch = () => {
console.log('搜索:', form.value.search)
// 执行搜索逻辑
}

const toggleDropdown = () => {
isDropdownOpen.value = !isDropdownOpen.value
}

const closeDropdown = () => {
isDropdownOpen.value = false
}
</script>

<style scoped>
.form-container {
max-width: 400px;
margin: 0 auto;
padding: 20px;
}

.dropdown {
position: relative;
display: inline-block;
margin-top: 20px;
}

.dropdown-content {
position: absolute;
background: white;
border: 1px solid #ccc;
padding: 10px;
z-index: 1000;
}
</style>

六、测试自定义指令

1. 创建单元测试

typescript

// tests/directives/NumberOnlyDirective.test.ts
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import { mount } from '@vue/test-utils'
import { defineComponent, ref, nextTick } from 'vue'
import { NumberOnlyDirective } from '@/directives/NumberOnlyDirective'

// 创建测试组件
const createTestComponent = (template: string) => {
return defineComponent({
template,
directives: {
'number-only': NumberOnlyDirective
},
setup() {
return {
value: ref('')
}
}
})
}

describe('NumberOnlyDirective', () => {
it('应该过滤非数字字符', async () => {
const wrapper = mount(createTestComponent(
'<input v-number-only v-model="value" />'
))

const input = wrapper.find('input')
await input.setValue('abc123def')

expect(input.element.value).toBe('123')
expect(wrapper.vm.value).toBe('123')
})

it('应该限制最大长度', async () => {
const wrapper = mount(createTestComponent(
'<input v-number-only="{ maxLength: 4 }" v-model="value" />'
))

const input = wrapper.find('input')
await input.setValue('123456')

expect(input.element.value).toBe('1234')
expect(wrapper.vm.value).toBe('1234')
})

it('应该允许负数和浮点数', async () => {
const wrapper = mount(createTestComponent(
'<input v-number-only="{ allowNegative: true, allowDecimal: true }" v-model="value" />'
))

const input = wrapper.find('input')
await input.setValue('-123.45')

expect(input.element.value).toBe('-123.45')
expect(wrapper.vm.value).toBe('-123.45')
})

it('应该在元素卸载时清理事件监听', async () => {
const wrapper = mount(createTestComponent(
'<input v-number-only v-model="value" v-if="show" />'
))

const input = wrapper.find('input')
const removeEventListenerSpy = vi.spyOn(input.element, 'removeEventListener')

wrapper.vm.show = false
await nextTick()

expect(removeEventListenerSpy).toHaveBeenCalled()
})
})

七、最佳实践和注意事项

1. 命名规范

typescript

// 好的命名
vNumberOnly // 小写字母,短横线连接
vAutoFocus // 描述清晰
vClickOutside // 表达意图

// 不好的命名
vNumber // 太泛泛
vOnly // 不清楚功能
vOut // 缩写不明确

2. 性能优化

typescript

// 使用防抖/节流
import { throttle, debounce } from 'lodash-es'

const vOptimizedDirective = {
mounted(el, binding) {
const handler = binding.value

// 使用防抖
const debouncedHandler = debounce(handler, 300)

// 或者使用节流
const throttledHandler = throttle(handler, 300)

el.addEventListener('input', debouncedHandler)
el._handler = debouncedHandler
}
}

// 懒加载指令
const lazyDirective = {
mounted(el, binding) {
// 使用 IntersectionObserver 实现懒加载
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
// 执行指令逻辑
binding.value()
observer.unobserve(el)
}
})
})

observer.observe(el)
el._observer = observer
},

unmounted(el) {
if (el._observer) {
el._observer.disconnect()
}
}
}

3. 错误处理

typescript

const vSafeDirective = {
mounted(el, binding) {
try {
// 指令逻辑
if (typeof binding.value !== 'function') {
throw new Error('指令参数必须是函数')
}

// 执行指令
binding.value()
} catch (error) {
console.error('指令执行出错:', error)

// 可以根据环境决定是否显示错误
if (process.env.NODE_ENV === 'development') {
el.style.border = '2px solid red'
el.title = `指令错误: ${error.message}`
}
}
}
}

八、总结

学习要点:

  • 理解指令生命周期:

    • mounted: 元素挂载时调用

    • updated: 元素更新时调用

    • unmounted: 元素卸载时调用

  • 掌握核心概念:

    • 指令参数:v-directive:arg="value"

    • 修饰符:v-directive.modifier

    • 绑定值:binding.value

  • TypeScript 类型支持:

    • 使用 Directive 泛型类型

    • 定义接口约束参数类型

    • 扩展 HTMLElement 类型

  • 最佳实践:

    • 一个指令只做一件事

    • 提供适当的配置选项

    • 清理事件监听防止内存泄漏

    • 添加错误处理

  • 实际应用建议:

  • 从简单开始:先实现基础功能,再逐步添加高级特性

  • 复用现有方案:查看是否已有成熟的指令库

  • 编写测试:确保指令的稳定性和可靠性

  • 文档化:记录指令的使用方法和参数说明

  • 通过这个完整的教程,你应该能够掌握 Vue3 + TypeScript 中自定义指令的创建、注册和使用。记住实践是最好的学习方式,尝试创建自己的指令来解决实际问题。


    赞(0)
    未经允许不得转载:171主机测评 » Vue3+TypeScript 自定义指令
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址