Vue插槽从入门到项目实战:默认插槽、具名插槽、作用域插槽,一次讲明白

Vue插槽从入门到项目实战:默认插槽、具名插槽、作用域插槽,一次讲明白

你肯定遇到过这种情况:写了一个弹窗组件,长宽、遮罩、关闭按钮都挺好,但是弹窗里显示的内容每个地方不一样------有时候是表单,有时候是一段文字,有时候是一张图片。

如果不用插槽,你可能会疯狂地通过 props 传配置,甚至传 HTML 字符串,又丑又难维护。但有了插槽,一切就优雅了:组件提供"占位符",父组件往里面塞任何东西,包括组件、HTML、文本,甚至还能拿到子组件的数据。

下面我们就用案例,一个一个搞清楚 Vue 的几种插槽。


一、默认插槽:最简单的占位

场景

一个通用的卡片组件,卡片的外框、标题、边框都一样,但中间的内容可以是文字、按钮、甚至一张图片。

1. 子组件 Card.vue

vue

复制代码
<template>
  <div class="card">
    <div class="card-header">
      <h3>我是卡片标题</h3>
    </div>
    <div class="card-body">
      <!-- 
        这里就是插槽的出口,<slot></slot> 是一个占位符
        父组件在使用 <Card> 时,放在它里面的内容会出现在这里 
        如果父组件没传内容,就显示“默认的卡片内容”
      -->
      <slot>默认的卡片内容</slot>
    </div>
  </div>
</template>

<script setup>
// 这个组件里没什么逻辑,只负责提供一个“壳”
</script>

<style scoped>
.card {
  border: 1px solid #ccc;
  border-radius: 8px;
  overflow: hidden;
  margin: 10px;
}
.card-header {
  background: #f0f0f0;
  padding: 10px;
}
.card-body {
  padding: 15px;
}
</style>

2. 父组件 App.vue

vue

复制代码
<template>
  <div class="app">
    <!-- 第一个卡片:里面塞了一段文字 -->
    <Card>
      <p>这是一段自定义的卡片内容,来自于父组件。</p>
    </Card>

    <!-- 第二个卡片:里面塞了一个按钮 -->
    <Card>
      <button @click="sayHello">点我打招呼</button>
    </Card>

    <!-- 第三个卡片:啥也不传,显示默认内容 -->
    <Card></Card>
  </div>
</template>

<script setup>
import Card from './components/Card.vue'

function sayHello() {
  alert('你好!')
}
</script>

案例小结: <slot></slot> 就是默认插槽。父组件在子组件标签里写的所有内容,都会出现在这个位置。子组件通过 <slot>默认内容</slot> 可以提供后备内容。


二、具名插槽:多个占位,精确控制

场景

一个页面布局组件,有头部、侧边栏、主内容区、底部四个区域。父组件需要分别往这四个地方塞不同内容。

1. 子组件 Layout.vue

vue

复制代码
<template>
  <div class="layout">
    <!-- 头部区域 -->
    <header class="layout-header">
      <!-- 给 slot 一个 name 属性,这就是具名插槽 -->
      <slot name="header">默认头部</slot>
    </header>

    <div class="layout-middle">
      <!-- 侧边栏区域 -->
      <aside class="layout-sidebar">
        <slot name="sidebar">默认侧边栏</slot>
      </aside>

      <!-- 主内容区域,没有名字的默认插槽 -->
      <main class="layout-main">
        <!-- 默认插槽,name 默认为 "default" -->
        <slot>默认主内容</slot>
      </main>
    </div>

    <!-- 底部区域 -->
    <footer class="layout-footer">
      <slot name="footer">默认底部</slot>
    </footer>
  </div>
</template>

<script setup>
</script>

<style scoped>
.layout-header, .layout-footer {
  padding: 10px;
  background: #e9ecef;
  text-align: center;
}
.layout-middle {
  display: flex;
}
.layout-sidebar {
  width: 200px;
  padding: 10px;
  background: #f8f9fa;
}
.layout-main {
  flex: 1;
  padding: 10px;
}
</style>

2. 父组件 App.vue

vue

复制代码
<template>
  <Layout>
    <!-- 
      通过 v-slot:插槽名 或 #插槽名 来指定内容放到哪个插槽 
      #是v-slot的简写
    -->
    <template #header>
      <h1>网站标题(父组件提供)</h1>
    </template>

    <!-- 侧边栏内容 -->
    <template #sidebar>
      <ul>
        <li>导航一</li>
        <li>导航二</li>
      </ul>
    </template>

    <!-- 默认插槽内容,可以直接写,不用包 template 也行 -->
    <div>
      <p>这里是主内容区域,可以放任何东西。</p>
      <p>包括其他组件、文本、图片等。</p>
    </div>

    <!-- 底部内容 -->
    <template #footer>
      <small>© 2025 我的公司</small>
    </template>
  </Layout>
</template>

