插槽(Slot)实战:默认插槽、具名插槽、作用域插槽、插槽的复用场景
插槽这个东西,说白了就是 Vue 组件里的一块「预留区域」。你写组件的时候,有些内容不确定,那就留个坑,让使用者来填。我刚开始学 Vue 时,觉得插槽不就是个占位符吗?后来做复杂组件才发现,它其实是组件复用性的灵魂。
今天咱们就把插槽的四种形态——默认插槽、具名插槽、作用域插槽、以及复用场景,一次性讲透。
一、默认插槽:最简单的占位方式
先看最基础的。你写一个卡片组件,卡片标题和内容不确定,那就用 <slot> 留个位置。
<!-- Card.vue -->
<template>
<div class="card">
<div class="card-header">卡片标题</div>
<div class="card-body">
<slot></slot>
</div>
</div>
</template>
使用时,直接往组件标签里塞内容:
<Card>
<p>这是一段卡片内容</p>
</Card>
如果没传内容,<slot> 里可以写默认值:
<slot>默认显示的内容</slot>
二、具名插槽:一个组件多个坑
一个组件只有一个插槽?那太局限了。比如一个布局组件,有头部、侧边栏、主内容区,每个区域都需要独立插槽。
这时候就用 name 属性:
<!-- Layout.vue -->
<template>
<div class="layout">
<header>
<slot name="header">默认头部</slot>
</header>
<aside>
<slot name="sidebar">默认侧边栏</slot>
</aside>
<main>
<slot name="main">默认主内容</slot>
</main>
</div>
</template>
使用时,用 v-slot 指定名字:
<Layout>
<template v-slot:header>
<h1>网站标题</h1>
</template>
<template v-slot:sidebar>
<ul><li>导航1</li></ul>
</template>
<template v-slot:main>
<p>正文内容</p>
</template>
</Layout>
v-slot 可以简写为 #:
<template #header>...</template>
v-slot:default 包裹,否则会报错。Vue 3 修复了这个问题,但如果你还在维护老项目,千万注意。
三、作用域插槽:让父组件访问子组件的数据
默认插槽和具名插槽,都是父组件往子组件里塞「静态内容」。但有时候,子组件内部有数据,想让父组件决定「怎么渲染」——这就是作用域插槽的用武之地。
举个例子,一个列表组件:
<!-- List.vue -->
<template>
<ul>
<li v-for="item in items" :key="item.id">
<slot :item="item" :index="index">
{{ item.name }}
</slot>
</li>
</ul>
</template>
<script>
export default {
data() {
return {
items: [
{ id: 1, name: '苹果', price: 5 },
{ id: 2, name: '香蕉', price: 3 }
]
}
}
}
</script>
父组件使用时,通过 v-slot 接收数据:
<List>
<template v-slot:default="slotProps">
<span>{{ slotProps.item.name }} - ¥{{ slotProps.item.price }}</span>
</template>
</List>
也可以解构:
<List>
<template v-slot="{ item }">
<span>{{ item.name }} - ¥{{ item.price }}</span>
</template>
</List>
四、插槽的复用场景:实战中的三个典型
光讲理论没意思。我挑三个真实项目里高频出现的场景,咱们看看插槽怎么用。
场景一:弹窗组件
弹窗的标题、内容、底部按钮,每个项目都不一样。用插槽最合适:
<!-- Modal.vue -->
<template>
<div class="modal-overlay" v-if="visible">
<div class="modal">
<div class="modal-header">
<slot name="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>
使用时,想怎么定制都行:
<Modal :visible="showModal">
<template #title>确认删除?</template>
<p>删除后无法恢复,确定要删除吗?</p>
<template #footer>
<button @click="cancel">取消</button>
<button @click="confirm" class="danger">确认删除</button>
</template>
</Modal>
场景二:表格列自定义
表格组件里,每一列的渲染方式可能不同。有的列要显示图片,有的要显示状态标签,有的要显示操作按钮。作用域插槽完美解决:
<!-- Table.vue -->
<template>
<table>
<thead>
<tr>
<th v-for="col in columns" :key="col.key">{{ col.title }}</th>
</tr>
</thead>
<tbody>
<tr v-for="row in data" :key="row.id">
<td v-for="col in columns" :key="col.key">
<slot :name="col.key" :row="row" :value="row[col.key]">
{{ row[col.key] }}
</slot>
</td>
</tr>
</tbody>
</table>
</template>
使用时,对特定列做定制:
<Table :columns="columns" :data="list">
<template #status="{ value }">
<span :class="value === 'active' ? 'green' : 'gray'">
{{ value === 'active' ? '启用' : '停用' }}
</span>
</template>
<template #action="{ row }">
<button @click="edit(row)">编辑</button>
<button @click="delete(row)">删除</button>
</template>
</Table>
场景三:表单组件
表单的每个表单项,验证规则、布局、样式都可能不同。用插槽把「表单项内容」和「表单容器」解耦:
<!-- FormItem.vue -->
<template>
<div class="form-item">
<label>{{ label }}</label>
<div class="form-control">
<slot :error="error"></slot>
<span v-if="error" class="error-msg">{{ error }}</span>
</div>
</div>
</template>
使用时,传入任意类型的输入控件:
<FormItem label="用户名" :error="errors.username">
<template v-slot="{ error }">
<input
v-model="form.username"
:class="{ 'is-error': error }"
placeholder="请输入用户名"
/>
</template>
</FormItem>
五、总结一下
插槽这东西,用好了能让组件复用性翻倍。我个人的经验是:
- 默认插槽:适合简单的内容嵌入,比如卡片、弹窗主体
- 具名插槽:适合多区域布局,比如弹窗的标题、内容、底部
- 作用域插槽:适合需要父组件自定义渲染的场景,比如表格列、列表项
- 复用场景:弹窗、表格、表单、列表、布局组件,都是插槽的高频使用地
你想想看,如果没有插槽,每个组件都得写死内容,那还谈什么复用?插槽就是 Vue 组件化里「开放封闭原则」的最佳实践——对扩展开放,对修改封闭。
下一章咱们聊聊「混入(Mixin)与组合式函数(Composables)」,看看怎么把组件里的逻辑抽出来复用。到时候你会发现,插槽负责视图复用,混入和组合式函数负责逻辑复用,两者配合才是完整的组件化方案。