技术知识文章集合TECHNICAL ARCHIVE · 457 DOCUMENTS

显示模式

登录
ARCHIVE DOCUMENTVUE

详解 Vue 中的插槽用法:默认插槽、具名插槽、作用域插槽

所属馆藏
Vue
文件格式
Markdown
原始路径
Vue/99-详解Vue中的插槽用法:默认插槽、具名插槽、作用域插槽
本文目录10 个章节
  1. 一、什么是插槽
  2. 二、默认插槽
  3. 三、具名插槽
  4. 四、作用域插槽
  5. 五、作用域插槽的应用:可定制 Card
  6. 六、Vue 3 中的插槽 API
  7. 七、Vue 2 的旧插槽语法
  8. 八、插槽、props 和事件如何选择
  9. 总结
  10. 参考资料

详解 Vue 中的插槽用法:默认插槽、具名插槽、作用域插槽

本文保留原文从默认插槽、具名插槽到作用域插槽的学习路线,并补充 Vue 3.5 的写法、插槽作用域、动态插槽名、回退内容和 defineSlots。原文中的抓取噪声、默认插槽名称拼写错误、把 v-slot 用在普通 HTML 元素上的错误说法以及 Vue 2/3 混用已修正。

一、什么是插槽

在项目开发中,一个组件通常需要被多个页面复用。父组件除了通过 props 给子组件传递数据、通过 emits 接收事件之外,有时还希望决定子组件某一块区域显示什么内容。

插槽(slot)就是 Vue 的内容分发机制:子组件预留一个或多个位置,父组件在使用子组件时把模板内容传入这些位置。

父组件提供内容
  ↓
子组件通过 <slot> 声明接收位置
  ↓
Vue 将父组件的内容渲染到子组件模板的指定位置

插槽内容不是简单的 HTML 字符串。父组件传入的模板会保留父组件的渲染作用域;如果父组件需要使用子组件内部的数据,应使用作用域插槽,由子组件显式传递 slot props。

原文使用 Element UI 的 Badge 和 Progress 展示组件复用场景。Element UI 主要面向 Vue 2;Vue 3 项目通常选择 Element Plus 或其他兼容 Vue 3 的组件库,插槽机制本身不受具体 UI 库影响。

子组件中包含可复用内容的示例(原文图片已本地化)

下面是一个简单的父子组件关系:

<!-- Child.vue:Vue 2/3 Options API 都可以理解这段模板 -->
<template>
  <div class="child">
    <div>
      <span>Badge 标记</span>
      <div class="content">
        <!-- Vue 2 + Element UI 示例 -->
        <el-badge :value="12" class="item">
          <el-button size="small">评论</el-button>
        </el-badge>
      </div>
    </div>

    <div>
      <span>进度条</span>
      <div class="content">
        <el-progress :percentage="50" />
      </div>
    </div>
  </div>
</template>
<!-- App.vue -->
<template>
  <div id="app">
    <Child />
  </div>
</template>

<script>
import Child from './components/Child.vue'

export default {
  name: 'App',
  components: { Child },
}
</script>

此时 Child 内部的固定内容会正常显示。但如果父组件直接在 <Child> 标签内部写入一段内容,子组件没有声明 <slot> 时,这段内容不会出现在子组件模板预期的位置:

<Child>
  <h1>欢迎大家关注小土豆</h1>
</Child>

没有声明 slot 时的组件渲染结果(原文图片已本地化)

父组件传入内容但子组件尚未接收的示例(原文图片已本地化)

二、默认插槽

2.1 子组件声明 <slot>

在子组件需要接收内容的位置写入 <slot />

<!-- Child.vue -->
<template>
  <div class="child">
    <div>子组件原有内容</div>

    <!-- 父组件没有传内容时,显示这里的回退内容 -->
    <slot>我是这个 slot 里面本来的内容</slot>
  </div>
</template>

父组件可以直接把内容放入子组件标签中:

<Child>
  <h1>欢迎大家关注小土豆</h1>
</Child>

如果父组件提供了内容,<slot> 内的回退内容会被替换;如果没有提供内容,才显示回退内容。