<script setup>
import Layout from './components/Layout.vue'
</script>

案例小结: 具名插槽用 <slot name="xxx"> 定义,父组件用 <template v-slot:xxx><template #xxx> 往里面填内容。没有名字的就是默认插槽。


三、作用域插槽:子组件的数据,父组件来决定怎么展示

这是插槽里最强大、也最容易让新手懵的地方。一句话:数据在子组件里,但是渲染成什么样由父组件决定。

场景

一个商品列表组件,它负责获取数据和循环,但每一项的外观(是卡片、行、还是列表)由使用它的父组件决定。

1. 子组件 ProductList.vue

vue

复制代码
<template>
  <div>
    <h2>商品列表</h2>
    <!-- 循环商品数据 -->
    <div v-for="(product, index) in products" :key="product.id" class="product-item">
      <!-- 
        关键在这里:<slot> 除了占位置,还可以绑定属性传给父组件
        :product="product" 表示把当前循环的 product 对象暴露出去
        :index="index" 还可以暴露下标 
      -->
      <slot name="product" :product="product" :index="index">
        <!-- 后备内容:简单显示名字 -->
        {{ product.name }}
      </slot>
    </div>
  </div>
</template>

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

// 模拟商品数据
const products = ref([
  { id: 1, name: '键盘', price: 299 },
  { id: 2, name: '鼠标', price: 99 },
  { id: 3, name: '显示器', price: 1299 }
])
</script>

2. 父组件 App.vue

vue

复制代码
<template>
  <div>
    <!-- 第一种:用卡片方式展示 -->
    <ProductList>
      <!-- 
        v-slot:product="slotProps" 
        slotProps 就是子组件 <slot> 上绑定的所有属性的集合 
        这里 slotProps 包含 { product, index }
      -->
      <template #product="{ product, index }">
        <div style="border:1px solid #ddd; padding:10px; margin:5px;">
          <strong>#{{ index + 1 }}</strong> 
          <span>{{ product.name }}</span> 
          <span style="color:red;">¥{{ product.price }}</span>
        </div>
      </template>
    </ProductList>

    <hr />

    <!-- 第二种:用纯文字列表 -->
    <ProductList>
      <template #product="{ product }">
        <p>📦 {{ product.name }} —— {{ product.price }}元</p>
      </template>
    </ProductList>
  </div>
</template>

<script setup>
import ProductList from './components/ProductList.vue'
</script>

案例小结: 子组件通过 <slot :数据名="数据值"> 把数据传给父组件,父组件通过 v-slot="slotProps" 接收,然后可以随意解构使用。这样父组件就拥有了对子组件数据的"渲染控制权"。


四、动态插槽:插槽名也可以变

有时候一个组件会渲染多个不同名的插槽,但父组件想用同一个模板去填充,只是名字不同,这时可以用动态插槽。

vue

复制代码
<!-- 子组件 -->
<template>
  <div>
    <slot name="tab-home">首页</slot>
    <slot name="tab-about">关于</slot>
  </div>
</template>

父组件可以这样动态匹配:

vue

复制代码
<template>
  <MyTabs>
    <!-- [动态插槽名] 会根据 tabName 的值变化 -->
    <template v-for="tab in tabs" :key="tab.slotName" #[tab.slotName]>
      <p>{{ tab.content }}</p>
    </template>
  </MyTabs>
</template>

<script setup>
import { ref } from 'vue'
const tabs = ref([
  { slotName: 'tab-home', content: '首页内容' },
  { slotName: 'tab-about', content: '关于内容' }
])
</script>

本质还是具名插槽,只是名字用了变量。


练习题

选择题

  1. 在子组件中,用来定义插槽的标签是( )

    A. <template>

    B. <slot>

    C. <v-slot>

    D. <content>

  2. 父组件使用具名插槽时,正确的写法是( )

    A. <slot name="header">

    B. <template slot="header">

    C. <template v-slot:header><template #header>

    D. <div v-slot="header">

  3. 作用域插槽的核心作用是( )

    A. 子组件可以修改父组件的数据

    B. 父组件可以拿到子组件的数据,并决定如何渲染

    C. 插槽可以动态切换名称

    D. 子组件可以向父组件传递事件

判断题

  1. 一个组件里只能有一个 <slot>。( )

  2. 作用域插槽中,父组件拿到的数据是只读的,不能修改。( )

