注意

我注意到章节标题「自定义指令与插件」和额外上下文(app.directiveapp.usev-loading/v-permission)都是 Vue.js 的概念,但开头要求写「Python 教程」。

我先按 Vue3 自定义指令与插件的内容来写(采用您要求的教学风格),如果需要的是 Python 对应主题的教程请告知。


第3章 3.3 自定义指令与插件——让 Vue 默认功能不够用的时候,你自己来造!

🎯 开场:为什么 Vue 内置的不够用?

想象你开了一家奶茶店,总部发了一套标准操作流程(Vue 内置指令),但你发现:

  • 客人问「我的奶茶做好了没」,总部没给「查询进度」的流程
  • 有个常客是 VIP,你得给他单独加个优先通道,但总部没这个功能
  • 每天有 100 个客人要点「去冰三分糖」,你每次都要手写一堆重复指令

痛点来了
1. 内置指令只解决 80% 的问题,剩下 20% 你需要自己造
2. 同样一段逻辑在多个组件里复制粘贴,改一次要改 N 个地方
3. 有些功能横\n\nSimple tech illustration expla\n\nAI comic creation scene, creat\n\n跨多个不相关的组件,你想「复用」但不知道怎么搞

学完这章你能
- 自己写一个 v-focus 指令,让输入框自动聚焦
- 做一个 v-permission 指令,控制按钮「谁能点」
- 把多个指令和组件封装成一个插件,一行代码全项目生效


🧱 基础:核心概念(小白视角)

3.3.1 指令是什么?你天天在用,只是不知道名字

先回忆一下你用过的指令:

<input v-model="name" />
<button v-on:click="submit">提交</button>
<div v-if="show">显示我</div>

这些 v-xxx 都是指令。说白了,指令就是:

指令 = 一套预设好的「如果...那么...」的动作剧本

  • v-model:如果用户打字,就更新数据
  • v-if:如果条件为真,就显示这个元素
  • v-on:click:如果点击,就触发函数

Vue 内置了这些常用指令,但——总有不够用的时候


3.3.2 自定义指令:你自己写的「如果...那么...」

假设你有这个需求:「页面加载后,这个输入框要自动聚焦(光标跳进去)」。

Vue 没内置这个功能,怎么办?

用自定义指令自己造!

<script setup>
// 定义一个叫 vFocus 的指令
const vFocus = {
mounted(el) {
// mounted 是指令的一个「生命周期钩子」
// el 是这个指令所在的 DOM 元素(也就是那个 input)
el.focus()  // 让他聚焦!
}
}
</script>

<template>
<!-- 使用方式:vFocus(会自动转成 v-focus) -->
<input v-focus placeholder="点我自动聚焦" />
</template>

解释一下
- vFocus 这个名字会自动变成 v-focus 在模板里用
- mounted(el) 是一个钩子函数,当指令挂载到 DOM 上时自动触发
- el 就是那个 input 的原生 DOM 对象,所以你能调 el.focus()


3.3.3 指令的完整生命周期——不只是 mounted

指令有 5 个生命周期钩子(可以理解成「人生的 5 个关键节点」):

const vMyDirective = {
// 1. 创建时(元素本身创建了,但还没挂到页面上)
created(el, binding, vnode) {
console.log('出生了!')
},

// 2. 挂载前(元素要显示但还没显示)
beforeMount(el, binding, vnode) {
console.log('要显示了...')
},

// 3. 挂载时(已经显示在页面上了)⭐最常用
mounted(el, binding, vnode) {
console.log('显示了!')
},

// 4. 更新时(组件数据变了,指令重新执行)
updated(el, binding, vnode) {
console.log('数据变了,我也跟着变')
},

// 5. 卸载时(指令不用了,清理资源)
unmounted(el, binding, vnode) {
console.log('退休了,清理一下')
}
}

类比一下:就像一个人出生、上学、工作、退休、去世的过程。


3.3.4 binding 参数——指令的「配置文件」

你可能注意到了 binding 这个参数。它是啥?

