Vue3 插槽(Slot)使用

插槽是 Vue 中一种强大的内容分发机制,允许组件接收外部传入的内容并进行渲染。下面我通过简单易懂的示例详细说明。

📚 插槽的基本概念

定义 :插槽是组件内部预留的"占位符",允许父组件向子组件传递模板内容(HTML结构、组件等)。

类比理解:就像电脑的 USB 接口(插槽),你可以插入 U盘、鼠标、键盘等(内容),接口本身是固定的,但插入的内容可以变化。


🔧 三种插槽类型

类型 数量 特点 使用场景
默认插槽 1个 没有名称 通用内容分发
具名插槽 多个 有名称标识 多个内容位置
作用域插槽 不限 可传递数据 子组件向父组件传数据

一、默认插槽

基本示例

vue 复制代码
<!-- ChildComponent.vue -->
<template>
  <div class="card">
    <div class="card-header">
      <!-- 默认插槽:没有 name 属性 -->
      <slot></slot>
    </div>
    <div class="card-body">
      <p>这是卡片的内容区域</p>
    </div>
  </div>
</template>
vue 复制代码
<!-- ParentComponent.vue -->
<template>
  <div>
    <!-- 使用子组件 -->
    <ChildComponent>
      <!-- 这里的内容会替换子组件中的 <slot></slot> -->
      <h2>自定义标题</h2>
      <p>这是自定义的内容</p>
    </ChildComponent>
    
    <!-- 也可以传递组件 -->
    <ChildComponent>
      <CustomButton>点击我</CustomButton>
    </ChildComponent>
    
    <!-- 不传内容时显示默认内容 -->
    <ChildComponent></ChildComponent>
  </div>
</template>

输出结果

html 复制代码
<div class="card">
  <div class="card-header">
    <h2>自定义标题</h2>
    <p>这是自定义的内容</p>
  </div>
  <div class="card-body">
    <p>这是卡片的内容区域</p>
  </div>
</div>

带默认内容的插槽

vue 复制代码
<!-- ChildComponent.vue -->
<template>
  <div class="notification">
    <!-- 默认内容:当父组件不传内容时显示 -->
    <slot>
        <p>默认通知:没有新消息</p>
    </slot>
  </div>
</template>
vue 复制代码
<!-- ParentComponent.vue -->
<template>
  <div>
    <!-- 不传内容,显示默认 -->
    <NotificationComponent />
    
    <!-- 传递自定义内容 -->
    <NotificationComponent>
      <p style="color: red;">⚠️ 有紧急消息!</p>
    </NotificationComponent>
  </div>
</template>

二、具名插槽

基本用法

vue 复制代码
<!-- LayoutComponent.vue -->
<template>
  <div class="layout">
    <!-- 具名插槽:有 name 属性 -->
    <header>
      <slot name="header"></slot>
    </header>
    
    <main>
      <!-- 默认插槽(可以省略 name 或写成 name="default") -->
      <slot></slot>
    </main>
    
    <footer>
      <slot name="footer"></slot>
    </footer>
    
    <aside>
      <slot name="sidebar"></slot>
    </aside>
  </div>
</template>

