Vue.js: 插槽 slots

最后更新:2026-08-26

插槽(Slot)是 Vue 的内容分发 API——父组件可以往子组件的"槽位"插入任意内容。插槽让组件像"模板"一样灵活:父组件控制显示什么,子组件控制显示在哪里。

Vue 3 提供了 3 种插槽:默认插槽、具名插槽、作用域插槽。本课帮你掌握这 3 种用法和最佳实践。

1. 你将学到


2. 一个 Card 组件无法定制内容的难题

(1) 痛点:5 种 Card 样式,要写 5 个组件

Alice 的电商后台需要 5 种卡片:

VUE
<!-- ❌ 翻车版:5 个组件 -->
<!-- ProductCard.vue -->
<div class="card">{{ product.name }}</div>

<!-- UserCard.vue -->
<div class="card">{{ user.name }}</div>

<!-- OrderCard.vue -->
<div class="card">Order #{{ order.id }}</div>

<!-- 5 个组件,90% 代码相同,只有内容不同 -->

产品经理 Charlie:

"Alice,下周还要加 5 种卡片类型。不能一直加新组件。我们需要一个灵活的 Card,能接收任何内容。"

(2) Vue 插槽解法:1 个 BaseCard,父组件塞内容

VUE
<!-- BaseCard.vue - 通用卡片,插槽接收任何内容 -->
<template>
  <div class="card">
    <div v-if="$slots.header" class="card-header">
      <slot name="header" />
    </div>
    <div class="card-body">
      <slot />  <!-- 默认插槽 -->
    </div>
    <div v-if="$slots.footer" class="card-footer">
      <slot name="footer" />
    </div>
  </div>
</template>
VUE
<!-- ProductCard 使用 BaseCard -->
<BaseCard>
  <template #header>
    <h3>Product</h3>
  </template>
  
  <p>iPhone 15 Pro</p>  <!-- 默认插槽 -->
  
  <template #footer>
    <button>Add to Cart</button>
  </template>
</BaseCard>

1 个 BaseCard.vue → 无限种卡片。每次新增只需写内容,不用改组件。

(3) 收益

使用插槽后:


3. 默认插槽

(1) 基本用法

VUE
<!-- BaseCard.vue 子组件 -->
<template>
  <div class="card">
    <!-- 插槽:父组件可以插入任何内容 -->
    <slot />
  </div>
</template>
VUE
<!-- App.vue 父组件 -->
<template>
  <BaseCard>
    <h3>Hello</h3>
    <p>This is the card content</p>
  </BaseCard>
</template>

渲染结果

HTML
<div class="card">
  <h3>Hello</h3>
  <p>This is the card content</p>
</div>

(2) 默认内容(fallback)

VUE
<!-- 子组件 -->
<template>
  <div class="card">
    <slot>
      <!-- 默认内容:父组件不传时显示 -->
      <p>No content provided</p>
    </slot>
  </div>
</template>
VUE
<!-- 父组件不传内容 -->
<BaseCard />

<!-- 渲染:<p>No content provided</p> -->

<!-- 父组件传内容 -->
<BaseCard>
  <p>Custom content</p>
</BaseCard>

<!-- 渲染:<p>Custom content</p>(覆盖默认)-->

(3) 5 大使用场景

场景 用法
卡片内容 <slot />
按钮文字 <slot />
列表项 <slot :item="item" />
表单字段 <slot />
模态框主体 <slot />

4. 具名插槽

(1) 定义具名插槽

VUE
<!-- BaseLayout.vue 子组件 -->
<template>
  <div class="layout">
    <header>
      <slot name="header" />
    </header>
    <main>
      <slot />  <!-- 默认插槽(可省略 name="default") -->
    </main>
    <footer>
      <slot name="footer" />
    </footer>
  </div>
</template>

(2) 父组件使用(3 种语法)

VUE
<!-- App.vue 父组件 -->

<!-- 写法 1:v-slot:name(最完整) -->
<BaseLayout>
  <template v-slot:header>
    <h1>My App</h1>
  </template>
  
  <template v-slot:default>
    <p>Main content</p>
  </template>
  
  <template v-slot:footer>
    <p>Footer</p>
  </template>
</BaseLayout>

<!-- 写法 2:缩写 #name(推荐) -->
<BaseLayout>
  <template #header>
    <h1>My App</h1>
  </template>
  
  <p>Main content</p>  <!-- 默认插槽可省略 template -->
  
  <template #footer>
    <p>Footer</p>
  </template>
</BaseLayout>