默认插槽渲染结果(原文图片已本地化)

2.2 显式写默认插槽

Vue 2.6+ 和 Vue 3 可以使用 v-slot,简写为 #

<Child>
  <template #default>
    <h1>欢迎大家关注小土豆</h1>
  </template>
</Child>

对于只有默认插槽的简单场景,直接写子节点更简洁;需要和其他具名插槽一起使用,或者需要绑定 slot props 时,使用 <template #default> 更清晰。

默认插槽的名字确实是 default,原文示例中的拼写错误需要改正:

<Child>
  <template v-slot:default>
    <h1>欢迎关注小土豆</h1>
  </template>
</Child>

2.3 插槽内容的作用域

插槽内容由父组件提供,因此默认使用父组件的作用域:

<!-- Parent.vue -->
<script setup>
import { ref } from 'vue'
import Child from './Child.vue'

const parentMessage = ref('来自父组件')
</script>

<template>
  <Child>
    <!-- 这里读取的是 Parent 的 parentMessage -->
    <span>{{ parentMessage }}</span>
  </Child>
</template>

不能在这里直接读取 Child 内部未暴露的 childMessage。需要子组件传递 slot props 时,使用作用域插槽,见后文。

三、具名插槽

当组件需要多个内容分发位置时,为每个 <slot> 设置 name

<!-- Child.vue -->
<template>
  <div class="child">
    <header>
      <slot name="heading" />
    </header>

    <section>
      <slot name="sub-heading" />
      <div>子组件自己的 Badge/Progress 内容</div>
    </section>

    <footer>
      <slot name="footer-text" />
    </footer>
  </div>
</template>

父组件使用 v-slot:name#name

<!-- App.vue,Vue 2.6+ / Vue 3 -->
<Child>
  <template #heading>
    <h1>Element 组件</h1>
  </template>

  <template #sub-heading>
    <p>这里是 Element 组件的介绍</p>
  </template>

  <template #footer-text>
    <p>出品:小土豆</p>
  </template>
</Child>

未提供的具名插槽不会自动显示内容;如果希望它有默认内容,应在子组件的 <slot name="..."> 内提供回退内容。

具名插槽定义示例(原文图片已本地化)

具名插槽填充后的页面效果(原文图片已本地化)

3.1 具名插槽和默认插槽同时存在

未命名的 <slot> 接收默认插槽内容:

<template>
  <div>
    <slot name="header" />
    <main>
      <slot />
    </main>
    <slot name="footer" />
  </div>
</template>
<Child>
  <template #header>
    <h2>标题</h2>
  </template>

  <p>这段内容进入默认插槽</p>

  <template #footer>
    <small>页脚</small>
  </template>
</Child>

3.2 v-slot 的使用位置

v-slot 应该使用在组件标签或 <template> 上:

<Child>
  <template #heading>
    <h1>标题</h1>
  </template>
</Child>

原文说可以把 v-slot="default" 写在任意普通 div 上并产生一层 DOM,这是不准确的。普通 HTML 元素不负责接收 Vue 组件插槽,v-slot 也不能这样使用;如果需要一层真实 DOM 包裹,直接写 <div>,再把它作为 slot 内容传入即可:

<Child>
  <div class="text">
    <h1>欢迎关注小土豆</h1>
  </div>
</Child>

<template> 本身不会渲染成真实 DOM 节点,只有其中的内容会被插入。

3.3 动态插槽名

Vue 3 支持动态参数形式:

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

const dynamicSlotName = ref('header')
</script>

<template>
  <Child>
    <template #[dynamicSlotName]>
      <h2>动态插槽内容</h2>
    </template>
  </Child>
</template>

Vue 2.6+ 也支持 v-slot:[dynamicSlotName],但要注意动态参数表达式必须能够得到合法的插槽名。

四、作用域插槽

作用域插槽解决的是:子组件提供数据,父组件决定如何渲染这部分数据

数据流可以表示为:

子组件准备 slot props
  ↓
父组件通过 v-slot 接收
  ↓
