Search K
Appearance
Appearance
📊 SEO元描述:2024年最新Vue3实用自定义指令案例教程,详解v-focus、v-loading、v-permission等实战指令。包含完整源码实现,适合Vue3开发者掌握指令开发实战技能。
核心关键词:Vue3自定义指令案例、v-focus指令、v-loading指令、v-permission指令、Vue3指令实战
长尾关键词:Vue3自定义指令怎么写、实用指令开发案例、Vue3指令库开发、自定义指令最佳实践、Vue3指令源码实现
通过本节实用自定义指令案例,你将系统性掌握:
为什么需要这些实用指令?在实际项目开发中,我们经常遇到重复的DOM操作、复杂的用户交互和通用的业务逻辑。实用指令将这些常见需求封装成可复用的解决方案。
💡 设计理念:好的指令应该是"开箱即用"的,它们应该解决真实的业务问题,提供直观的API,并且具备良好的扩展性。
自动聚焦是提升用户体验的重要功能,特别是在表单、搜索框、模态框等场景中:
// 🎉 v-focus 自动聚焦指令完整实现
const focusDirective = {
mounted(el, binding) {
// 验证元素是否可聚焦
if (!this.isFocusable(el)) {
console.warn('v-focus指令只能用于可聚焦的元素')
return
}
// 解析配置参数
const config = this.parseConfig(binding)
// 根据配置决定聚焦时机
if (config.immediate) {
this.focusElement(el, config)
} else if (config.delay > 0) {
setTimeout(() => this.focusElement(el, config), config.delay)
} else {
// 默认在下一个事件循环中聚焦
this.$nextTick(() => this.focusElement(el, config))
}
// 存储配置以便后续使用
el._focusConfig = config
},
updated(el, binding) {
// 当绑定值发生变化时重新处理聚焦
if (binding.value !== binding.oldValue) {
const config = this.parseConfig(binding)
if (config.autoRefocus && binding.value) {
this.focusElement(el, config)
}
el._focusConfig = config
}
},
// 解析配置参数
parseConfig(binding) {
const defaultConfig = {
immediate: false,
delay: 0,
select: false,
preventScroll: false,
autoRefocus: false,
condition: true
}
// 如果绑定值是布尔值,直接作为condition
if (typeof binding.value === 'boolean') {
return { ...defaultConfig, condition: binding.value }
}
// 如果绑定值是对象,合并配置
if (typeof binding.value === 'object' && binding.value !== null) {
return { ...defaultConfig, ...binding.value }
}
// 处理修饰符
const modifiers = binding.modifiers
return {
...defaultConfig,
immediate: modifiers.immediate || defaultConfig.immediate,
select: modifiers.select || defaultConfig.select,
preventScroll: modifiers.prevent || defaultConfig.preventScroll,
delay: this.getDelayFromModifiers(modifiers) || defaultConfig.delay
}
},
// 从修饰符中获取延迟时间
getDelayFromModifiers(modifiers) {
const delayKeys = Object.keys(modifiers).filter(key => /^\d+$/.test(key))
return delayKeys.length > 0 ? parseInt(delayKeys[0]) : 0
},
// 检查元素是否可聚焦
isFocusable(el) {
const focusableElements = [
'input', 'textarea', 'select', 'button', 'a'
]
const tagName = el.tagName.toLowerCase()
// 检查标签类型
if (focusableElements.includes(tagName)) {
return true
}
// 检查是否有tabindex属性
if (el.hasAttribute('tabindex')) {
return true
}
// 检查是否是contenteditable元素
if (el.contentEditable === 'true') {
return true
}
return false
},
// 执行聚焦操作
focusElement(el, config) {
try {
// 检查聚焦条件
if (!config.condition) {
return
}
// 检查元素是否在DOM中且可见
if (!document.contains(el) || !this.isVisible(el)) {
return
}
// 执行聚焦
const focusOptions = {
preventScroll: config.preventScroll
}
el.focus(focusOptions)
// 如果需要选中文本内容
if (config.select && (el.select || el.setSelectionRange)) {
if (el.select) {
el.select()
} else if (el.setSelectionRange) {
el.setSelectionRange(0, el.value.length)
}
}
// 触发自定义事件
el.dispatchEvent(new CustomEvent('focus-applied', {
detail: { config }
}))
} catch (error) {
console.error('聚焦失败:', error)
}
},
// 检查元素是否可见
isVisible(el) {
const style = window.getComputedStyle(el)
return style.display !== 'none' &&
style.visibility !== 'hidden' &&
style.opacity !== '0'
}
}
// 注册指令
app.directive('focus', focusDirective)<!-- v-focus 使用示例 -->
<template>
<div>
<!-- 基础用法 -->
<input v-focus placeholder="页面加载后自动聚焦">
<!-- 立即聚焦 -->
<input v-focus.immediate placeholder="立即聚焦">
<!-- 延迟聚焦 -->
<input v-focus.500 placeholder="500ms后聚焦">
<!-- 聚焦并选中文本 -->
<input v-focus.select value="这段文字会被选中">
<!-- 条件聚焦 -->
<input v-focus="shouldFocus" placeholder="条件聚焦">
<!-- 高级配置 -->
<input
v-focus="{
immediate: true,
select: true,
preventScroll: true,
condition: isModalOpen
}"
placeholder="高级配置聚焦"
>
<!-- 在模态框中使用 -->
<div v-if="showModal" class="modal">
<input v-focus.immediate placeholder="模态框打开时自动聚焦">
<button @click="showModal = false">关闭</button>
</div>
<button @click="showModal = true">打开模态框</button>
</div>
</template>
<script>
export default {
data() {
return {
shouldFocus: true,
isModalOpen: false,
showModal: false
}
}
}
</script>加载状态管理是现代Web应用的重要组成部分:
// 🎉 v-loading 加载状态指令完整实现
const loadingDirective = {
mounted(el, binding) {
// 创建加载状态管理器
el._loadingManager = new LoadingManager(el, binding)
// 根据初始值设置加载状态
if (binding.value) {
el._loadingManager.show()
}
},
updated(el, binding) {
if (!el._loadingManager) return
// 更新配置
el._loadingManager.updateConfig(binding)
// 根据新值切换加载状态
if (binding.value !== binding.oldValue) {
if (binding.value) {
el._loadingManager.show()
} else {
el._loadingManager.hide()
}
}
},
beforeUnmount(el) {
if (el._loadingManager) {
el._loadingManager.destroy()
delete el._loadingManager
}
}
}
// 加载状态管理器类
class LoadingManager {
constructor(el, binding) {
this.el = el
this.isShowing = false
this.loadingEl = null
this.originalPosition = null
this.originalOverflow = null
// 解析配置
this.config = this.parseConfig(binding)
// 初始化
this.init()
}
parseConfig(binding) {
const defaultConfig = {
text: '加载中...',
spinner: 'default',
background: 'rgba(255, 255, 255, 0.8)',
color: '#409eff',
size: 'medium',
fullscreen: false,
lock: true,
customClass: '',
zIndex: 2000
}
// 处理不同类型的绑定值
if (typeof binding.value === 'boolean') {
return defaultConfig
}
if (typeof binding.value === 'string') {
return { ...defaultConfig, text: binding.value }
}
if (typeof binding.value === 'object' && binding.value !== null) {
return { ...defaultConfig, ...binding.value }
}
// 处理修饰符
const modifiers = binding.modifiers
return {
...defaultConfig,
fullscreen: modifiers.fullscreen || defaultConfig.fullscreen,
lock: modifiers.lock !== undefined ? modifiers.lock : defaultConfig.lock,
spinner: modifiers.dots ? 'dots' :
modifiers.spinner ? 'spinner' :
modifiers.pulse ? 'pulse' : defaultConfig.spinner
}
}
init() {
// 保存原始样式
const computedStyle = window.getComputedStyle(this.el)
this.originalPosition = computedStyle.position
this.originalOverflow = computedStyle.overflow
// 确保容器有定位上下文
if (this.originalPosition === 'static') {
this.el.style.position = 'relative'
}
}
show() {
if (this.isShowing) return
this.isShowing = true
// 创建加载元素
this.createLoadingElement()
// 添加到DOM
if (this.config.fullscreen) {
document.body.appendChild(this.loadingEl)
} else {
this.el.appendChild(this.loadingEl)
}
// 锁定滚动
if (this.config.lock) {
if (this.config.fullscreen) {
document.body.style.overflow = 'hidden'
} else {
this.el.style.overflow = 'hidden'
}
}
// 添加动画
requestAnimationFrame(() => {
this.loadingEl.classList.add('loading-fade-in')
})
// 触发事件
this.el.dispatchEvent(new CustomEvent('loading-show'))
}
hide() {
if (!this.isShowing || !this.loadingEl) return
this.isShowing = false
// 添加淡出动画
this.loadingEl.classList.add('loading-fade-out')
// 动画结束后移除元素
setTimeout(() => {
this.removeLoadingElement()
}, 300)
// 恢复滚动
if (this.config.lock) {
if (this.config.fullscreen) {
document.body.style.overflow = ''
} else {
this.el.style.overflow = this.originalOverflow
}
}
// 触发事件
this.el.dispatchEvent(new CustomEvent('loading-hide'))
}
createLoadingElement() {
this.loadingEl = document.createElement('div')
this.loadingEl.className = `v-loading-mask ${this.config.customClass}`
// 设置样式
this.loadingEl.style.cssText = `
position: ${this.config.fullscreen ? 'fixed' : 'absolute'};
top: 0;
left: 0;
right: 0;
bottom: 0;
background: ${this.config.background};
z-index: ${this.config.zIndex};
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
opacity: 0;
transition: opacity 0.3s ease;
`
// 创建加载内容
const loadingContent = document.createElement('div')
loadingContent.className = 'v-loading-content'
loadingContent.style.cssText = `
text-align: center;
color: ${this.config.color};
`
// 创建加载动画
const spinner = this.createSpinner()
loadingContent.appendChild(spinner)
// 创建加载文字
if (this.config.text) {
const textEl = document.createElement('div')
textEl.className = 'v-loading-text'
textEl.textContent = this.config.text
textEl.style.cssText = `
margin-top: 12px;
font-size: 14px;
color: ${this.config.color};
`
loadingContent.appendChild(textEl)
}
this.loadingEl.appendChild(loadingContent)
}
createSpinner() {
const spinner = document.createElement('div')
spinner.className = `v-loading-spinner v-loading-${this.config.spinner}`
const size = this.getSizeValue()
switch (this.config.spinner) {
case 'dots':
spinner.innerHTML = this.createDotsSpinner(size)
break
case 'pulse':
spinner.innerHTML = this.createPulseSpinner(size)
break
default:
spinner.innerHTML = this.createDefaultSpinner(size)
}
return spinner
}
getSizeValue() {
const sizeMap = {
small: 24,
medium: 32,
large: 40
}
return sizeMap[this.config.size] || 32
}
createDefaultSpinner(size) {
return `
<div style="
width: ${size}px;
height: ${size}px;
border: 3px solid transparent;
border-top: 3px solid ${this.config.color};
border-radius: 50%;
animation: v-loading-rotate 1s linear infinite;
"></div>
`
}
createDotsSpinner(size) {
const dotSize = size / 4
return `
<div style="display: flex; gap: ${dotSize/2}px;">
${Array(3).fill(0).map((_, i) => `
<div style="
width: ${dotSize}px;
height: ${dotSize}px;
background: ${this.config.color};
border-radius: 50%;
animation: v-loading-bounce 1.4s ease-in-out infinite both;
animation-delay: ${i * 0.16}s;
"></div>
`).join('')}
</div>
`
}
createPulseSpinner(size) {
return `
<div style="
width: ${size}px;
height: ${size}px;
background: ${this.config.color};
border-radius: 50%;
animation: v-loading-pulse 1.5s ease-in-out infinite;
"></div>
`
}
removeLoadingElement() {
if (this.loadingEl) {
if (this.loadingEl.parentNode) {
this.loadingEl.parentNode.removeChild(this.loadingEl)
}
this.loadingEl = null
}
}
updateConfig(binding) {
this.config = this.parseConfig(binding)
}
destroy() {
this.hide()
// 恢复原始样式
if (this.originalPosition !== null) {
this.el.style.position = this.originalPosition
}
if (this.originalOverflow !== null) {
this.el.style.overflow = this.originalOverflow
}
}
}
// 添加CSS动画
const style = document.createElement('style')
style.textContent = `
.loading-fade-in {
opacity: 1 !important;
}
.loading-fade-out {
opacity: 0 !important;
}
@keyframes v-loading-rotate {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
@keyframes v-loading-bounce {
0%, 80%, 100% { transform: scale(0); }
40% { transform: scale(1); }
}
@keyframes v-loading-pulse {
0% { transform: scale(0); opacity: 1; }
100% { transform: scale(1); opacity: 0; }
}
`
document.head.appendChild(style)
// 注册指令
app.directive('loading', loadingDirective)<!-- v-loading 使用示例 -->
<template>
<div>
<!-- 基础用法 -->
<div v-loading="isLoading" style="height: 200px; border: 1px solid #ccc;">
<p>这里是内容区域</p>
<button @click="toggleLoading">切换加载状态</button>
</div>
<!-- 自定义文字 -->
<div v-loading="'正在保存数据...'" style="height: 150px; margin-top: 20px;">
<p>自定义加载文字</p>
</div>
<!-- 不同的加载动画 -->
<div v-loading.dots="isLoading" style="height: 150px; margin-top: 20px;">
<p>点状加载动画</p>
</div>
<!-- 全屏加载 -->
<button v-loading.fullscreen="isFullscreenLoading" @click="showFullscreenLoading">
全屏加载
</button>
<!-- 高级配置 -->
<div
v-loading="{
loading: isAdvancedLoading,
text: '数据处理中,请稍候...',
spinner: 'pulse',
background: 'rgba(0, 0, 0, 0.8)',
color: '#fff',
size: 'large'
}"
style="height: 200px; margin-top: 20px;"
>
<p>高级配置加载</p>
<button @click="isAdvancedLoading = !isAdvancedLoading">切换</button>
</div>
</div>
</template>
<script>
export default {
data() {
return {
isLoading: false,
isFullscreenLoading: false,
isAdvancedLoading: false
}
},
methods: {
toggleLoading() {
this.isLoading = !this.isLoading
},
showFullscreenLoading() {
this.isFullscreenLoading = true
setTimeout(() => {
this.isFullscreenLoading = false
}, 3000)
}
}
}
</script>权限控制是企业级应用的核心功能:
// 🎉 v-permission 权限控制指令完整实现
const permissionDirective = {
mounted(el, binding) {
// 创建权限管理器
el._permissionManager = new PermissionManager(el, binding)
// 执行权限检查
el._permissionManager.checkPermission()
},
updated(el, binding) {
if (!el._permissionManager) return
// 更新权限配置
el._permissionManager.updateConfig(binding)
// 重新检查权限
el._permissionManager.checkPermission()
},
beforeUnmount(el) {
if (el._permissionManager) {
el._permissionManager.destroy()
delete el._permissionManager
}
}
}
// 权限管理器类
class PermissionManager {
constructor(el, binding) {
this.el = el
this.originalDisplay = el.style.display
this.originalVisibility = el.style.visibility
this.isHidden = false
// 解析权限配置
this.config = this.parseConfig(binding)
// 初始化
this.init()
}
parseConfig(binding) {
const defaultConfig = {
permissions: [],
roles: [],
mode: 'any', // 'any' | 'all'
action: 'hide', // 'hide' | 'disable' | 'remove'
fallback: null,
strict: false
}
// 处理字符串权限
if (typeof binding.value === 'string') {
return {
...defaultConfig,
permissions: [binding.value]
}
}
// 处理数组权限
if (Array.isArray(binding.value)) {
return {
...defaultConfig,
permissions: binding.value
}
}
// 处理对象配置
if (typeof binding.value === 'object' && binding.value !== null) {
return { ...defaultConfig, ...binding.value }
}
// 处理修饰符
const modifiers = binding.modifiers
const config = { ...defaultConfig }
if (modifiers.all) config.mode = 'all'
if (modifiers.disable) config.action = 'disable'
if (modifiers.remove) config.action = 'remove'
if (modifiers.strict) config.strict = true
return config
}
init() {
// 监听权限变化事件
document.addEventListener('permission-changed', this.handlePermissionChange.bind(this))
}
checkPermission() {
const hasPermission = this.evaluatePermission()
if (hasPermission) {
this.showElement()
} else {
this.hideElement()
}
// 触发权限检查事件
this.el.dispatchEvent(new CustomEvent('permission-checked', {
detail: {
hasPermission,
config: this.config
}
}))
}
evaluatePermission() {
const userPermissions = this.getUserPermissions()
const userRoles = this.getUserRoles()
// 检查权限
const permissionCheck = this.checkPermissions(userPermissions)
// 检查角色
const roleCheck = this.checkRoles(userRoles)
// 根据模式返回结果
if (this.config.mode === 'all') {
return permissionCheck && roleCheck
} else {
return permissionCheck || roleCheck
}
}
checkPermissions(userPermissions) {
if (this.config.permissions.length === 0) return true
if (this.config.mode === 'all') {
return this.config.permissions.every(permission =>
this.hasPermission(userPermissions, permission)
)
} else {
return this.config.permissions.some(permission =>
this.hasPermission(userPermissions, permission)
)
}
}
checkRoles(userRoles) {
if (this.config.roles.length === 0) return true
if (this.config.mode === 'all') {
return this.config.roles.every(role => userRoles.includes(role))
} else {
return this.config.roles.some(role => userRoles.includes(role))
}
}
hasPermission(userPermissions, requiredPermission) {
// 支持通配符权限
if (requiredPermission.includes('*')) {
const pattern = requiredPermission.replace(/\*/g, '.*')
const regex = new RegExp(`^${pattern}$`)
return userPermissions.some(permission => regex.test(permission))
}
// 支持层级权限
if (requiredPermission.includes(':')) {
return userPermissions.some(permission => {
if (this.config.strict) {
return permission === requiredPermission
} else {
return permission.startsWith(requiredPermission) ||
requiredPermission.startsWith(permission)
}
})
}
// 精确匹配
return userPermissions.includes(requiredPermission)
}
getUserPermissions() {
// 从全局状态获取用户权限
return window.$permissionStore?.permissions || []
}
getUserRoles() {
// 从全局状态获取用户角色
return window.$permissionStore?.roles || []
}
showElement() {
if (!this.isHidden) return
this.isHidden = false
switch (this.config.action) {
case 'hide':
this.el.style.display = this.originalDisplay
this.el.style.visibility = this.originalVisibility
break
case 'disable':
this.el.disabled = false
this.el.classList.remove('permission-disabled')
break
case 'remove':
if (this.el._permissionPlaceholder) {
this.el._permissionPlaceholder.parentNode.replaceChild(
this.el,
this.el._permissionPlaceholder
)
delete this.el._permissionPlaceholder
}
break
}
}
hideElement() {
if (this.isHidden) return
this.isHidden = true
switch (this.config.action) {
case 'hide':
this.el.style.display = 'none'
this.el.style.visibility = 'hidden'
break
case 'disable':
this.el.disabled = true
this.el.classList.add('permission-disabled')
break
case 'remove':
const placeholder = document.createComment('permission-hidden')
this.el._permissionPlaceholder = placeholder
this.el.parentNode.replaceChild(placeholder, this.el)
break
}
// 显示降级内容
if (this.config.fallback) {
this.showFallback()
}
}
showFallback() {
if (this.el._fallbackElement) return
const fallbackEl = document.createElement('div')
fallbackEl.className = 'permission-fallback'
fallbackEl.innerHTML = this.config.fallback
this.el.parentNode.insertBefore(fallbackEl, this.el.nextSibling)
this.el._fallbackElement = fallbackEl
}
hideFallback() {
if (this.el._fallbackElement) {
this.el._fallbackElement.parentNode.removeChild(this.el._fallbackElement)
delete this.el._fallbackElement
}
}
handlePermissionChange() {
// 权限发生变化时重新检查
this.checkPermission()
}
updateConfig(binding) {
this.config = this.parseConfig(binding)
}
destroy() {
document.removeEventListener('permission-changed', this.handlePermissionChange)
this.hideFallback()
}
}
// 权限存储管理
window.$permissionStore = {
permissions: [],
roles: [],
setPermissions(permissions) {
this.permissions = permissions
document.dispatchEvent(new CustomEvent('permission-changed'))
},
setRoles(roles) {
this.roles = roles
document.dispatchEvent(new CustomEvent('permission-changed'))
},
hasPermission(permission) {
return this.permissions.includes(permission)
},
hasRole(role) {
return this.roles.includes(role)
}
}
// 添加权限相关样式
const permissionStyle = document.createElement('style')
permissionStyle.textContent = `
.permission-disabled {
opacity: 0.5;
cursor: not-allowed;
pointer-events: none;
}
.permission-fallback {
padding: 8px 12px;
background: #f5f5f5;
border: 1px solid #d9d9d9;
border-radius: 4px;
color: #666;
font-size: 12px;
}
`
document.head.appendChild(permissionStyle)
// 注册指令
app.directive('permission', permissionDirective)<!-- v-permission 使用示例 -->
<template>
<div>
<!-- 基础权限控制 -->
<button v-permission="'user:create'">创建用户</button>
<button v-permission="'user:edit'">编辑用户</button>
<button v-permission="'user:delete'">删除用户</button>
<!-- 多权限控制 -->
<button v-permission="['admin', 'super-admin']">管理员功能</button>
<!-- 角色控制 -->
<div v-permission="{ roles: ['admin'] }">
<h3>管理员专区</h3>
<p>只有管理员可以看到这个内容</p>
</div>
<!-- 复杂权限配置 -->
<button
v-permission="{
permissions: ['user:edit', 'user:view'],
mode: 'all',
action: 'disable',
fallback: '您没有权限执行此操作'
}"
>
编辑用户信息
</button>
<!-- 通配符权限 -->
<div v-permission="'user:*'">
<p>拥有所有用户相关权限</p>
</div>
<!-- 层级权限 -->
<button v-permission="'system:config:database'">数据库配置</button>
<!-- 权限控制演示 -->
<div class="permission-demo">
<h3>权限演示</h3>
<button @click="setUserPermissions">设置用户权限</button>
<button @click="setAdminPermissions">设置管理员权限</button>
<button @click="clearPermissions">清除权限</button>
</div>
</div>
</template>
<script>
export default {
methods: {
setUserPermissions() {
window.$permissionStore.setPermissions(['user:view', 'user:edit'])
window.$permissionStore.setRoles(['user'])
},
setAdminPermissions() {
window.$permissionStore.setPermissions([
'user:*',
'system:*',
'admin:*'
])
window.$permissionStore.setRoles(['admin', 'user'])
},
clearPermissions() {
window.$permissionStore.setPermissions([])
window.$permissionStore.setRoles([])
}
}
}
</script>实用指令案例总结:
💼 实际应用:这些指令在实际项目中能够显著提升开发效率和用户体验,建议根据项目需求进行定制和扩展。
通过本节实用自定义指令案例的学习,你已经掌握:
A: 大部分指令都适用于移动端,但需要注意触摸事件和移动端特有的交互模式。建议针对移动端进行适配优化。
A: 通过命名空间、优先级设置和冲突检测机制来避免指令冲突。建议在指令设计时考虑与其他指令的兼容性。
A: 合理设计的指令性能开销很小。关键是避免频繁的DOM操作和内存泄漏,及时清理资源。
A: 可以使用Vue Test Utils进行单元测试,模拟不同的绑定值和DOM环境,测试指令的各种行为。
A: 可以,建议将指令封装为独立的npm包,通过配置参数适应不同项目的需求。
// 指令库开发模板
const DirectiveLibrary = {
// 指令注册
install(app, options = {}) {
const directives = {
focus: focusDirective,
loading: loadingDirective,
permission: permissionDirective
}
Object.keys(directives).forEach(name => {
app.directive(name, directives[name])
})
// 全局配置
app.config.globalProperties.$directiveConfig = options
},
// 单独导出指令
focus: focusDirective,
loading: loadingDirective,
permission: permissionDirective
}
export default DirectiveLibrary"实用的自定义指令是Vue3开发中的利器,它们让我们能够以声明式的方式解决复杂的交互问题。掌握这些指令的开发技巧,就是掌握了现代前端开发的核心竞争力。"