Vue进阶实战:自定义指令与插槽的核心用法及实战案例

Vue进阶实战:自定义指令与插槽的核心用法及实战案例

在Vue的开发体系中,除了基础的指令和组件化开发,自定义指令插槽 是提升组件复用性、灵活性的两大核心特性。自定义指令能帮我们封装通用的DOM操作,插槽则让组件的结构定制化成为可能。本文将结合实际开发案例,从基础用法到实战封装,全面讲解Vue自定义指令和插槽的使用技巧,最后通过一个比特课程列表综合案例,将两大特性融会贯通。

一、Vue自定义指令:封装通用DOM操作

Vue提供了v-modelv-forv-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)简化写法

如果mountedupdated的逻辑完全一致,可直接用函数形式定义指令,简化代码:

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)使用步骤
  1. 组件内占位 :用<slot></slot>标记需要定制的区域;

  2. 使用时传值:将组件写成双标签,包裹需要展示的自定义结构。

(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)使用步骤
  1. 组件内命名占位<slot name="插槽名"></slot>

  2. 使用时定向传值<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)使用步骤
  1. 组件内绑定数据<slot :自定义属性="组件内部数据"></slot>

  2. 使用时接收数据<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. 需求分析

实现一个课程列表表格,包含序号、类别、课程名称、封面、特点、操作列,要求:

  1. 特点列支持双击编辑、回车新增、失焦隐藏,并封装为独立组件;

  2. 特点列的输入框实现自动聚焦 ,使用自定义指令v-focus

  3. 表格整体结构封装为通用组件,通过插槽实现列定制;

  4. 操作列支持删除课程,通过作用域插槽获取行索引;

  5. 课程特点支持双向绑定,修改后实时更新原数据。

2. 核心技术点

  1. 自定义指令v-focus:实现输入框自动聚焦;

  2. 作用域插槽:通用表格组件封装,定制操作列和特点列;

  3. 组件双向绑定:defineModel实现特点组件与父组件的数据双向同步;

  4. 列表交互:双击编辑、回车新增、确认删除等基础交互。

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. 最终效果

  1. 课程列表表格结构清晰,支持删除操作;

  2. 特点列双击可编辑,输入框自动聚焦,回车新增特点,失焦自动隐藏;

  3. 所有组件均为独立封装,可在项目中全局复用;

  4. 自定义指令和插槽的结合,让组件既满足通用需求,又支持个性化定制。

四、总结

Vue的自定义指令插槽是提升组件开发效率的两大核心利器,二者各司其职、又能完美结合:

  • 自定义指令 :专注于DOM操作的封装,解决"通用DOM操作重复写"的问题,比如聚焦、懒加载、样式修改等,支持全局注册,一次定义、全局使用;

  • 插槽 :专注于组件结构的定制,从默认插槽到具名插槽,再到作用域插槽,逐步实现"单区域定制→多区域定制→带数据的定制",让组件摆脱"结构固定"的限制,变得灵活可复用。

在实际开发中,我们要学会将通用逻辑封装为自定义指令 ,将通用结构封装为带插槽的组件,二者结合能实现高复用、高灵活的Vue组件体系。本文的案例均来自实际开发场景,掌握这些技巧后,你能轻松应对Vue中大部分的组件封装和交互需求。

相关推荐
西门吹-禅2 小时前
【iFLow skills】
前端·chrome
春波petal2 小时前
MacOS 13.7.8版本-前端环境一键搭建指南
前端·macos
Never_Satisfied2 小时前
在JavaScript / HTML中,触发某个对象的click事件
开发语言·javascript·html
许同2 小时前
JS-WPS 自动化办公(5)多Sheet整合
开发语言·前端·javascript
_OP_CHEN2 小时前
【前端开发之JavaScript】(四)JS基础语法下篇:函数与对象核心要点深度解析
开发语言·前端·javascript·界面开发·前端开发·网页开发·语法基础
henry1010102 小时前
通过GitHub Page服务免费部署静态Web网站
前端·html·github·html5
少云清2 小时前
【UI自动化测试】3_web自动化测试 _Selenium-IDE
前端·selenium·web自动化测试
强子感冒了2 小时前
JavaScript学习笔记:函数、方法与继承(原型与class)
javascript·笔记·学习
明月_清风2 小时前
你真的懂 JSON 吗?那些被忽略的底层边界与性能陷阱
前端·json