<!-- 写法 3:v-slot 接收多个插槽对象(Vue 3 推荐) -->
<BaseLayout>
  <template #header>
    <h1>My App</h1>
  </template>
  
  <template #default="{ user }">  <!-- 解构作用域插槽 -->
    <p>Hello, {{ user.name }}</p>
  </template>
  
  <template #footer>
    <button>Logout</button>
  </template>
</BaseLayout>

(3) 动态插槽名

VUE
<!-- 子组件 -->
<template>
  <div>
    <slot :name="slotName" />
  </div>
</template>

<script setup>
const { ref } = Vue
const dynamicSlotName = ref('header')
</script>

<!-- 父组件:动态绑定插槽 -->
<BaseLayout>
  <template #[dynamicSlotName]>
    <p>Dynamic content</p>
  </template>
</BaseLayout>

(4) 检测插槽存在

VUE
<!-- 子组件:检查插槽是否传入 -->
<template>
  <div class="card">
    <div v-if="$slots.header" class="card-header">
      <slot name="header" />
    </div>
    <slot />
  </div>
</template>
VUE
<!-- 父组件 -->
<BaseCard>
  <template #header>...</template>  <!-- 传了 header 插槽,显示 -->
</BaseCard>

<BaseCard>
  <!-- 没传 header 插槽,不显示 card-header -->
</BaseCard>

5. 作用域插槽

(1) 核心:子组件传数据给父组件插槽

VUE
<!-- 子组件:UserList.vue -->
<template>
  <ul>
    <li v-for="user in users" :key="user.id">
      <!-- 把 user 传给父组件插槽 -->
      <slot :user="user" :index="index" />
    </li>
  </ul>
</template>

<script setup>
defineProps({ users: Array })
</script>
VUE
<!-- 父组件:App.vue -->
<template>
  <UserList :users="users">
    <!-- 接收子组件传的数据 -->
    <template #default="{ user, index }">
      <p>{{ index + 1 }}. {{ user.name }} ({{ user.email }})</p>
    </template>
  </UserList>
</template>

渲染结果

HTML
<ul>
  <li><p>1. Alice (alice@example.com)</p></li>
  <li><p>2. Bob (bob@example.com)</p></li>
  <li><p>3. Charlie (charlie@example.com)</p></li>
</ul>

(2) 5 种作用域插槽模式

模式 父组件写法 用途
接收所有 props <template #default="slotProps"> 接收整个对象
解构 <template #default="{ user }"> 只用需要的字段
重命名 <template #default="{ user: u }"> 避免变量冲突
默认值 <template #default="{ user = defaultUser }"> 解构时给默认值
不用 template {{ slotProps.user.name }} 简单场景(默认插槽)

(3) 完整示例:可定制列表

VUE
<!-- UserList.vue 子组件 -->
<template>
  <ul class="user-list">
    <li v-for="(user, index) in users" :key="user.id">
      <slot :user="user" :index="index" :isAdmin="user.role === 'admin'" />
    </li>
  </ul>
</template>

<script setup>
defineProps({ users: Array })
</script>
VUE
<!-- 父组件使用(3 种方式) -->
<template>
  <!-- 方式 1:简单展示 -->
  <UserList :users="users">
    <template #default="{ user }">
      {{ user.name }}
    </template>
  </UserList>
  
  <!-- 方式 2:表格展示 -->
  <UserList :users="users">
    <template #default="{ user, index, isAdmin }">
      <tr>
        <td>{{ index + 1 }}</td>
        <td>{{ user.name }}</td>
        <td>{{ user.email }}</td>
        <td>
          <span v-if="isAdmin" class="badge admin">Admin</span>
          <span v-else class="badge user">User</span>
        </td>
      </tr>
    </template>
  </UserList>
  
  <!-- 方式 3:卡片展示 -->
  <UserList :users="users">
    <template #default="{ user }">
      <UserCard :user="user" />
    </template>
  </UserList>
</template>

6. $slots 和 useSlots

(1) $slots 访问插槽

VUE
<!-- 子组件 -->
<template>
  <div>
    <!-- 列出所有传入的插槽名 -->
    <div v-for="(_, name) in $slots" :key="name">
      <slot :name="name" />
    </div>
  </div>
</template>

(2) useSlots(Composition API)

VUE
<script setup>
const { useSlots } = Vue

const slots = useSlots()

// 检查插槽是否存在
if (slots.header) {
  console.log('Header slot exists')
}

// 动态渲染
if (slots.default) {
  console.log('Default slot exists')
}
</script>

(3) $slots vs useSlots

维度 $slots useSlots()
API 类型 模板中直接用 JS 中用(Composition API)
返回 插槽对象 插槽对象(refs 形式)
场景 template 内 <script setup>