编程题

  1. 实现一个弹窗组件 Modal

    • 弹窗有一个头部(标题)和主体内容,以及底部按钮区

    • 头部标题父组件可以通过 prop 传递,但也可以使用具名插槽覆盖

    • 主体内容完全由父组件通过默认插槽决定

    • 底部按钮区由父组件通过具名插槽 footer 填充,如果没传则默认显示"关闭"按钮

    • 弹窗的显示/隐藏由父组件通过 v-model 控制

  2. 实现一个表格组件 DataTable

    • 表格接收一个 data 数组(每项是对象)和一个 columns 配置数组(如 [{key: 'name', title: '姓名'}, ...]

    • 默认情况下,表格用 v-for 渲染行和列,显示文本

    • 额外要求:允许父组件通过作用域插槽自定义某个列的渲染方式(比如把"姓名"列渲染成链接)


答案

  1. B

  2. C

  3. B

  4. 错误,可以有多个具名插槽和一个默认插槽。

  5. 正确,子组件传给插槽的数据应该由子组件修改,父组件不应直接改动,除非子组件提供了方法。

  6. 参考实现:

    Modal.vue

vue

复制代码
<template>
  <div v-if="modelValue" class="modal-mask" @click.self="close">
    <div class="modal-box">
      <div class="modal-header">
        <!-- 具名插槽 header,后备内容为 props.title -->
        <slot name="header">{{ title }}</slot>
      </div>
      <div class="modal-body">
        <!-- 默认插槽 -->
        <slot>主体内容</slot>
      </div>
      <div class="modal-footer">
        <slot name="footer">
          <button @click="close">关闭</button>
        </slot>
      </div>
    </div>
  </div>
</template>

<script setup>
defineProps({
  modelValue: Boolean,
  title: { type: String, default: '弹窗标题' }
})
const emit = defineEmits(['update:modelValue'])
function close() {
  emit('update:modelValue', false)
}
</script>

<style scoped>
.modal-mask {
  position: fixed; top:0; left:0; width:100%; height:100%;
  background: rgba(0,0,0,0.5); display:flex; align-items:center; justify-content:center;
}
.modal-box {
  background: white; border-radius: 8px; min-width: 300px;
}
.modal-header, .modal-body, .modal-footer { padding: 15px; }
</style>

父组件使用

vue

复制代码
<Modal v-model="showModal" title="用户登录">
  <template #header>
    <h2>自定义头部(覆盖了title prop)</h2>
  </template>
  <form>...表单内容...</form>
  <template #footer>
    <button @click="showModal=false">确定</button>
    <button @click="showModal=false">取消</button>
  </template>
</Modal>
  1. 参考实现:

    DataTable.vue

vue

复制代码
<template>
  <table>
    <thead>
      <tr><th v-for="col in columns" :key="col.key">{{ col.title }}</th></tr>
    </thead>
    <tbody>
      <tr v-for="(row, rowIndex) in data" :key="rowIndex">
        <td v-for="col in columns" :key="col.key">
          <!-- 这里定义了作用域插槽,名字为 'cell-'+col.key  -->
          <slot :name="'cell-' + col.key" :row="row" :value="row[col.key]" :col="col">
            {{ row[col.key] }}
          </slot>
        </td>
      </tr>
    </tbody>
  </table>
</template>

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

父组件使用(自定义姓名列)

vue

复制代码
<DataTable :data="users" :columns="columns">
  <!-- 动态插槽名,针对 'cell-name' 进行自定义 -->
  <template #cell-name="{ row, value }">
    <a :href="'/user/' + row.id">{{ value }}</a>
  </template>
</DataTable>

<script setup>
import { ref } from 'vue'
const users = ref([
  { id: 1, name: 'Alice', age: 25 },
  { id: 2, name: 'Bob', age: 30 }
])
const columns = [
  { key: 'name', title: '姓名' },
  { key: 'age', title: '年龄' }
]
</script>

写在最后

插槽是 Vue 组件复用中非常核心的一环,它让组件从"写死"变成了"可定制"。初学作用域插槽可能会有点绕,记住一句话:数据在孩子手里,但是家长决定孩子穿什么衣服。 多写几个弹窗、列表、布局组件,你就能体会到它的强大了。

老规矩,有问题评论区见,我挨个回。下篇咱们聊生命周期或者动画。

相关推荐
SEO-狼术17 小时前
Build Interactive Maps Crack
前端
小则又沐风a17 小时前
进程最终篇---进程控制(模拟实现xshell)
java·linux·服务器·前端
川冰ICE17 小时前
JavaScript工程化②|Webpack5基础配置,打包你的第一个项目
开发语言·javascript·ecmascript
YHHLAI17 小时前
JavaScript 同步异步精讲:单线程、事件循环、Promise 执行机制
开发语言·javascript·ecmascript
Web打印17 小时前
HttpPrinter web打印控件 官方文档(https://wiki.httpprinter.com/)快速检索目录
java·javascript·chrome
Invictus_cl17 小时前
条纹圆形进度条(彩虹色)
开发语言·前端·javascript
excel17 小时前
从封装到继承:深入理解 TypeScript 类中的 public、private、protected、static
前端
imkaifan17 小时前
工作流(Worker/Graph)配置对象如何解读、子图
javascript·工作流·(worker/graph)·配置对象如何解读·子图
向日的葵00617 小时前
vue3路由的replace属性(四)
前端·javascript·vue.js·vue路由