欢迎光临
我们一直在努力

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>

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


练习题

选择题

  • 在子组件中,用来定义插槽的标签是( )
    A. <template>
    B. <slot>
    C. <v-slot>
    D. <content>

  • 父组件使用具名插槽时,正确的写法是( )
    A. <slot name="header">
    B. <template slot="header">
    C. <template v-slot:header> 或 <template #header>
    D. <div v-slot="header">

  • 作用域插槽的核心作用是( )
    A. 子组件可以修改父组件的数据
    B. 父组件可以拿到子组件的数据,并决定如何渲染
    C. 插槽可以动态切换名称
    D. 子组件可以向父组件传递事件

  • 判断题

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

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

  • 编程题

  • 实现一个弹窗组件 Modal:

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

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

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

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

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

  • 实现一个表格组件 DataTable:

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

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

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


  • 答案

  • B

  • C

  • B

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

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

  • 参考实现:
    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>

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

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

    赞(0)
    未经允许不得转载:171主机测评 » Vue插槽从入门到项目实战:默认插槽、具名插槽、作用域插槽,一次讲明白
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址