父组件使用自己的模板渲染

这不是让父组件直接访问子组件的内部状态,而是子组件主动把指定数据作为插槽参数暴露出来。

4.1 子组件提供 slot props

<!-- Child.vue -->
<script>
export default {
  name: 'Child',
  data() {
    return {
      heading: '这里是默认的 heading',
    }
  },
}
</script>

<template>
  <div class="child">
    <slot name="heading" :headingValue="heading">
      {{ heading }}
    </slot>
  </div>
</template>

也可以使用 Vue 3 <script setup>

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

const heading = ref('这里是默认的 heading')
</script>

<template>
  <slot name="heading" :headingValue="heading" />
</template>

在模板中,ref 会自动解包,所以传给 slot 的是当前值。

4.2 父组件接收 slot props

<Child>
  <template #heading="slotProps">
    <h1>Element 组件:{{ slotProps.headingValue }}</h1>
  </template>
</Child>

变量名可以任意命名。也可以使用解构:

<Child>
  <template #heading="{ headingValue }">
    <h1>{{ headingValue }}</h1>
  </template>
</Child>

原文中 slotValue 是一个对象,键名就是子组件传出的 headingValue,这个理解是正确的;只是现在通常使用 #heading="{ headingValue }" 来减少重复写法。

作用域插槽接收数据的示例(原文图片已本地化)

作用域插槽 props 对象的展示结果(原文图片已本地化)

五、作用域插槽的应用:可定制 Card

假设有一个 Card 组件,原始版本负责渲染标题和列表:

<!-- Card.vue,Vue 2 Options API 写法 -->
<template>
  <div class="card">
    <h3>{{ title }}</h3>
    <p v-for="item in list" :key="item.id">
      {{ item.id }}.{{ item.text }}
    </p>
  </div>
</template>

<script>
export default {
  name: 'Card',
  props: {
    title: String,
    list: {
      type: Array,
      default: () => [],
    },
  },
}
</script>

父组件可以传入:

<Card :title="title" :list="list" />

这种 Card 能复用,但显示方式固定。如果某个页面想在标题处增加图标,或者列表项不显示 1、2、3 编号,可以把渲染权交给父组件:

<!-- Card.vue -->
<template>
  <div class="card">
    <h3>
      <slot name="title" :titleValue="title">
        {{ title }}
      </slot>
    </h3>

    <p v-for="item in list" :key="item.id">
      <slot name="text" :itemValue="item">
        {{ item.id }}.{{ item.text }}
      </slot>
    </p>
  </div>
</template>

父组件自定义标题:

<Card :title="title" :list="list">
  <template #title="{ titleValue }">
    <span class="icon">★</span>
    {{ titleValue }}
  </template>
</Card>

父组件自定义列表文本,不显示编号:

<Card :title="title" :list="list">
  <template #text="{ itemValue }">
    {{ itemValue.text }}
  </template>
</Card>

Card 组件默认渲染效果(原文图片已本地化)

通过作用域插槽增加标题图标(原文图片已本地化)

通过作用域插槽自定义列表项(原文图片已本地化)

这体现了“对扩展开放、对修改关闭”的组件设计思路:组件保留数据和布局的公共部分,父组件通过插槽定制局部视图。

六、Vue 3 中的插槽 API

6.1 在 <script setup> 中访问插槽

当组件需要根据插槽是否存在来决定是否渲染包裹元素,可以使用 useSlots

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

const slots = useSlots()
</script>

<template>
  <div v-if="slots.header" class="header">
    <slot name="header" />
  </div>
  <main>
    <slot />
  </main>
</template>

slots.header 表示是否提供了这个 slot 函数,不代表调用后一定有可见文本;复杂场景还要根据实际 slot 内容判断。

6.2 defineSlots

Vue 3.3+ 的 <script setup> 支持 defineSlots 编译宏,可以为插槽 props 和返回内容提供类型提示:

<script setup lang="ts">
const slots = defineSlots<{
  default?: (props: { message: string }) => any
  footer?: (props: { disabled: boolean }) => any
}>()

