Vue进阶实战:自定义指令与插槽的核心用法及实战案例
在Vue的开发体系中,除了基础的指令和组件化开发,自定义指令 和插槽 是提升组件复用性、灵活性的两大核心特性。自定义指令能帮我们封装通用的DOM操作,插槽则让组件的结构定制化成为可能。本文将结合实际开发案例,从基础用法到实战封装,全面讲解Vue自定义指令和插槽的使用技巧,最后通过一个比特课程列表综合案例,将两大特性融会贯通。
一、Vue自定义指令:封装通用DOM操作
Vue提供了v-model、v-for、v-bind等内置指令,满足日常开发的基础需求,但在实际项目中,我们常常需要重复执行某些DOM操作,比如元素自动聚焦、图片懒加载、样式动态修改等。此时,自定义指令就是最优解,它能将公共的DOM操作封装起来,实现一次定义、全局复用。
1. 自定义指令的核心基础
(1)核心作用
封装一段公共的DOM操作代码,减少重复开发,提升代码可维护性。
(2)基本使用步骤
自定义指令的使用分为注册 和使用 两步,且支持全局注册(在main.js中),注册后可在项目任意组件中使用。
JavaScript
// main.js 全局注册自定义指令
app.directive('指令名', {
// 元素挂载为真实DOM后自动执行一次
mounted(el) {
// el:指令绑定的DOM元素,可直接操作
}
})
// 组件中使用
<p v-指令名></p>
(3)基础案例:元素自动聚焦
实现页面加载时,输入框自动获取焦点,这是自定义指令最经典的基础案例:
JavaScript
// main.js 注册v-focus指令
app.directive('focus', {
mounted(el) {
el.focus() // 操作DOM:让元素聚焦
}
})
// 组件中使用
<input type="text" v-focus />
2. 带参数的自定义指令:动态绑定数据
实际开发中,指令往往需要根据外部数据动态调整,比如动态修改元素文字颜色。此时可通过指令传值 +binding.value获取参数,同时配合updated钩子实现数据更新时的指令重执行。
(1)核心语法
JavaScript
// 注册带参数的指令
app.directive('指令名', {
mounted(el, binding) {
// binding.value:获取指令绑定的参数值
},
updated(el, binding) {
// 数据更新时触发,与mounted逻辑一致
}
})
// 组件中传值
<div v-指令名="参数值"></div>
(2)案例:动态修改文字颜色
JavaScript
// main.js 注册v-color指令
app.directive('color', {
mounted(el, binding) {
el.style.color = binding.value
},
updated(el, binding) {
el.style.color = binding.value
}
})
// 组件中使用
<script setup>
import { ref } from 'vue'
const colorStr = ref('red') // 动态修改颜色
</script>
<template>
<p v-color="colorStr">动态变色的文字</p>
</template>
(3)简化写法
如果mounted和updated的逻辑完全一致,可直接用函数形式定义指令,简化代码:
JavaScript
app.directive('color', (el, binding) => {
el.style.color = binding.value // 同时在mounted和updated执行
})
3. 实战案例:图片懒加载指令v-lazyload
图片懒加载是前端性能优化的必备方案,核心逻辑是图片进入可视区后再加载真实地址 ,避免一次性加载大量图片导致页面卡顿。我们可以封装v-lazyload指令,实现全局复用。
(1)核心技术:IntersectionObserver
使用浏览器原生APIIntersectionObserver(交叉监视器),监听元素是否进入可视区,替代传统的滚动监听,性能更优。
(2)完整实现
JavaScript
// main.js 注册v-lazyload指令
app.directive('lazyload', (el, binding) => {
// el:img标签 binding.value:图片真实地址
const io = new IntersectionObserver(([entry]) => {
// entry.isIntersecting:判断元素是否进入可视区
if (entry.isIntersecting) {
el.src = binding.value // 加载真实图片
// 图片加载错误处理
el.addEventListener('error', (err) => {
console.log('图片加载失败:', err)
})
io.unobserve(el) // 停止监听当前元素
io.disconnect() // 关闭监听器
}
})
io.observe(el) // 开启监听
})
// 组件中使用
<script setup>
const imgList = ref([// 多张图片地址数组])
</script>
<template>
<img v-for="url in imgList" :key="url" v-lazyload="url" />
</template>
该指令实现后,项目中所有需要懒加载的图片,只需绑定v-lazyload并传入真实地址即可,无需重复编写逻辑。
二、Vue插槽:让组件结构可自定义
组件化开发的核心是复用 ,但很多时候组件的整体结构固定,局部结构需要灵活定制 ,比如折叠面板的内容、表格的操作列、卡片的主体区域等。Vue的插槽(Slot) 就是为解决这个问题而生,它本质是组件内的占位符,使用组件时可传入自定义结构,替换占位符,实现组件结构的定制化。
Vue的插槽分为三大类:默认插槽 、具名插槽 、作用域插槽,从简单到复杂,满足不同的定制化需求。
1. 默认插槽:单区域结构定制
(1)核心作用
为组件的单个不确定区域提供占位,使用组件时传入自定义结构,替换该占位符。
(2)使用步骤
-
组件内占位 :用
<slot></slot>标记需要定制的区域; -
使用时传值:将组件写成双标签,包裹需要展示的自定义结构。
(3)案例:折叠面板组件定制
Plain
// 折叠面板组件 bit-panel.vue
<template>
<div class="panel">
<div class="title" @click="visible = !visible">
<h4>折叠面板</h4>
<span>{{ visible ? '收起' : '展开' }}</span>
</div>
<div class="container" v-show="visible">
<!-- 插槽占位:内容区域可自定义 -->
<slot></slot>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const visible = ref(false)
</script>
// 父组件中使用:传入不同内容
<template>
<!-- 传入文字 -->
<bit-panel>
<p>生命诚可贵,爱情价更高</p>
</bit-panel>
<!-- 传入图片 -->
<bit-panel>
<img src="./assets/img.png" alt="图片" />
</bit-panel>
</template>
(4)插槽默认值
如果使用组件时未传入自定义结构 ,插槽区域会显示空白,可给<slot>设置默认内容,提升组件体验:
Plain
// bit-panel.vue 给插槽设置默认值
<slot>
<p>默认展示的内容</p>
</slot>
效果:传内容则显示传入的结构,不传则显示默认内容。
2. 具名插槽:多区域结构定制
默认插槽只能处理单个不确定区域 ,如果组件中有多处需要定制的结构 (比如折叠面板的标题+内容、卡片的头部+主体+底部),就需要使用具名插槽。
(1)核心作用
为组件的多个不确定区域 分别占位,通过name属性区分,实现精准的结构定制。
(2)使用步骤
-
组件内命名占位 :
<slot name="插槽名"></slot>; -
使用时定向传值 :
<template #插槽名>包裹对应结构(#是v-slot:的简写)。
(3)案例:折叠面板标题+内容双定制
Plain
// bit-panel.vue 具名插槽占位
<template>
<div class="panel">
<div class="title" @click="visible = !visible">
<!-- 标题插槽:name="title" -->
<slot name="title"><h4>默认标题</h4></slot>
<span>{{ visible ? '收起' : '展开' }}</span>
</div>
<div class="container" v-show="visible">
<!-- 内容插槽:name="body" -->
<slot name="body"><p>默认内容</p></slot>
</div>
</div>
</template>
// 父组件中使用:定向传入标题和内容
<template>
<bit-panel>
<template #title>
<b>登鹳雀楼</b> <!-- 定制标题 -->
</template>
<template #body>
<p>白日依山尽,黄河入海流</p> <!-- 定制内容 -->
</template>
</bit-panel>
</template>
3. 作用域插槽:带数据的结构定制
默认插槽和具名插槽仅实现结构定制 ,但实际开发中,定制结构时往往需要使用组件内部的数据 (比如表格的操作列需要获取当前行数据)。作用域插槽 就是带数据的插槽,组件内给插槽绑定数据,使用组件时可接收并使用该数据,让组件的灵活性和复用性达到极致。
(1)核心作用
让插槽携带组件内部的数据,实现"结构定制+数据复用"的双重需求。
(2)使用步骤
-
组件内绑定数据 :
<slot :自定义属性="组件内部数据"></slot>; -
使用时接收数据 :
<template #插槽名="接收对象">,通过接收对象.自定义属性使用数据(支持解构赋值)。
(3)经典案例:通用表格组件封装
表格是前端开发中最常用的组件,不同业务的表格结构一致,操作列不同(比如有的是"查看",有的是"删除"),用作用域插槽封装通用表格组件,可实现一次封装、全局复用。
Plain
// 通用表格组件 bit-table.vue
<template>
<table class="bit-table">
<thead>
<tr>
<th>序号</th>
<th>名称</th>
<th>价格</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="(item, index) in data" :key="item.id">
<td>{{ index + 1 }}</td>
<td>{{ item.name }}</td>
<td>{{ item.price }}</td>
<!-- 作用域插槽:绑定当前行数据item和索引index -->
<td><slot :row="item" :i="index"></slot></td>
</tr>
</tbody>
</table>
</template>
<script setup>
const props = defineProps({
data: { type: Array, default: () => [] } // 接收父组件传入的表格数据
})
</script>
// 父组件中使用:接收数据并定制操作列
<template>
<!-- 表格1:操作列为"查看" -->
<bit-table :data="tableData1">
<template #default="{ row }">
<span @click="inspect(row)">查看</span>
</template>
</bit-table>
<!-- 表格2:操作列为"删除" -->
<bit-table :data="tableData2">
<template #default="{ row, i }">
<button @click="del(row, i)">删除</button>
</template>
</bit-table>
</template>
<script setup>
import { ref } from 'vue'
const tableData1 = ref([/* 数据1 */])
const tableData2 = ref([/* 数据2 */])
const inspect = (row) => { alert('查看:' + JSON.stringify(row)) }
const del = (row, i) => { tableData2.value.splice(i, 1) }
</script>
上述案例中,通用表格组件bit-table只负责渲染表格结构和公共数据,操作列通过作用域插槽交给父组件定制,同时父组件能获取到表格的行数据,实现个性化操作,这就是作用域插槽的核心价值。
三、综合实战:比特课程列表组件封装
掌握了自定义指令和插槽的核心用法后,我们通过一个比特课程列表综合案例 ,将两大特性结合起来,实现一个可定制、可交互、高复用的实战组件。
1. 需求分析
实现一个课程列表表格,包含序号、类别、课程名称、封面、特点、操作列,要求:
-
特点列支持双击编辑、回车新增、失焦隐藏,并封装为独立组件;
-
特点列的输入框实现自动聚焦 ,使用自定义指令
v-focus; -
表格整体结构封装为通用组件,通过插槽实现列定制;
-
操作列支持删除课程,通过作用域插槽获取行索引;
-
课程特点支持双向绑定,修改后实时更新原数据。
2. 核心技术点
-
自定义指令
v-focus:实现输入框自动聚焦; -
作用域插槽:通用表格组件封装,定制操作列和特点列;
-
组件双向绑定:
defineModel实现特点组件与父组件的数据双向同步; -
列表交互:双击编辑、回车新增、确认删除等基础交互。
3. 核心组件实现
(1)自定义指令v-focus:全局注册
JavaScript
// main.js
app.directive('focus', {
mounted(el) {
el.focus() // 输入框挂载后自动聚焦
}
})
(2)特点组件bit-feature.vue:封装编辑交互
Plain
<template>
<div class="bit-feature">
<!-- 输入框:v-focus实现自动聚焦,回车新增、失焦隐藏 -->
<input
v-focus
v-if="visible"
v-model.trim="f"
class="ipt"
placeholder="请输入特点"
@blur="hide"
@keydown.enter="add"
/>
<!-- 特点展示:双击显示输入框 -->
<div class="feature" v-else @dblclick="show">
<span v-for="(item, index) in model" :key="index">{{ item }}</span>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const model = defineModel() // 双向绑定父组件数据
const visible = ref(false) // 控制输入框显示/隐藏
const f = ref('') // 输入框绑定值
// 显示输入框
const show = () => { visible.value = true }
// 隐藏输入框并清空
const hide = () => {
visible.value = false
f.value = ''
}
// 新增特点
const add = () => {
if (f.value) {
model.value.push(f.value)
hide()
}
}
</script>
(3)通用表格组件bit-table.vue:插槽封装
Plain
<template>
<table class="bit-table">
<thead>
<tr>
<!-- 表头插槽:定制表头列 -->
<slot name="thead"></slot>
</tr>
</thead>
<tbody>
<!-- 表体插槽:绑定行数据row和索引i -->
<tr v-for="(item, index) in data" :key="item.id">
<slot :row="item" :i="index"></slot>
</tr>
</tbody>
</table>
</template>
<script setup>
const props = defineProps({
data: { type: Array, default: () => [] }
})
</script>
(4)父组件整合:课程列表展示与交互
Plain
<template>
<h1>比特就业课课程列表</h1>
<bit-table :data="courseList">
<!-- 定制表头 -->
<template #thead>
<th>序号</th>
<th>类别</th>
<th>名称</th>
<th>封面</th>
<th>特点</th>
<th>操作</th>
</template>
<!-- 定制表体:接收行数据row和索引i -->
<template #default="{ row, i }">
<td>{{ i + 1 }}</td>
<td>{{ row.type }}</td>
<td>{{ row.title }}</td>
<td><img :src="row.cover" :alt="row.title" width="240" /></td>
<td><bit-feature v-model="row.feature" /></td>
<td><button @click="del(i)">删除</button></td>
</template>
</bit-table>
</template>
<script setup>
import { ref } from 'vue'
import BitTable from './components/bit-table.vue'
import BitFeature from './components/bit-feature.vue'
// 课程列表数据
const courseList = ref([
{ id: 101001, title: 'C语言刷题', type: 'C语言', cover: 'xxx.jpg', feature: ['通俗易懂', '上手快'] },
{ id: 101002, title: 'C++系统就业课', type: 'C++', cover: 'xxx.jpg', feature: ['全面', '高效'] },
// 更多课程...
])
// 删除课程
const del = (i) => {
if (window.confirm('确认删除该课程吗?')) {
courseList.value.splice(i, 1)
}
}
</script>
4. 最终效果
-
课程列表表格结构清晰,支持删除操作;
-
特点列双击可编辑,输入框自动聚焦,回车新增特点,失焦自动隐藏;
-
所有组件均为独立封装,可在项目中全局复用;
-
自定义指令和插槽的结合,让组件既满足通用需求,又支持个性化定制。
四、总结
Vue的自定义指令 和插槽是提升组件开发效率的两大核心利器,二者各司其职、又能完美结合:
-
自定义指令 :专注于DOM操作的封装,解决"通用DOM操作重复写"的问题,比如聚焦、懒加载、样式修改等,支持全局注册,一次定义、全局使用;
-
插槽 :专注于组件结构的定制,从默认插槽到具名插槽,再到作用域插槽,逐步实现"单区域定制→多区域定制→带数据的定制",让组件摆脱"结构固定"的限制,变得灵活可复用。
在实际开发中,我们要学会将通用逻辑封装为自定义指令 ,将通用结构封装为带插槽的组件,二者结合能实现高复用、高灵活的Vue组件体系。本文的案例均来自实际开发场景,掌握这些技巧后,你能轻松应对Vue中大部分的组件封装和交互需求。