<script setup>
const vHighlight = {
mounted(el, binding) {
// binding.value 是指令的值,比如 v-highlight="'yellow'" 里的 'yellow'
el.style.backgroundColor = binding.value || 'yellow'

// binding.arg 是指令的参数,比如 v-highlight:click 里的 'click'
console.log('触发的参数是:', binding.arg)

// binding.modifiers 是修饰符,比如 v-highlight.left 里的 left
console.log('修饰符有:', binding.modifiers)
}
}
</script>

<template>
<!-- value: 'pink' -->
<!-- arg: 'click' -->
<!-- modifiers: { left: true } -->
<div v-highlight:click.left="'pink'">点我试试</div>
</template>

用人话讲
- binding.value = 指令等号后面的值(v-highlight="'pink'" 里的 'pink'
- binding.arg = 冒号后面的参数(v-highlight:click 里的 click
- binding.modifiers = 点后面的修饰符(v-highlight.left 里的 left


3.3.5 全局注册 vs 局部注册——用一次还是用 N 次?

局部注册(只在当前组件用):

<script setup>
const vFocus = { /* ... */ }
</script>

<template>
<input v-focus />
</template>

全局注册(所有组件都能用)——在 main.js 里:

import { createApp } from 'vue'

const app = createApp(App)

// 造一个全局可用的 v-loading 指令
app.directive('loading', {
mounted(el, binding) {
if (binding.value) {
  el.classList.add('loading-overlay')
}
}
})

app.mount('#app')

什么时候用哪个?
- 指令只在一个组件用 → 局部
- 指令在整个项目都要用 → 全局


3.3.6 插件是什么?一箱子工具打包卖

你有了很多自定义指令,还有几个工具函数,还有几个通用组件……

每次在新项目用,你是一个个复制粘贴吗?

插件就是用来打包这些的

一个插件本质上就是一个带有 install 方法的对象

// myPlugin.js
export default {
install(app, options) {
// app 是 Vue 实例
// options 是你传入的配置参数

// 1. 注册全局指令
app.directive('focus', { /* ... */ })
app.directive('permission', { /* ... */ })

// 2. 注册全局组件
app.component('MyButton', { /* ... */ })

// 3. 给 app 添加全局属性
app.config.globalProperties.$myUtils = {
  formatDate: (date) => { /* ... */ },
  validateEmail: (email) => { /* ... */ }
}
}
}

在 main.js 里使用插件

import { createApp } from 'vue'
import App from './App.vue'
import myPlugin from './plugins/myPlugin'

const app = createApp(App)

// 一行代码,插件里所有东西都能用了!
app.use(myPlugin, { someOption: 'value' })

app.mount('#app')

类比:插件就像一个「工具箱套餐」,你不用一个个买工具,一次性把整套解决方案搬回家。


🔥 实战:3 个递进小项目

项目 1:5 分钟——做一个自动聚焦 + 滚动到顶部指令

需求:做一个 v-focus 指令让输入框自动聚焦,再做一个 v-scroll-top 让页面滚到顶部。

<!-- App.vue -->
<script setup>
import { ref } from 'vue'

// 局部注册 v-focus 指令
const vFocus = {
mounted(el) {
el.focus()
}
}

// 局部注册 v-scroll-top 指令
const vScrollTop = {
mounted(el) {
el.scrollIntoView({ behavior: 'smooth', block: 'start' })
}
}

const searchText = ref('')
</script>

<template>
<div style="height: 500px; background: #f0f0f0; padding: 20px;">
<h2>页面顶部</h2>
<p>往下滑,页面很长...</p>
</div>

<!-- 这个输入框会自己聚焦 -->
<div style="padding: 20px;">
<input 
  v-focus
  v-model="searchText"
  placeholder="页面一加载我就聚焦了!" 
  style="padding: 10px; font-size: 16px; width: 300px;"
/>
</div>

<!-- 点这个会滚到顶部 -->
<button 
v-scroll-top
style="position: fixed; bottom: 20px; right: 20px; padding: 10px 20px;"
>
⬆️ 回到顶部
</button>
</template>

预期输出:页面一加载,输入框就有光标;点按钮页面会平滑滚动到顶部。

一句话解释mounted 钩子在指令挂载时触发,正好用来做「自动做某事」的场景。


项目 2:15 分钟——做一个权限指令 v-permission

需求:根据用户角色显示/隐藏按钮(admin 能看全部,普通用户只能看部分)。

先准备一个模拟的用户数据:

// userStore.js
import { reactive } from 'vue'

export const userState = reactive({
name: '张三',
role: 'admin'  // 可改成 'user' 试试不同效果
})

然后是我们的指令文件:

// directives/permission.js
export const vPermission = {
mounted(el, binding) {
const userRole = binding.value  // 需要的角色
const currentUserRole = 'admin' // 模拟当前用户角色(实际从 store 拿)

// 如果当前角色不在允许列表里,就隐藏这个元素
const allowedRoles = Array.isArray(userRole) ? userRole : [userRole]

if (!allowedRoles.includes(currentUserRole)) {
  el.style.display = 'none'
}
}
}

在组件里用:

<!-- SomeComponent.vue -->
<script setup>
import { vPermission } from './directives/permission'
import { userState } from './userStore'
</script>

<template>
<h1>欢迎,{{ userState.name }}(角色:{{ userState.role }})</h1>

<!-- 只有 admin 能看到删除按钮 -->
<button v-permission="'admin'" style="background: red; color: white;">
🗑️ 删除用户(仅 admin)
</button>

<!-- admin 和 editor 都能看到编辑按钮 -->
<button v-permission="['admin', 'editor']" style="background: blue; color: white;">
✏️ 编辑内容(admin 或 editor)
</button>

<!-- 所有人都能看到查看按钮 -->
<button v-permission="'user'" style="background: green; color: white;">
👁️ 查看内容(所有用户)
</button>
</template>

预期输出
- 当前用户 role='admin',三个按钮都显示
- 把 userStore 里改成 role='user',只有绿色按钮显示

一句话解释:指令在 mounted 时检查权限,不符合就 el.style.display = 'none' 藏起来。


项目 3:15 分钟——封装一个 Loading 插件

需求:做一个 v-loading 指令显示加载状态,再做成插件让全项目复用。

第一步:写指令逻辑

// loading.js
export const vLoading = {
mounted(el, binding) {
if (binding.value) {
  // 创建一个遮罩层
  const overlay = document.createElement('div')
  overlay.className = 'loading-overlay'
  overlay.innerHTML = '<div class="spinner">⏳ 加载中...</div>'
  overlay.style.cssText = `
    position: absolute;
    top: 0; left: 0; right: 0; bottom: 0;
    background: rgba(0,0,0,0.5);
    display: flex;
    align-items: center;
    justify-content: center;
    color: white;
    font-size: 18px;
    border-radius: 4px;
  `

  // 父元素要 relative 才能放遮罩
  el.style.position = 'relative'
  el.appendChild(overlay)

  // 把 overlay 存到 el 上,后面 unmounted 要用到
  el._loadingOverlay = overlay
}
},

updated(el, binding) {
// 如果 loading 从 true 变成 false,移除遮罩
if (!binding.value && el._loadingOverlay) {
  el._loadingOverlay.remove()
  el._loadingOverlay = null
}
},

unmounted(el) {
// 清理工作,防止内存泄漏
if (el._loadingOverlay) {
  el._loadingOverlay.remove()
  el._loadingOverlay = null
}
}
}

第二步:做成插件

// loadingPlugin.js
import { vLoading } from './loading'

export default {
install(app) {
// 全局注册 v-loading 指令
app.directive('loading', vLoading)

// 顺便加一个全局方法 this.$showLoading()
app.config.globalProperties.$loading = {
  show: (el) => {
    el._manualLoading = true
    // 手动控制加载状态
  },
  hide: (el) => {
    el._manualLoading = false
  }
}
}
}

第三步:在 main.js 启用

import { createApp } from 'vue'
import App from './App.vue'
import loadingPlugin from './plugins/loadingPlugin'

const app = createApp(App)
app.use(loadingPlugin)  // 一行激活!
app.mount('#app')

第四步:在任何组件里用

<script setup>
import { ref } from 'vue'

const isLoading = ref(false)

const loadData = async () => {
isLoading.value = true
await new Promise(resolve => setTimeout(resolve, 2000))  // 模拟请求
isLoading.value = false
}
</script>

<template>
<div>
<!-- 给整个 div 加 loading 遮罩 -->
<div 
  v-loading="isLoading" 
  style="width: 300px; height: 200px; background: #f5f5f5; padding: 20px;"
>
  <p>这里是内容区域</p>
  <p>加载时会显示遮罩</p>
</div>

<button @click="loadData" style="margin-top: 20px; padding: 10px;">
  模拟加载数据
</button>
</div>
</template>

预期输出:点按钮后 2 秒内,灰色区域显示「⏳ 加载中...」遮罩,2 秒后消失。

一句话解释app.directive() 注册全局指令,app.use() 激活插件,一行代码全项目生效。


💪 进阶:常见坑 + 性能小贴士

坑 1:指令名大小写搞混

<!-- ❌ 错误:会自动转成 kebab-case -->
<script setup>
const vMyDirective = { /* ... */ }
</script>

<template>
<!-- 这个写的是 camelCase,不会生效 -->
<div vMyDirective>内容</div>
</template>
<!-- ✅ 正确:模板里用 kebab-case -->
<script setup>
const vMyDirective = { /* ... */ }
</script>

<template>
<div v-my-directive>内容</div>
</template>

原因:HTML 属性不区分大小写,Vue 自动把 camelCase 转成 kebab-case。


坑 2:binding.value 不是响应式的

// ❌ 错误写法:binding 在指令创建时捕获,之后不会更新
const vBad = {
mounted(el, binding) {
// 这里的 doSomething(binding.value) 只会在 mounted 时执行一次
doSomething(binding.value)
}
}
// ✅ 正确写法:用 updated 钩子监听变化
const vGood = {
mounted(el, binding) {
doSomething(binding.value)
},
updated(el, binding) {
// 当 binding.value 变化时,重新执行
if (binding.value !== binding.oldValue) {
  doSomething(binding.value)
}
}
}

原因mounted 只执行一次,updated 会在每次数据更新时触发。


坑 3:全局指令注册顺序

// ❌ 错误:在 createApp 之后注册全局指令
const app = createApp(App)
app.mount('#app')
app.directive('focus', { /* ... */ })  // 太晚了,指令已经编译完了
// ✅ 正确:在 mount 之前注册
const app = createApp(App)
app.directive('focus', { /* ... */ })  // 先注册
app.directive('loading', { /* ... */ }) // 再注册
app.mount('#app')  // 最后挂载

坑 4:指令里直接修改组件数据

<!-- ❌ 错误:在指令里直接改 prop,可能引发警告 -->
<script setup>
const vBad = {
mounted(el, binding, vnode) {
vnode.props.value = 'new value'  // 不要这样!
}
}
</script>
<!-- ✅ 正确:通过 emits 或事件修改 -->
<script setup>
const vGood = {
mounted(el, binding, vnode) {
// 用 context.emit 通知组件
vnode.emit('update:value', 'new value')
}
}
</script>

坑 5:插件里忘了 return

// ❌ 错误:install 不是箭头函数的隐式 return
app.use({
install(app) {
app.directive('focus', { /* ... */ })
// 缺少 return,但如果你想返回 Promise 或值就有问题
}
})

性能小贴士:避免在指令里频繁操作 DOM

// ❌ 性能差:每次 updated 都操作 DOM
const vInefficient = {
updated(el, binding) {
el.innerHTML = '<span>' + binding.value + '</span>'
}
}
// ✅ 性能好:只更新变化的属性
const vEfficient = {
updated(el, binding) {
el.textContent = binding.value  // 直接改文本,不重建 DOM
}
}

调试技巧:用 console.log 打印 binding

const vDebug = {
mounted(el, binding) {
console.log('=== 指令调试信息 ===')
console.log('el:', el)
console.log('binding.value:', binding.value)
console.log('binding.arg:', binding.arg)
console.log('binding.modifiers:', binding.modifiers)
console.log('binding.oldValue:', binding.oldValue)
console.log('========================')
}
}

提示:加一个 v-debug 指令专门用来调试,看看到底收到了什么值。


✏️ 练习题

练习 1(2 分钟):抄改聚焦指令

  • 输入:把项目 1 的 v-focus 指令改造成 v-select,让它自动选中输入框里的文字
  • 预期输出:输入框加载后,文字被蓝底白字选中
  • 提示:用 el.select() 而不是 el.focus()

练习 2(2 分钟):加一个条件判断

  • 输入:在项目 2 的 v-permission 里加逻辑,只有 role === 'admin' 并且 userState.isLocked === false 时才显示
  • 预期输出:加一个 isLocked: true 到 userState,admin 也看不到按钮了
  • 提示:在 mounted 里加一个 && 条件

练习 3(5 分钟):处理新的权限场景

  • 输入:新增一个「财务」角色,能看到「财务报表」按钮,但普通 user 和 editor 看不到
  • 预期输出:role='finance' 时显示按钮,其他角色隐藏
  • 提示:参考练习 2,把判断逻辑改成检查数组包含

练习 4(8 分钟):两个指令串起来

  • 输入:做一个 v-lazy-load 指令,只有页面滚动到元素可见时才显示图片;再结合 v-permission 让它只对会员可见
  • 预期输出:会员滚动到可视区才看到图片,非会员始终不显示
  • 提示:用 IntersectionObserver 做懒加载判断

练习 5(5 分钟):分析报错

  • 输入:用户写了以下代码,点击按钮后报错 Cannot read property 'style' of undefined
  • 预期输出:找到原因并修复
  • 提示:检查指令是否在正确的生命周期操作 DOM
<script setup>
const vBad = {
mounted(el, binding) {
el.style.background = binding.value
}
}
</script>

<template>
<div v-bad="'yellow'">内容</div>
</template>

作业:做一个「页面访问控制」插件

需求描述
做一个插件,包含以下功能:
1. v-auth 指令:根据用户权限控制元素显示
2. v-role-badge 指令:显示用户角色标签
3. 一个全局方法 $checkPermission(需要的权限) 返回布尔值

功能点
1. 权限配置可传入(支持数组形式的多个权限)
2. 隐藏元素时不是简单 display: none,而是有个「权限不足」占位符
3. 插件支持 debug 选项,开启后打印权限检查日志

加分项
1. 加一个 v-loading 状态指令
2. 支持权限指令在元素上显示「权限不足」的灰色遮罩和图标

验收标准
- 能跑起来
- 切换用户角色时,页面权限控制实时生效
- 控制台有清晰的调试日志


📚 总结 + 资源

本文学了 3 件事
1. 自定义指令 = 你自己写的「如果...那么...」动作,用 app.directive() 或局部 const vXxx = {}
2. 指令生命周期 = created → beforeMount → mounted → updated → unmounted
3. 插件 = 把指令、组件、全局方法打包成一个工具箱,用 app.use() 一行激活

延伸学习资源

资源 推荐理由
Vue3 官方自定义指令文档 最权威,有完整 API 说明
VueUse 源码 大量实战自定义指令,可以参考学习
《Vue.js 设计与实现》 从框架设计角度讲指令系统,理解更深入

互动钩子

你在项目里自己写过哪些自定义指令?有没有什么「只有自己知道」的实用指令?评论区聊聊,老粉优先回复!


下章预告
学会了自定义指令和插件,你肯定在想:「这些确实厉害,但有没有更优雅的方式复用逻辑?」——比如把逻辑抽出来但不用继承、不用 mixin?下一章我们来聊聊 mixin 与组合式函数的对比,看看 Vue3 里「复用代码」的正确姿势!

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。