<style scoped>
.layout {
  display: grid;
  grid-template-areas: 
    "header header"
    "sidebar main"
    "footer footer";
  grid-template-columns: 200px 1fr;
  gap: 20px;
}
header { grid-area: header; background: #f0f0f0; padding: 20px; }
main { grid-area: main; background: white; padding: 20px; }
footer { grid-area: footer; background: #333; color: white; padding: 20px; }
aside { grid-area: sidebar; background: #f5f5f5; padding: 20px; }
</style>
vue 复制代码
<!-- ParentComponent.vue -->
<template>
  <LayoutComponent>
    <!-- 使用 v-slot:name 或 #name 指定插槽 -->
    <template v-slot:header>
      <h1>网站标题</h1>
      <nav>
        <a href="/">首页</a>
        <a href="/about">关于</a>
      </nav>
    </template>
    
    <!-- 简写:使用 # 代替 v-slot: -->
    <template #sidebar>
      <h3>侧边栏</h3>
      <ul>
        <li>菜单1</li>
        <li>菜单2</li>
      </ul>
    </template>
    
    <!-- 默认插槽(不指定名字的内容) -->
    <h2>主要内容区域</h2>
    <p>这里是页面的主要内容...</p>
    
    <template #footer>
      <p>© 2024 版权所有</p>
      <p>联系邮箱:contact@example.com</p>
    </template>
  </LayoutComponent>
</template>

多种写法对比

vue 复制代码
<!-- 完整写法 -->
<template v-slot:header>内容</template>

<!-- 简写1 -->
<template #header>内容</template>

<!-- 简写2(动态插槽名) -->
<template #[dynamicSlotName]>内容</template>

<!-- 简写3(在组件上使用,不推荐) -->
<LayoutComponent #header>内容</LayoutComponent>

三、作用域插槽

基本概念

作用域插槽允许子组件向父组件传递数据,父组件可以使用这些数据来渲染插槽内容。

vue 复制代码
<!-- UserList.vue (子组件) -->
<template>
  <div class="user-list">
    <h3>用户列表</h3>
    <ul>
      <li v-for="user in users" :key="user.id">
        <!-- 向插槽传递数据(user 和 index) -->
        <slot :user="user" :index="$index">
          <!-- 默认显示 -->
          {{ user.name }} ({{ user.email }})
        </slot>
      </li>
    </ul>
  </div>
</template>

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

const users = ref([
  { id: 1, name: '张三', email: 'zhang@example.com', age: 25 },
  { id: 2, name: '李四', email: 'li@example.com', age: 30 },
  { id: 3, name: '王五', email: 'wang@example.com', age: 28 }
])
</script>
vue 复制代码
<!-- ParentComponent.vue (父组件) -->
<template>
  <div>
    <h2>用户管理</h2>
    
    <!-- 使用作用域插槽 -->
    <UserList v-slot="{ user, index }">
      <div class="user-item">
        <strong>{{ index + 1 }}. {{ user.name }}</strong>
        <p>邮箱: {{ user.email }}</p>
        <p>年龄: {{ user.age }}</p>
        <button @click="selectUser(user)">选择</button>
      </div>
    </UserList>
    
    <!-- 另一种写法:指定插槽名 -->
    <UserList>
      <template #default="{ user, index }">
        <div>
          {{ index }} - {{ user.name }} ({{ user.age }}岁)
        </div>
      </template>
    </UserList>
  </div>
</template>

<script setup>
import UserList from './UserList.vue'

const selectUser = (user) => {
  console.log('选中用户:', user)
}
</script>

实际应用示例:表格组件

vue 复制代码
<!-- DataTable.vue -->
<template>
  <div class="data-table">
    <table>
      <thead>
        <tr>
          <!-- 表头插槽 -->
          <slot name="header">
            <!-- 默认表头 -->
            <th v-for="column in columns" :key="column.key">
              {{ column.title }}
            </th>
          </slot>
        </tr>
      </thead>
      <tbody>
        <tr v-for="(row, index) in data" :key="row.id">
          <!-- 行数据插槽 -->
          <slot :row="row" :index="index">
            <!-- 默认显示:显示所有列 -->
            <td v-for="column in columns" :key="column.key">
              {{ row[column.key] }}
            </td>
          </slot>
        </tr>
      </tbody>
    </table>
  </div>
</template>

<script setup>
defineProps({
  data: {
    type: Array,
    required: true
  },
  columns: {
    type: Array,
    default: () => []
  }
})
</script>

<style scoped>
.data-table table {
  width: 100%;
  border-collapse: collapse;
}
.data-table th, .data-table td {
  border: 1px solid #ddd;
  padding: 8px;
  text-align: left;
}
.data-table th {
  background-color: #f5f5f5;
}
</style>
vue 复制代码
<!-- 使用表格组件 -->
<template>
  <DataTable :data="users" :columns="columns">
    <!-- 自定义表头 -->
    <template #header>
      <th>序号</th>
      <th>姓名</th>
      <th>年龄</th>
      <th>操作</th>
    </template>
    
    <!-- 自定义行内容 -->
    <template v-slot="{ row, index }">
      <td>{{ index + 1 }}</td>
      <td>
        <strong>{{ row.name }}</strong>
        <small>{{ row.email }}</small>
      </td>
      <td :class="{ 'young': row.age < 30 }">
        {{ row.age }}
        <span v-if="row.age < 30">👶</span>
        <span v-else>🧓</span>
      </td>
      <td>
        <button @click="editUser(row)">编辑</button>
        <button @click="deleteUser(row)">删除</button>
      </td>
    </template>
  </DataTable>
</template>

<script setup>
import { ref } from 'vue'
import DataTable from './DataTable.vue'

const users = ref([
  { id: 1, name: '张三', email: 'zhang@example.com', age: 25 },
  { id: 2, name: '李四', email: 'li@example.com', age: 35 },
  { id: 3, name: '王五', email: 'wang@example.com', age: 28 }
])

const columns = ref([
  { key: 'name', title: '姓名' },
  { key: 'email', title: '邮箱' },
  { key: 'age', title: '年龄' }
])

const editUser = (user) => {
  console.log('编辑用户:', user)
}

const deleteUser = (user) => {
  console.log('删除用户:', user)
}
</script>

<style>
.young {
  color: green;
  font-weight: bold;
}
</style>

四、高级用法

1. 动态插槽名

vue 复制代码
<!-- DynamicSlotComponent.vue -->
<template>
  <div>
    <slot name="dynamicContent"></slot>
  </div>
</template>

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

// 动态插槽名
const slotName = ref('contentA')
</script>
vue 复制代码
<!-- 使用动态插槽 -->
<template>
  <DynamicSlotComponent>
    <!-- 动态决定使用哪个插槽 -->
    <template #[slotName]>
      动态内容
    </template>
  </DynamicSlotComponent>
</template>

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

const slotName = ref('dynamicContent')
</script>

2. 嵌套插槽

vue 复制代码
<!-- LayoutWrapper.vue -->
<template>
  <div class="wrapper">
    <slot name="wrapperHeader">
      <h2>包装器头部</h2>
    </slot>
    
    <!-- 嵌套:将插槽传递给子组件 -->
    <BaseLayout>
      <!-- 将父组件的内容传递给子组件的插槽 -->
      <template v-for="(_, name) in $slots" #[name]="slotData">
        <slot :name="name" v-bind="slotData"></slot>
      </template>
    </BaseLayout>
    
    <slot name="wrapperFooter">
      <p>包装器底部</p>
    </slot>
  </div>
</template>

3. 插槽函数

vue 复制代码
<!-- Renderless组件(无渲染组件) -->
<template>
  <!-- 只提供逻辑,不渲染任何内容 -->
  <slot :data="data" :loading="loading" :error="error"></slot>
</template>

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

const data = ref(null)
const loading = ref(false)
const error = ref(null)

onMounted(async () => {
  loading.value = true
  try {
    const response = await fetch('/api/data')
    data.value = await response.json()
  } catch (e) {
    error.value = e
  } finally {
    loading.value = false
  }
})
</script>
vue 复制代码
<!-- 使用无渲染组件 -->
<template>
  <DataFetcher v-slot="{ data, loading, error }">
    <div v-if="loading">加载中...</div>
    <div v-else-if="error">错误: {{ error.message }}</div>
    <div v-else>
      <h3>数据:</h3>
      <pre>{{ data }}</pre>
    </div>
  </DataFetcher>
</template>

五、实际项目示例

示例1:模态框组件

vue 复制代码
<!-- Modal.vue -->
<template>
  <div v-if="visible" class="modal-overlay" @click.self="close">
    <div class="modal">
      <!-- 标题插槽 -->
      <div class="modal-header">
        <slot name="header">
          <h3>提示</h3>
        </slot>
        <button class="close-btn" @click="close">×</button>
      </div>
      
      <!-- 内容插槽 -->
      <div class="modal-body">
        <slot></slot>
      </div>
      
      <!-- 底部插槽 -->
      <div class="modal-footer">
        <slot name="footer">
          <button @click="close">取消</button>
          <button @click="confirm">确认</button>
        </slot>
      </div>
    </div>
  </div>
</template>

<script setup>
defineProps({
  visible: Boolean
})

const emit = defineEmits(['update:visible', 'confirm', 'close'])

const close = () => {
  emit('update:visible', false)
  emit('close')
}

const confirm = () => {
  emit('confirm')
  close()
}
</script>

<style scoped>
.modal-overlay {
  position: fixed;
  top: 0; left: 0; right: 0; bottom: 0;
  background: rgba(0,0,0,0.5);
  display: flex;
  align-items: center;
  justify-content: center;
}
.modal {
  background: white;
  border-radius: 8px;
  min-width: 300px;
  max-width: 500px;
}
.modal-header {
  padding: 20px;
  border-bottom: 1px solid #eee;
  display: flex;
  justify-content: space-between;
  align-items: center;
}
.modal-body {
  padding: 20px;
}
.modal-footer {
  padding: 20px;
  border-top: 1px solid #eee;
  text-align: right;
}
.close-btn {
  background: none;
  border: none;
  font-size: 20px;
  cursor: pointer;
}
</style>
vue 复制代码
<!-- 使用模态框 -->
<template>
  <button @click="showModal = true">打开模态框</button>
  
  <Modal v-model:visible="showModal" @confirm="handleConfirm">
    <!-- 自定义标题 -->
    <template #header>
      <h3 style="color: red;">⚠️ 警告</h3>
    </template>
    
    <!-- 自定义内容 -->
    <p>确定要删除这条数据吗?</p>
    <p>删除后将无法恢复!</p>
    
    <!-- 自定义底部 -->
    <template #footer>
      <button @click="showModal = false">取消</button>
      <button @click="deleteItem" style="background: red; color: white;">
        确认删除
      </button>
    </template>
  </Modal>
</template>

<script setup>
import { ref } from 'vue'
import Modal from './Modal.vue'

const showModal = ref(false)

const handleConfirm = () => {
  console.log('确认操作')
}

const deleteItem = () => {
  console.log('删除项目')
  showModal.value = false
}
</script>

示例2:卡片组件

vue 复制代码
<!-- Card.vue -->
<template>
  <div class="card" :class="{ 'highlight': highlight }">
    <!-- 图片插槽 -->
    <div v-if="$slots.image" class="card-image">
      <slot name="image"></slot>
    </div>
    
    <!-- 内容区域 -->
    <div class="card-content">
      <!-- 标题插槽 -->
      <div class="card-title">
        <slot name="title">
          <h3>默认标题</h3>
        </slot>
      </div>
      
      <!-- 默认插槽(内容) -->
      <div class="card-body">
        <slot></slot>
      </div>
      
      <!-- 操作区域插槽 -->
      <div v-if="$slots.actions" class="card-actions">
        <slot name="actions"></slot>
      </div>
    </div>
  </div>
</template>

<script setup>
defineProps({
  highlight: {
    type: Boolean,
    default: false
  }
})
</script>

<style scoped>
.card {
  border: 1px solid #ddd;
  border-radius: 8px;
  overflow: hidden;
  transition: box-shadow 0.3s;
}
.card.highlight {
  border-color: #007bff;
  box-shadow: 0 4px 12px rgba(0, 123, 255, 0.2);
}
.card-image img {
  width: 100%;
  height: 200px;
  object-fit: cover;
}
.card-content {
  padding: 20px;
}
.card-title {
  margin-bottom: 10px;
}
.card-body {
  margin-bottom: 15px;
}
.card-actions {
  display: flex;
  gap: 10px;
  margin-top: 15px;
}
</style>
vue 复制代码
<!-- 使用卡片组件 -->
<template>
  <div class="product-list">
    <!-- 产品1:完整自定义 -->
    <Card highlight>
      <template #image>
        <img src="/api/placeholder/300/200" alt="产品图片">
      </template>
      
      <template #title>
        <h3 style="color: #007bff;">🔥 热销产品</h3>
      </template>
      
      <p>这款产品是我们的热销款,质量有保证!</p>
      <p>价格: <strong>¥299</strong></p>
      
      <template #actions>
        <button>加入购物车</button>
        <button>立即购买</button>
      </template>
    </Card>
    
    <!-- 产品2:使用默认 -->
    <Card>
      <p>普通产品,价格实惠</p>
      <p>价格: ¥99</p>
    </Card>
  </div>
</template>

📊 插槽速查表

场景 子组件定义 父组件使用 说明
默认插槽 <slot>默认内容</slot> <Child>内容</Child> 一个组件只能有一个默认插槽
具名插槽 <slot name="header"></slot> <template #header>内容</template> 多个具名插槽
作用域插槽 <slot :data="data"></slot> <Child v-slot="{ data }">{``{ data }}</Child> 子向父传数据
动态插槽名 <slot :name="slotName"></slot> <template #[dynamicName]>内容</template> 动态决定插槽
嵌套插槽 在组件内使用其他组件的插槽 正常使用 插槽透传

🎯 核心要点总结

  1. 插槽是内容分发机制:允许父组件向子组件传递模板内容
  2. 三种类型
    • 默认插槽:<slot>
    • 具名插槽:<slot name="xxx">
    • 作用域插槽:<slot :data="data">
  3. 使用语法
    • v-slot:name 或简写 #name
    • 作用域插槽:v-slot="{ data }"
  4. 默认内容 :在 <slot> 标签内部可以定义默认内容
  5. 实际应用:常用于布局组件、UI组件库、可复用组件

简单记忆口诀

复制代码
插槽就像预留位,父传内容子显示
默认一个无名槽,具名多个靠名字
作用域槽能传数,灵活使用真强大

通过合理使用插槽,可以创建出高度可定制、可复用的组件,大大提高开发效率。

相关推荐
Arya_aa2 小时前
3.生成vue模板在views中写一个Test.vue进行展示,学习指令,带有v-前缀的特殊attribute,并下载ELementUI
vue.js
血玥珏2 小时前
血玥珏-BMP名字图片生成器
前端·html
ZTLJQ2 小时前
构建网页的三剑客:HTML, CSS, JavaScript 完全解析
javascript·css·html
weixin199701080162 小时前
《QX 游戏商城商品详情页前端性能优化实战》
前端·游戏·性能优化
方也_arkling2 小时前
【Vue-Day12】Vue组件的生命周期
前端·javascript·vue.js
森叶2 小时前
深入解析:Claude 桌面应用与 Chrome 扩展的 Native Messaging 通信机制
前端·chrome
苏武难飞2 小时前
分享一个THREE.JS中无限滚动的技巧
前端·javascript·css
bitbrowser2 小时前
2026 PC端多Chrome账号管理指南:从日常切换到防关联实战
前端·chrome