7. 完整示例:可定制 Card 组件

▶ 示例:BaseCard 默认 + 具名插槽

HTML 📖 仅展示
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>

<style>
.card { border: 1px solid #e5e7eb; border-radius: 8px; overflow: hidden; margin: 0.5rem 0; }
.card-header, .card-footer { background: #f9fafb; padding: 0.75rem; font-weight: bold; }
.card-body { padding: 1rem; }
.btn { padding: 6px 12px; background: #42b883; color: white; border: none; border-radius: 4px; cursor: pointer; }
</style>

<div id="app">
  <!-- 1. 最简单(只传 body) -->
  <base-card>
    <p>Simple content (only body)</p>
  </base-card>

  <!-- 2. 带 header -->
  <base-card>
    <template #header>
      <h3>Product Name</h3>
    </template>
    <p>Product description</p>
  </base-card>

  <!-- 3. 完整 header + body + footer -->
  <base-card>
    <template #header>
      <h3>Order #12345</h3>
    </template>
    <p>Total: $99.99</p>
    <template #footer>
      <button class="btn">View Details</button>
    </template>
  </base-card>
</div>

<script>
const { createApp } = Vue

// 子组件:BaseCard
const BaseCard = {
  template: `
    <div class="card">
      <div v-if="$slots.header" class="card-header">
        <slot name="header" />
      </div>
      <div class="card-body">
        <slot>
          <p>No content (默认内容)</p>
        </slot>
      </div>
      <div v-if="$slots.footer" class="card-footer">
        <slot name="footer" />
      </div>
    </div>
  `
}

const App = { components: { BaseCard } }
createApp(App).mount('#app')
</script>
逻辑代码 49 行(超过 40 行限制,仅展示)

▶ 示例:作用域插槽(DataTable 自定义列)

HTML 📖 仅展示
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>

<style>
table { width: 100%; border-collapse: collapse; }
th, td { padding: 8px; border: 1px solid #ddd; text-align: left; }
th { background: #f9fafb; }
.badge { padding: 2px 8px; border-radius: 4px; color: white; font-size: 0.85rem; }
.badge.active { background: #10b981; }
.badge.inactive { background: #6b7280; }
.btn { padding: 4px 8px; background: #3b82f6; color: white; border: none; border-radius: 4px; cursor: pointer; }
</style>

<div id="app">
  <data-table :columns="columns" :data="users"></data-table>
</div>

<script>
const { createApp } = Vue

// 子组件:DataTable(作用域插槽 + 默认插槽 fallback)
const DataTable = {
  props: {
    columns: { type: Array, required: true },
    data: { type: Array, required: true }
  },
  template: `
    <table>
      <thead>
        <tr>
          <th v-for="col in columns" :key="col.key">{{ col.label }}</th>
        </tr>
      </thead>
      <tbody>
        <tr v-for="(row, index) in data" :key="row.id">
          <td v-for="col in columns" :key="col.key">
            <!-- 作用域插槽:传 row/index/value 给父组件 -->
            <slot :name="'cell-' + col.key" :row="row" :index="index" :value="row[col.key]">
              <!-- 默认显示:父组件未定义该列插槽时 -->
              {{ row[col.key] }}
            </slot>
          </td>
        </tr>
      </tbody>
    </table>
  `
}

// 父组件:自定义 status 和 actions 列
const App = {
  components: { DataTable },
  setup() {
    const columns = [
      { key: 'id', label: 'ID' },
      { key: 'name', label: 'Name' },
      { key: 'email', label: 'Email' },
      { key: 'status', label: 'Status' },
      { key: 'actions', label: 'Actions' }
    ]
    const users = [
      { id: 1, name: 'Alice', email: 'alice@x.com', status: 'active' },
      { id: 2, name: 'Bob', email: 'bob@x.com', status: 'inactive' }
    ]
    return { columns, users }
  },
  methods: {
    editUser(row) { console.log('Edit:', row.name) }
  },
  template: `
    <data-table :columns="columns" :data="users">
      <!-- 自定义 status 列 -->
      <template #cell-status="{ row }">
        <span :class="['badge', row.status]">{{ row.status }}</span>
      </template>

      <!-- 自定义 actions 列 -->
      <template #cell-actions="{ row }">
        <button class="btn" @click="editUser(row)">Edit</button>
      </template>
    </data-table>
  `
}

createApp(App).mount('#app')
</script>
逻辑代码 71 行(超过 40 行限制,仅展示)

▶ 示例:5 种 BaseCard 使用方式速查(纯展示)

方式 父组件模板 用途
1. 最简单(只传 body) <BaseCard><p>Simple</p></BaseCard> 只用默认插槽
2. 带 header <BaseCard><template #header>...</template>...</BaseCard> 加头部
3. 完整 header + body + footer 三段 <template #name> 完整布局
4. 完整写法(v-slot) <template v-slot:header> 长写法(旧文档)
5. 作用域插槽 <template #actions="{ user }"> 子→父传数据

▶ 示例:5 个常见错误速查

错误 现象 解决
插槽名拼错 内容不显示 检查 <slot name="...">#name 一致
父组件多个根元素 警告 父组件用 <template #name> 包装
v-slot 旧语法 Vue 2 兼容 改用 v-slot:header#header
默认插槽没传 父组件不显示 检查父组件的 <Component /> 内容
作用域插槽不解构 模板冗长 { user, index } 解构

▶ 示例:5 个性能对比

写法 编译 性能 推荐
<slot /> 静态 ⭐⭐⭐⭐⭐ 默认
<slot name="x" /> 静态 ⭐⭐⭐⭐⭐ 具名
<slot :x="x" /> 静态 ⭐⭐⭐⭐ 作用域
v-if="$slots.x" 条件 ⭐⭐⭐⭐ 检测存在
useSlots() 运行时 ⭐⭐⭐ JS 中用

▶ 示例:$slots 5 大用途

HTML
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>

<div id="app">
  <smart-wrapper>
    <template #header>Header 内容</template>
    <p>默认插槽:主要内容</p>
    <template #footer>Footer 内容</template>
  </smart-wrapper>
</div>

<script>
const { createApp } = Vue

const SmartWrapper = {
  template: `
    <div style="border: 1px solid #ddd; padding: 1rem; border-radius: 8px;">
      <div v-if="slots.header" style="background: #f9fafb; padding: 8px;">
        <slot name="header" />
      </div>
      <div style="padding: 8px;">
        <slot>
          <p>Default fallback</p>
        </slot>
      </div>
      <div v-if="slots.footer" style="background: #f9fafb; padding: 8px;">
        <slot name="footer" />
      </div>
      <p style="color: #999; font-size: 0.85rem;">
        已检测插槽: {{ Object.keys(slots).join(', ') }}
      </p>
    </div>
  `,
  computed: {
    slots() { return this.$slots }
  }
}

const App = { components: { SmartWrapper } }
createApp(App).mount('#app')
</script>
▶ 试一试

❓ 常见问题

Q 插槽和 props 有什么区别?
A props 传数据(任何 JS 值),插槽传内容(HTML/组件片段)。Props 适合配置项,插槽适合内容定制。
Q 作用域插槽什么时候用?
A 当父组件需要基于子组件的数据自定义渲染时。例如:表格的列渲染、列表的项渲染、卡片的内容布局。
Q v-slot 和 # 缩写区别?
A 完全等价,#headerv-slot:header 的简写。推荐用缩写(更简洁)。
Q 默认插槽可以命名吗?
A 默认插槽不需要命名,<slot /> 就是默认。如果非要命名,用 <slot name="default" />,父组件用 <template #default> 或直接传内容。
Q 插槽能在 props 中传递吗?
A 不能直接 prop 插槽。但可以 prop 一个 VNode / Render 函数(高级用法)。普通场景用 <slot> 即可。
Q 怎么在父组件中检测子组件的插槽?
A$slots(template)或 useSlots()(JS)。子组件用 <slot> 暴露,父组件用 <template #name> 填充。
Q 具名插槽可以有默认内容吗?
A 可以。<slot name="header"><h3>Default Header</h3></slot>。当父组件不传 header 时显示。

📖 小节


📝 作业

  1. 基础题(难度⭐) 实现一个简单的 Button 组件:

    • BaseButton.vue:默认插槽 + 3 种 variant(primary/success/danger)
    • props: variant (String)
    • 父组件:测试 3 种 variant + 自定义插槽内容
  2. 进阶题(难度⭐⭐) 实现 BaseLayout 组件:

    • 子组件:3 个具名插槽(header / default / footer)
    • 父组件:使用 BaseLayout 实现 Dashboard 页面
    • $slots 检测插槽是否存在
    • 添加默认内容(fallback)
  3. 挑战题(难度⭐⭐⭐) 实现完整的 DataTable 组件系统:

    1. DataTable.vue:接收 columns + data,渲染表格
    2. 作用域插槽:#cell-{key} 允许父组件自定义单元格
    3. 默认插槽:父组件不提供时显示原始值
    4. 父组件示例:用户表 + 订单表,2 种不同的列渲染
    5. 性能:v-memo 缓存行(数据不变时跳过重渲染)
    6. TypeScript 强类型
Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