插槽(Slot)是Vue组件提供的一种内容分发机制。它允许你在组件模板中预留位置,让父组件可以插入自定义的内容。
可以插入任意内容
组件像乐高一样组合
同一组件不同内容
单个内容插槽
基本多个指定位置插槽
常用数据回传的插槽
高级| 插槽类型 | 语法 | 描述 | 使用场景 |
|---|---|---|---|
| 默认插槽 | <slot></slot> |
未命名的插槽,接收所有未匹配的内容 | 简单内容分发 |
| 具名插槽 | <slot name="header"></slot> |
有名称的插槽,可以指定插入位置 | 布局组件、卡片组件 |
| 作用域插槽 | <slot :item="item"></slot> |
子组件向插槽内容传递数据 | 列表渲染、数据表格 |
| 后备内容 | <slot>默认内容</slot> |
当父组件没有提供内容时显示的默认内容 | 可配置的默认状态 |
| 动态插槽名 | <slot :name="slotName"></slot> |
动态决定插槽名称 | 高度动态的布局 |
最简单的插槽形式,用于接收父组件传递的所有内容:
<!-- Alert.vue - 子组件 -->
<template>
<div class="alert" :class="type">
<!-- 默认插槽 -->
<slot></slot>
<!-- 带后备内容的插槽 -->
<slot>
<!-- 后备内容:当父组件没有提供内容时显示 -->
默认警告信息
</slot>
</div>
</template>
<script>
export default {
name: 'Alert',
props: {
type: {
type: String,
default: 'info',
validator: value => ['info', 'success', 'warning', 'danger'].includes(value)
}
}
}
</script>
<style scoped>
.alert {
padding: 15px;
border-radius: 5px;
margin: 10px 0;
}
.alert.info {
background-color: #d1ecf1;
border: 1px solid #bee5eb;
color: #0c5460;
}
.alert.success {
background-color: #d4edda;
border: 1px solid #c3e6cb;
color: #155724;
}
.alert.warning {
background-color: #fff3cd;
border: 1px solid #ffeaa7;
color: #856404;
}
.alert.danger {
background-color: #f8d7da;
border: 1px solid #f5c6cb;
color: #721c24;
}
</style>
<!-- 父组件使用 -->
<template>
<div>
<!-- 使用自定义内容 -->
<alert type="success">
<strong>操作成功!</strong> 您的数据已保存。
</alert>
<!-- 使用复杂内容 -->
<alert type="warning">
<div>
<h4>警告</h4>
<p>即将删除重要数据,请确认操作。</p>
<button @click="confirmDelete">确认删除</button>
</div>
</alert>
<!-- 不提供内容,显示后备内容 -->
<alert type="info" />
<!-- 使用动态内容 -->
<alert type="danger">
<p>错误代码: {{ errorCode }}</p>
<p>错误信息: {{ errorMessage }}</p>
</alert>
</div>
</template>
<script>
import Alert from './Alert.vue'
export default {
components: {
Alert
},
data() {
return {
errorCode: 404,
errorMessage: '页面不存在'
}
},
methods: {
confirmDelete() {
console.log('删除确认');
}
}
}
</script>
即将删除重要数据,请确认操作。
错误代码: 404
错误信息: 页面不存在
当组件需要多个插槽时,可以使用具名插槽来区分不同的内容位置:
<!-- Card.vue - 卡片组件 -->
<template>
<div class="card">
<!-- 头部插槽 -->
<div class="card-header" v-if="$slots.header">
<slot name="header"></slot>
</div>
<!-- 默认内容插槽 -->
<div class="card-body">
<slot>
<!-- 默认内容 -->
卡片内容
</slot>
</div>
<!-- 底部插槽 -->
<div class="card-footer" v-if="$slots.footer">
<slot name="footer"></slot>
</div>
<!-- 额外操作插槽 -->
<div class="card-actions" v-if="$slots.actions">
<slot name="actions"></slot>
</div>
</div>
</template>
<!-- 父组件使用具名插槽 -->
<template>
<div>
<!-- 使用v-slot指令 -->
<card>
<template v-slot:header>
<h3>用户信息</h3>
</template>
<!-- 默认插槽可以简写 -->
<p>用户名: 张三</p>
<p>邮箱: zhangsan@example.com</p>
<template v-slot:footer>
<small class="text-muted">最后更新: 2024-01-15</small>
</template>
<template v-slot:actions>
<button class="btn btn-primary">编辑</button>
<button class="btn btn-danger">删除</button>
</template>
</card>
<!-- 简写语法(Vue 2.6+) -->
<card>
<template #header>
<h3>产品信息</h3>
</template>
<p>产品名称: Vue.js实战</p>
<p>价格: ¥89.00</p>
<template #footer>
<small class="text-muted">库存: 100件</small>
</template>
<template #actions>
<button class="btn btn-success">加入购物车</button>
</template>
</card>
</div>
</template>
作用域插槽允许子组件向插槽内容传递数据,实现更灵活的渲染控制:
绑定数据到插槽
<slot :item="item">
通过slot props传递
接收并渲染数据
v-slot="slotProps"
<!-- ListComponent.vue - 子组件 -->
<template>
<div class="list">
<ul>
<li v-for="item in items" :key="item.id">
<!-- 作用域插槽:向父组件传递item数据 -->
<slot :item="item" :index="index">
<!-- 默认渲染 -->
{{ item.name }}
</slot>
</li>
</ul>
<!-- 多个数据的作用域插槽 -->
<slot
:items="items"
:total="items.length"
:selected="selectedItem"
></slot>
</div>
</template>
<script>
export default {
name: 'ListComponent',
props: {
items: {
type: Array,
required: true,
default: () => []
}
},
data() {
return {
selectedItem: null
}
}
}
</script>
<!-- 父组件使用 -->
<template>
<div>
<!-- 基本用法 -->
<list-component :items="users">
<template v-slot:default="slotProps">
<li>
<strong>{{ slotProps.item.name }}</strong>
<span> - {{ slotProps.item.email }}</span>
<button @click="selectUser(slotProps.item)">
选择
</button>
</li>
</template>
</list-component>
<!-- 解构语法(推荐) -->
<list-component :items="products">
<template v-slot:default="{ item, index }">
<li :class="{ 'active': index % 2 === 0 }">
{{ index + 1 }}. {{ item.name }} - ¥{{ item.price }}
</li>
</template>
</list-component>
<!-- 简写语法 -->
<list-component :items="orders">
<template #default="{ item }">
<div class="order-item">
<h4>{{ item.orderNumber }}</h4>
<p>状态: {{ item.status }}</p>
<p>金额: ¥{{ item.amount }}</p>
</div>
</template>
</list-component>
<!-- 使用多个数据的插槽 -->
<list-component :items="data">
<template #default="{ items, total, selected }">
<div>
<p>共 {{ total }} 条记录</p>
<div v-if="selected">
已选择: {{ selected.name }}
</div>
</div>
</template>
</list-component>
</div>
</template>
<script>
export default {
data() {
return {
users: [
{ id: 1, name: '张三', email: 'zhangsan@example.com' },
{ id: 2, name: '李四', email: 'lisi@example.com' }
],
products: [
{ id: 1, name: 'Vue.js实战', price: 89 },
{ id: 2, name: 'React进阶', price: 99 }
],
orders: [
{ orderNumber: 'ORD001', status: '已发货', amount: 299 },
{ orderNumber: 'ORD002', status: '处理中', amount: 599 }
]
}
},
methods: {
selectUser(user) {
console.log('选择用户:', user)
}
}
}
</script>
在实际项目中,经常需要组合使用多种插槽:
<!-- DataTable.vue -->
<template>
<div class="data-table">
<!-- 表格标题 -->
<slot name="header">
<h3>数据表格</h3>
</slot>
<!-- 表格内容 -->
<table>
<thead>
<slot name="thead">
<tr>
<th v-for="col in columns" :key="col.key">
{{ col.title }}
</th>
</tr>
</slot>
</thead>
<tbody>
<tr v-for="(item, index) in data" :key="item.id">
<slot name="tbody" :item="item" :index="index">
<!-- 默认单元格渲染 -->
<td v-for="col in columns" :key="col.key">
{{ item[col.key] }}
</td>
</slot>
</tr>
</tbody>
</table>
<!-- 表格底部 -->
<slot name="footer" :data="data" :total="data.length">
<div>共 {{ data.length }} 条记录</div>
</slot>
</div>
</template>
<!-- Layout.vue -->
<template>
<div class="layout">
<header class="header">
<slot name="header">默认头部</slot>
</header>
<div class="content">
<aside class="sidebar" v-if="$slots.sidebar">
<slot name="sidebar"></slot>
</aside>
<main class="main">
<slot>主要内容区域</slot>
</main>
</div>
<footer class="footer">
<slot name="footer">默认底部</slot>
</footer>
</div>
</template>
<!-- RenderList.vue -->
<template>
<div class="render-list">
<!-- 列表头部 -->
<slot name="before-list"></slot>
<!-- 列表项渲染 -->
<div v-for="(item, index) in items" :key="item.id">
<slot name="item" :item="item" :index="index">
<!-- 默认列表项渲染 -->
<div class="list-item">
{{ index + 1 }}. {{ item.name }}
</div>
</slot>
<!-- 分隔符插槽 -->
<slot
v-if="index < items.length - 1"
name="separator"
:current="item"
:next="items[index + 1]"
>
<hr>
</slot>
</div>
<!-- 列表底部 -->
<slot name="after-list" :items="items"></slot>
</div>
</template>
创建一个完整的可复用数据表格组件:
// DataTable.vue
export default {
name: 'DataTable',
props: {
data: {
type: Array,
required: true,
default: () => []
},
columns: {
type: Array,
default: () => []
},
loading: {
type: Boolean,
default: false
},
emptyText: {
type: String,
default: '暂无数据'
}
},
data() {
return {
sortBy: '',
sortDirection: 'asc'
};
},
computed: {
sortedData() {
if (!this.sortBy) return this.data;
return [...this.data].sort((a, b) => {
const aVal = a[this.sortBy];
const bVal = b[this.sortBy];
if (this.sortDirection === 'asc') {
return aVal > bVal ? 1 : -1;
} else {
return aVal < bVal ? 1 : -1;
}
});
}
},
methods: {
handleSort(column) {
if (this.sortBy === column.key) {
this.sortDirection = this.sortDirection === 'asc' ? 'desc' : 'asc';
} else {
this.sortBy = column.key;
this.sortDirection = 'asc';
}
this.$emit('sort-change', {
sortBy: this.sortBy,
sortDirection: this.sortDirection
});
}
},
template: `
<div class="data-table">
<!-- 表格标题插槽 -->
<div class="table-header">
<slot name="header">
<h4>数据列表</h4>
</slot>
<!-- 工具栏插槽 -->
<slot name="toolbar" :data="data"></slot>
</div>
<!-- 加载状态插槽 -->
<slot v-if="loading" name="loading">
<div class="loading">加载中...</div>
</slot>
<!-- 空状态插槽 -->
<slot v-else-if="data.length === 0" name="empty">
<div class="empty">{{ emptyText }}</div>
</slot>
<!-- 表格主体 -->
<table v-else>
<thead>
<tr>
<!-- 表头插槽 -->
<slot name="thead" :columns="columns" :sortBy="sortBy" :sortDirection="sortDirection">
<th
v-for="col in columns"
:key="col.key"
@click="col.sortable ? handleSort(col) : null"
:class="{ sortable: col.sortable, active: sortBy === col.key }"
>
{{ col.title }}
<span v-if="sortBy === col.key" class="sort-indicator">
{{ sortDirection === 'asc' ? '↑' : '↓' }}
</span>
</th>
</slot>
</tr>
</thead>
<tbody>
<!-- 行插槽 -->
<slot name="tbody" :data="sortedData" :columns="columns">
<tr v-for="(item, index) in sortedData" :key="item.id">
<!-- 单元格插槽 -->
<slot name="tbody-cell" :item="item" :index="index" :columns="columns">
<td v-for="col in columns" :key="col.key">
{{ item[col.key] }}
</td>
</slot>
<!-- 操作列插槽 -->
<td v-if="$slots.actions">
<slot name="actions" :item="item" :index="index"></slot>
</td>
</tr>
</slot>
</tbody>
</table>
<!-- 分页插槽 -->
<slot name="pagination" :data="data">
<div class="pagination">
显示 {{ data.length }} 条记录
</div>
</slot>
<!-- 底部统计插槽 -->
<slot name="footer" :data="data">
<div class="table-footer">
共 {{ data.length }} 条数据
</div>
</slot>
</div>
`
};
<!-- 使用数据表格组件 -->
<template>
<div>
<data-table
:data="users"
:columns="columns"
:loading="loading"
@sort-change="handleSortChange"
>
<!-- 自定义标题 -->
<template #header>
<div class="d-flex justify-content-between align-items-center">
<h3>用户管理</h3>
<button class="btn btn-primary" @click="addUser">
添加用户
</button>
</div>
</template>
<!-- 自定义表头 -->
<template #thead="{ columns }">
<tr>
<th v-for="col in columns" :key="col.key">
{{ col.title }}
</th>
<th>操作</th>
</tr>
</template>
<!-- 自定义行内容 -->
<template #tbody="{ data, columns }">
<tr v-for="user in data" :key="user.id">
<td>{{ user.id }}</td>
<td>
<div class="d-flex align-items-center">
<img :src="user.avatar" class="avatar">
<span>{{ user.name }}</span>
</div>
</td>
<td>{{ user.email }}</td>
<td>
<span :class="['badge', user.status === 'active' ? 'bg-success' : 'bg-secondary']">
{{ user.status }}
</span>
</td>
<td>{{ formatDate(user.createdAt) }}</td>
<!-- 自定义操作按钮 -->
<td>
<template #actions="{ item }">
<button @click="editUser(item)" class="btn btn-sm btn-outline-primary">
编辑
</button>
<button @click="deleteUser(item)" class="btn btn-sm btn-outline-danger">
删除
</button>
</template>
</td>
</tr>
</template>
<!-- 自定义空状态 -->
<template #empty>
<div class="text-center py-5">
<i class="fas fa-users fa-3x text-muted mb-3"></i>
<p>暂无用户数据</p>
<button @click="reloadData" class="btn btn-primary">
重新加载
</button>
</div>
</template>
<!-- 自定义分页 -->
<template #pagination="{ data }">
<div class="d-flex justify-content-between align-items-center">
<div>共 {{ data.length }} 条记录</div>
<div class="pagination-controls">
<button class="btn btn-sm" :disabled="currentPage === 1">
上一页
</button>
<span class="mx-2">第 {{ currentPage }} 页</span>
<button class="btn btn-sm" :disabled="currentPage === totalPages">
下一页
</button>
</div>
</div>
</template>
</data-table>
</div>
</template>
<!-- 子组件 -->
<template>
<div>
<slot :name="slotName">动态插槽</slot>
</div>
</template>
<script>
export default {
data() {
return {
slotName: 'header' // 可以动态改变
}
}
}
</script>
<!-- 父组件使用 -->
<child-component>
<template #[slotName]>
动态插入的内容
</template>
</child-component>
<!-- WrapperComponent.vue -->
<template>
<base-component>
<!-- 转发所有插槽 -->
<template v-for="slot in Object.keys($slots)" #[slot]="scope">
<slot :name="slot" v-bind="scope" />
</template>
<!-- 转发作用域插槽 -->
<template v-slot:item="{ item }">
<slot name="item" :item="item" />
</template>
</base-component>
</template>
// 使用函数作为插槽内容
export default {
methods: {
renderItem(item) {
return this.$createElement('div', [
this.$createElement('strong', item.name),
this.$createElement('span', ` - ${item.email}`)
]);
}
},
template: `
<list-component :items="users">
<template v-slot:default="{ item }">
<!-- 使用函数渲染 -->
{{ renderItem(item) }}
</template>
</list-component>
`
};
v-if="$slots.name"v-once提高静态内容性能现象:插槽内访问不到父组件数据
解决:插槽内容在父组件作用域编译
现象:插槽内容样式影响组件
解决:使用scoped样式或CSS Modules
现象:v-if导致插槽不显示
解决:使用v-show或检查$slots
现象:复杂插槽导致渲染缓慢
解决:优化插槽内容,使用虚拟滚动
为以下模态框组件设计合适的插槽:
// Modal.vue - 模态框组件
Vue.component('Modal', {
props: {
visible: Boolean,
title: String,
size: {
type: String,
default: 'medium'
}
},
// TODO: 设计插槽
// 考虑以下需求:
// 1. 自定义标题区域
// 2. 自定义内容区域
// 3. 自定义底部按钮区域
// 4. 自定义关闭按钮
// 5. 加载状态插槽
template: `
<div v-if="visible" class="modal">
<div class="modal-dialog" :class="'modal-' + size">
<div class="modal-content">
<!-- TODO: 实现插槽 -->
</div>
</div>
</div>
`
});