const message = '来自子组件'
</script>

<template>
  <slot :message="message" />
</template>

defineSlots 只在编译时使用,不需要导入;Vue 3.5 项目可以用它改善大型组件的插槽类型体验。

6.3 作用域插槽和渲染函数

在 Vue 3 的 render function 中,插槽通常表现为函数:

import { h } from 'vue'

export default {
  setup(_, { slots }) {
    return () => h('section', [
      slots.header?.(),
      h('main', slots.default?.()),
    ])
  },
}

带参数时:

slots.item?.({ value: item })

这也说明了为什么 slot props 是子组件传给父组件插槽函数的参数,而不是普通的父子 props 反向修改。

七、Vue 2 的旧插槽语法

Vue 2.6.0 之前常见 slotslot-scope;Vue 2.6+ 推荐统一使用 v-slot,Vue 3 已移除这些旧语法。

7.1 旧默认插槽

<!-- Child.vue -->
<slot />

<!-- Parent.vue,Vue 2.5 及更早常见写法 -->
<Child>
  <template slot="default">
    <h1>欢迎关注小土豆</h1>
  </template>
</Child>

简单默认内容也可以直接写在 <Child> 内部:

<Child>
  <h1>欢迎关注小土豆</h1>
</Child>

7.2 旧具名插槽

<!-- Child.vue -->
<slot name="heading" />
<slot name="footer-text" />

<!-- Parent.vue -->
<Child>
  <template slot="heading">
    <h1>Element 组件</h1>
  </template>
  <template slot="footer-text">
    <p>出品:小土豆</p>
  </template>
</Child>

7.3 旧作用域插槽

<!-- Child.vue -->
<slot name="heading" :headingValue="heading">
  {{ heading }}
</slot>

<!-- Parent.vue -->
<Child>
  <template slot="heading" slot-scope="slotProps">
    <h1>{{ slotProps.headingValue }}</h1>
  </template>
</Child>

迁移到 Vue 2.6+ 或 Vue 3 时,应改为 v-slot/# 语法。

旧版插槽语法示例(原文图片已本地化)

旧版具名插槽示例(原文图片已本地化)

旧版作用域插槽示例(原文图片已本地化)

八、插槽、props 和事件如何选择

  • props:父组件向子组件传递数据,适合明确的状态和配置;
  • emits/事件:子组件通知父组件发生了某件事;
  • 插槽:父组件定制子组件的局部视图;
  • provide/inject:跨多层组件提供依赖,避免逐层透传;
  • Pinia:多个页面或不相邻组件共享和管理应用状态。

插槽并不是所有通信问题的替代方案。一个组件如果通过大量 slot props 传递复杂状态,可能应该拆分组件边界,或者使用 composable、provide/inject、Pinia 等更合适的机制。

总结

  1. 插槽是 Vue 的内容分发机制,子组件通过 <slot> 声明接收位置;
  2. 默认插槽使用 <slot />,具名插槽使用 name#name/v-slot:name
  3. 插槽内容默认在父组件作用域中编译;
  4. 作用域插槽由子组件传出 slot props,父组件决定如何渲染;
  5. <template #name> 不会生成额外 DOM,普通 HTML 元素不能随意使用 v-slot
  6. Vue 3 支持动态插槽名、useSlotsdefineSlots
  7. Vue 2 的 slot/slot-scope 是历史语法,Vue 3 应使用 v-slot/#
  8. 组件设计应根据需求选择 props、事件、插槽、provide/inject 或 Pinia。

参考资料

原文作者:小土豆 biubiubiu。原文关于默认插槽、具名插槽、作用域插槽、Card 组件扩展和旧语法的主线予以保留;抓取标记、失效图片、默认插槽名称拼写、错误的普通元素 v-slot 示例以及 Vue 2/3 语法混用已整理或修正。

457 DOCUMENTS · 10 COLLECTIONS
ARCHIVE SEARCH457 篇文章

SEARCH GUIDE

输入关键词开始搜索

支持搜索文章标题、所属分类和原始文档路径。

按分类浏览

10 COLLECTIONS