Code前端首页关于Code前端联系我们

Vue3中如何判断插槽slot是否存在?有没有几种通用实用的方法?

terry 1小时前 阅读数 13 #Vue

很多刚从Vue2转Vue3的开发者,或者刚开始学Vue3组件开发的朋友,都会遇到这个问题:做一个通用组件时,比如带侧边栏的布局、带关闭按钮的弹窗、带自定义头部的卡片,父组件可能传某个插槽,也可能不传,甚至传了空占位或者注释,这时候怎么精准控制组件的显示逻辑?别慌,这篇文章会从Vue3的slot核心机制讲起,给你4种通用实用的判断方法,还有避坑指南和扩展场景,看完就能直接用在项目里。

先搞懂 Vue3 中 slot 的核心机制,判断起来更稳 别着急写代码,先搞清楚底层逻辑,不然很容易踩“空槽也算存在”的低级错误,在Vue2里,slot大多是直接存在this.$slots里的vnode数组,判断起来比较直观,但Vue3做了彻底的重构——不管是组合式API里的useSlots()返回的对象,还是选项式API里的this.$slots,都是一个Proxy代理对象。 这个Proxy对象里的每一个属性,对应你组件里提前定义的具名插槽,还有默认的“default”插槽(就是父组件直接写在组件标签里的内容,不用加#default的那种),那属性的值是什么呢?这里有个关键的区别: 如果父组件完全没碰过这个插槽(不管是显式写空还是带内容),那这个属性就是undefined; 如果父组件哪怕只是写了个占位(lt;template #header>、只有注释、只有一串空格),那这个属性都是一个渲染函数,必须执行这个函数之后,才能拿到真正的vnode数组,进而判断里面有没有实际内容。 哦对了,作用域插槽的判断逻辑和普通具名插槽完全一样——不管你传没传v-slot的参数,属性名还是那个插槽名,值还是渲染函数,不用单独处理。

用 setup 组合式 API 里的 useSlots() 来判断 这应该是现在Vue3项目里用得最多的方法了,尤其是配合script setup语法糖,写起来非常简洁,首先要从vue里引入useSlots(),然后在setup的顶层调用它,就能拿到响应式的slots对象。 接下来要区分两种最常见的需求,不同需求对应不同的判断代码: 一种是只要父组件传了插槽定义(不管空不空位),就显示相关结构——这种需求比较少见,比如有的设计稿要求哪怕头部是空的,也要留个灰色的占位条; 另一种是只有父组件传了有实际可渲染内容的插槽,才显示——这个才是绝大多数通用组件的需求,比如带自定义标题的卡片,父组件没传标题或者传了空注释,就不要浪费空间显示头部。 我们可以用一个封装好的辅助函数isSlotFilled来处理第二种需求,它会过滤掉注释节点和空白文本节点,只保留有意义的内容。 给你看一个完整的MyCard组件例子,用的是script setup:

<script setup>
import { useSlots } from 'vue'
// 1. 获取响应式的slots对象
const slots = useSlots()
// 2. 封装通用辅助函数:判断插槽是否有实际内容
const isSlotFilled = (slotName) => {
  // 先检查插槽函数是否存在,不存在直接返回false
  const slot = slots[slotName]
  if (!slot) return false
  // 执行渲染函数拿到vnode数组
  const vnodes = slot()
  // 遍历vnode数组,过滤掉无效内容
  return vnodes.some(node => {
    // 注释节点直接跳过
    if (node.type === Comment) return false
    // 文本节点检查是否只有空白字符
    if (node.type === Text) {
      return node.content.trim() !== ''
    }
    // 其他节点(div、span、自定义组件、图片等)都算有内容
    return true
  })
}
</script>
<template>
  <div class="my-card">
    <!-- 需求1:只要传了header定义就显示(灰色占位条) -->
    <!-- <div v-if="slots.header" class="card-header">
      <slot name="header"></slot>
    </div> -->
    <!-- 需求2:只有传了实际内容的header才显示 -->
    <div v-if="isSlotFilled('header')" class="card-header">
      <slot name="header"></slot>
    </div>
    <div class="card-body">
      <!-- 默认插槽,用isSlotFilled('default')也可以判断 -->
      <slot></slot>
    </div>
    <!-- 同样用实际内容判断footer -->
    <div v-if="isSlotFilled('footer')" class="card-footer">
      <slot name="footer"></slot>
    </div>
  </div>
</template>
<style scoped>
.my-card {
  border: 1px solid #e0e0e0;
  border-radius: 8px;
  overflow: hidden;
  margin: 20px auto;
  max-width: 600px;
}
.card-header {
  background-color: #f5f7fa;
  padding: 12px 16px;
  font-weight: 600;
  border-bottom: 1px solid #e0e0e0;
}
.card-body {
  padding: 16px;
  line-height: 1.6;
}
.card-footer {
  padding: 12px 16px;
  border-top: 1px solid #e0e0e0;
  text-align: right;
}
</style>

再给你写个父组件App.vue的测试代码,覆盖了所有可能的情况,你可以复制粘贴直接运行看效果:

<template>
  <div class="app-container">
    <!-- 测试1:完全不传header和footer -->
    <MyCard>
      <p>这是完全不传头部和底部的卡片内容</p>
    </MyCard>
    <!-- 测试2:传空的header占位 -->
    <MyCard>
      <template #header></template>
      <p>这是传了空头部占位的卡片内容</p>
    </MyCard>
    <!-- 测试3:传只有注释的header -->
    <MyCard>
      <template #header><!-- 这是一条没用的头部注释 --></template>
      <p>这是传了带注释头部的卡片内容</p>
    </MyCard>
    <!-- 测试4:传只有空格的header -->
    <MyCard>
      <template #header>   </template>
      <p>这是传了只有空格头部的卡片内容</p>
    </MyCard>
    <!-- 测试5:传有实际内容的header和footer -->
    <MyCard>
      <template #header>
        <span>📌</span> 这是带图标的自定义头部
      </template>
      <p>这是传了完整头部和底部的卡片内容,看起来是不是顺眼多了?</p>
      <template #footer>
        <button>查看详细内容</button>
      </template>
    </MyCard>
  </div>
</template>
<script setup>
import MyCard from './components/MyCard.vue'
</script>
<style scoped>
.app-container {
  padding: 20px;
  max-width: 800px;
  margin: 0 auto;
}
button {
  padding: 6px 12px;
  background-color: #409eff;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  transition: opacity 0.2s;
}
button:hover {
  opacity: 0.8;
}
</style>

选项式 API 里的 this.$slots 也能用,逻辑和组合式差不多 如果你的项目是刚从Vue2升级过来的,或者习惯用选项式API开发,那完全不用慌——直接用this.$slots就行,逻辑和组合式API里的useSlots()返回的对象一模一样,都是Proxy代理对象,属性值也是渲染函数。 只需要把刚才的辅助函数放到methods里,模板里用isSlotFilled('插槽名')调用,$slots代替slots就行,给你看简化版的MyCard选项式代码:

<script>
export default {
  name: 'MyCardOption',
  methods: {
    isSlotFilled(slotName) {
      const slot = this.$slots[slotName]
      if (!slot) return false
      const vnodes = slot()
      return vnodes.some(node => {
        if (node.type === Comment) return false
        if (node.type === Text) return node.content.trim() !== ''
        return true
      })
    }
  }
}
</script>
<template>
  <div class="my-card">
    <div v-if="isSlotFilled('header')" class="card-header">
      <slot name="header"></slot>
    </div>
    <div class="card-body">
      <slot></slot>
    </div>
    <div v-if="isSlotFilled('footer')" class="card-footer">
      <slot name="footer"></slot>
    </div>
  </div>
</template>
<!-- CSS和之前的完全一样,就不重复写了 -->

在模板里直接判断?不用写 JS 逻辑的小技巧 有时候你不想写额外的JS代码,比如只是做个超简单的组件,不需要封装辅助函数,那能不能直接在模板里判断?当然可以! Vue3的模板里可以直接访问$slots这个全局对象,和选项式API里的this.$slots是同一个东西。 如果你的需求是只要父组件传了插槽定义就显示,那直接写一行v-if就行,

<template>
  <div class="simple-card">
    <div v-if="$slots.header" class="simple-header">
      <slot name="header"></slot>
    </div>
    <div class="simple-body">
      <slot></slot>
    </div>
  </div>
</template>

如果你的需求是简单判断有没有非空内容(不用过滤注释的话),那可以用可选链?.直接调用$slots.header(),然后判断返回的vnode数组长度,

<div v-if="$slots.header?.().length > 0" class="simple-header">
  <slot name="header"></slot>
</div>

不过要注意,这个简单写法不会过滤注释节点和空白文本节点——如果父组件传了只有注释的header,$slots.header().length会是1,还是会显示头部,所以这个方法只适合和你自己的团队配合,而且团队约定不会传空占位或者注释的场景。

用 VueUse 的 useSlots 包装?TypeScript 项目更友好 现在很多Vue3项目都用上了TypeScript,而且也会集成VueUse这个生态工具库——它里面有很多封装好的实用函数,能帮你省不少事。 VueUse里也有一个useSlots函数,名字和官方的一样,但类型定义比官方的更细致,还能配合你自己定义的Slots接口使用,避免类型错误。 首先要安装VueUse,根据你的包管理器选一个命令:

  • npm i @vueuse/core
  • pnpm add @vueuse/core
  • yarn add @vueuse/core 然后在script setup里引入,为了避免和官方的useSlots重名(当然如果项目里没自己定义的话也可以直接用),可以给它起个别名,比如useVueUseSlots:
    <script setup lang="ts">
    // 引入VueUse的useSlots,起别名避免重名
    import { useSlots as useVueUseSlots } from '@vueuse/core'

// 定义你组件的插槽类型,提高代码的类型安全性 interface CardSlots { header?: () => any[] default?: () => any[] footer?: () => any[] }

// 传入泛型,获得更好的类型提示 const slots = useVueUseSlots

// 同样的辅助函数,加上TypeScript类型 const isSlotFilled = (slotName: keyof CardSlots) => { const slot = slots[slotName] if (!slot) return false const vnodes = slot() return vnodes.some(node => { if (node.type === Comment) return false if (node.type === Text) return node.content.trim() !== '' return true }) }

```

Vue3 判断 slot 存在时最容易踩的 3 个坑 刚才讲核心机制和方法的时候已经提到了一些,但这里再单独列出来强调一遍,避免你在项目里踩雷: 第一个坑是空插槽、注释插槽、空白文本插槽都算“插槽已定义”——新手最容易犯这个错误,直接写v-if="$slots.header",结果父组件传了空占位,头部还是显示了,白白占了空间,影响页面美观。 第二个坑是具名插槽和作用域插槽的判断有区别——很多人以为作用域插槽传了参数,判断方式会不一样,但其实完全相同,都是用slots.插槽名或者$slots.插槽名访问,不管有没有传v-slot的参数。 第三个坑是忘记默认插槽的名字是“default”——默认插槽如果父组件直接写在组件标签里,不用加#default,但判断的时候必须用isSlotFilled('default')或者v-if="$slots.default",不能省略名字。

基于 slot 判断可以实现的小优化和功能 学会了判断slot是否存在,还能用来做很多实用的功能,让你的通用组件更灵活、更智能: 第一个功能是动态调整组件的样式——比如MyCard如果有header插槽,就给body加padding-top: 0,避免重复留白;如果有header和footer,就给整个卡片加个渐变背景,用动态class或者style就能实现,

<div class="my-card" :class="{ 'has-header': isSlotFilled('header'), 'has-footer': isSlotFilled('footer') }">

然后在CSS里写对应的样式就行。 第二个功能是动态显示或者隐藏装饰性元素——比如如果有footer插槽,就显示footer上面的分割线;如果有header插槽,就显示header旁边的关闭按钮(当然关闭按钮也可以做成单独的插槽)。 第三个功能是做更灵活的“插槽 fallback”——就是如果父组件传了某个插槽,就用父组件的;如果没传,就用组件自带的默认内容,其实Vue3本身就有插槽默认内容的功能,但结合isSlotFilled,可以实现更复杂的逻辑,比如只有父组件传了非空内容才用父组件的,否则用默认内容;或者根据父组件传的插槽数量调整布局。 给你看一个灵活的MyForm表单组件例子:

<script setup>
import { useSlots } from 'vue'
const slots = useSlots()
const isSlotFilled = (slotName) => {
  const slot = slots[slotName]
  if (!slot) return false
  const vnodes = slot()
  return vnodes.some(node => {
    if (node.type === Comment) return false
    if (node.type === Text) return node.content.trim() !== ''
    return true
  })
}
const handleSubmit = () => {
  alert('表单提交成功!')
}
</script>
<template>
  <form class="my-form" @submit.prevent="handleSubmit">
    <div class="form-body">
      <slot></slot>
    </div>
    <div class="form-footer">
      <!-- 如果父组件传了非空的submit插槽,就用父组件的;否则用默认的提交按钮 -->
      <slot name="submit">
        <button type="submit" class="submit-btn">提交</button>
      </slot>
      <!-- 只有父组件传了非空的reset插槽,才显示重置按钮 -->
      <slot name="reset" v-if="isSlotFilled('reset')"></slot>
    </div>
  </form>
</template>
<style scoped>
.my-form {
  border: 1px solid #e0e0e0;
  border-radius: 8px;
  padding: 16px;
  margin: 20px auto;
  max-width: 600px;
}
.form-body {
  margin-bottom: 16px;
}
.form-body input {
  width: 100%;
  padding: 8px 12px;
  border: 1px solid #dcdfe6;
  border-radius: 4px;
  box-sizing: border-box;
  font-size: 14px;
}
.form-footer {
  text-align: right;
  gap: 8px;
  display: flex;
  justify-content: flex-end;
}
.submit-btn {
  padding: 6px 12px;
  background-color: #409eff;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}
</style>

日常开发中,你可以根据自己的项目情况和开发习惯灵活选择方法: 如果你用的是组合式API,尤其是现在主流的script setup语法,直接用Vue官方提供的useSlots()就行,然后根据需求写对应的判断逻辑,或者封装成通用的isSlotFilled辅助函数,放在utils文件夹里,以后所有组件都能复用。 如果你还在维护旧项目,用的是选项式API,那this.$slots完全够用,逻辑和组合式的一模一样,辅助函数可以放在methods里调用。 如果你只是做超简单的组件,和自己的团队有明确的约定(不会传空占位或者注释),那直接在模板里写v-if="$slots.插槽名"或者v-if="$slots.插槽名?.().length > 0"就行,一行代码搞定。 如果你的项目用了TypeScript,想要更好的类型提示,避免类型错误,那可以试试VueUse的useSlots,它的类型定义比官方的更细致,还能配合你自己定义的Slots接口使用。 不管选哪种方法,一定要先想清楚自己的需求:是只要父组件传了插槽占位就算,还是必须有实际可渲染的内容?这个是避免踩坑的核心。

版权声明

本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。

热门