Appearance
插槽 (Slots)
插槽基础
插槽(Slots)是Vue.js中实现内容分发的重要机制,允许父组件向子组件传递模板内容。插槽使得组件更加灵活和可重用。
默认插槽
最简单的插槽形式,子组件中使用slot元素作为内容分发的出口:
vue
<!-- 子组件 ButtonComponent.vue -->
<template>
<button class="btn">
<slot></slot>
</button>
</template>
<style>
.btn {
padding: 8px 16px;
border: 1px solid #ccc;
background-color: #f5f5f5;
cursor: pointer;
}
</style>
vue
<!-- 父组件使用 -->
<template>
<div>
<ButtonComponent>Click me!</ButtonComponent>
<ButtonComponent>
<span>Button with <strong>HTML</strong></span>
</ButtonComponent>
</div>
</template>
具名插槽
当需要多个插槽时,可以使用具名插槽。通过slot元素的name属性指定插槽名称:
vue
<!-- 子组件 LayoutComponent.vue -->
<template>
<div class="layout">
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot> <!-- 默认插槽 -->
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>
</template>
<style>
.layout {
display: flex;
flex-direction: column;
min-height: 100vh;
}
header, footer {
background-color: #f0f0f0;
padding: 1rem;
}
main {
flex: 1;
padding: 1rem;
}
</style>
vue
<!-- 父组件使用 -->
<template>
<LayoutComponent>
<template #header>
<h1>Website Header</h1>
</template>
<p>Main content goes here</p>
<template #footer>
<p>Footer content</p>
</template>
</LayoutComponent>
</template>
</template>
作用域插槽
作用域插槽允许子组件向父组件传递数据,使父组件可以基于子组件的数据来渲染内容。
基础作用域插槽
vue
<!-- 子组件 UserCard.vue -->
<template>
<div class="user-card">
<slot
:user="user"
:is-admin="user.role === 'admin'"
:formatted-name="formatName(user.name)"
></slot>
</div>
</template>
<script>
export default {
data() {
return {
user: {
id: 1,
name: 'John Doe',
email: 'john@example.com',
role: 'admin'
}
}
},
methods: {
formatName(name) {
return name.toUpperCase()
}
}
}
</script>
<style>
.user-card {
border: 1px solid #ddd;
padding: 1rem;
margin: 1rem 0;
}
</style>
vue
<!-- 父组件使用 -->
<template>
<UserCard v-slot="slotProps">
<h3>{{ slotProps.formattedName }}</h3>
<p>Email: {{ slotProps.user.email }}</p>
<p v-if="slotProps.is-admin" class="admin-badge">Administrator</p>
</UserCard>
</template>
列表组件中的作用域插槽
vue
<!-- 子组件 UserList.vue -->
<template>
<ul class="user-list">
<li v-for="user in users" :key="user.id" class="user-item">
<slot
:user="user"
:index="user.index"
:is-even="user.index % 2 === 0"
:is-admin="user.role === 'admin'"
></slot>
</li>
</ul>
</template>
<script>
export default {
data() {
return {
users: [
{ id: 1, name: 'Alice', email: 'alice@example.com', role: 'admin', index: 0 },
{ id: 2, name: 'Bob', email: 'bob@example.com', role: 'user', index: 1 },
{ id: 3, name: 'Charlie', email: 'charlie@example.com', role: 'user', index: 2 }
]
}
}
}
</script>
<style>
.user-list {
list-style: none;
padding: 0;
}
.user-item {
padding: 0.5rem;
border-bottom: 1px solid #eee;
}
.user-item:last-child {
border-bottom: none;
}
</style>
vue
<!-- 父组件使用 -->
<template>
<UserList v-slot="slotProps">
<div class="user-info">
<span
:class="{
'admin': slotProps.is-admin,
'even': slotProps.is-even
}"
>
{{ slotProps.index + 1 }}. {{ slotProps.user.name }}
</span>
<button v-if="slotProps.is-admin" class="admin-btn">Admin Action</button>
</div>
</UserList>
</template>
<style>
.admin {
font-weight: bold;
color: #42b983;
}
.even {
background-color: #f9f9f9;
}
.admin-btn {
background-color: #e74c3c;
color: white;
border: none;
padding: 4px 8px;
border-radius: 4px;
cursor: pointer;
}
</style>
</template>
组合式API中的插槽
在组合式API中,可以使用useSlots和useAttrs来处理插槽和属性:
vue
<!-- 子组件 AdvancedCard.vue -->
<script setup>
import { useSlots, useAttrs } from 'vue'
const slots = useSlots()
const attrs = useAttrs()
// 检查是否有特定插槽
const hasActionSlot = computed(() => !!slots.action)
const hasHeaderSlot = computed(() => !!slots.header)
</script>
<template>
<div class="advanced-card" v-bind="attrs">
<div v-if="hasHeaderSlot" class="card-header">
<slot name="header" />
</div>
<div class="card-body">
<slot />
</div>
<div v-if="hasActionSlot" class="card-actions">
<slot name="action" />
</div>
</div>
</template>
<style>
.advanced-card {
border: 1px solid #ddd;
border-radius: 8px;
overflow: hidden;
}
.card-header {
background-color: #f8f9fa;
padding: 1rem;
border-bottom: 1px solid #dee2e6;
}
.card-body {
padding: 1rem;
}
.card-actions {
padding: 1rem;
border-top: 1px solid #dee2e6;
text-align: right;
}
</style>
作用域插槽在组合式API中
vue
<!-- 子组件 DataList.vue -->
<script setup>
import { computed } from 'vue'
const props = defineProps({
items: {
type: Array,
default: () => []
}
})
// 计算属性,提供额外的数据给插槽
const processedItems = computed(() => {
return props.items.map((item, index) => ({
...item,
index,
isEven: index % 2 === 0,
isLast: index === props.items.length - 1
}))
})
</script>
<template>
<div class="data-list">
<div
v-for="item in processedItems"
:key="item.id"
class="list-item"
:class="{ 'even': item.isEven, 'last': item.isLast }"
>
<slot
:item="item"
:index="item.index"
:is-even="item.isEven"
:formatted-data="item.name.toUpperCase()"
/>
</div>
</div>
</template>
<style>
.data-list {
margin: 1rem 0;
}
.list-item {
padding: 0.75rem;
border-bottom: 1px solid #eee;
}
.list-item.even {
background-color: #fafafa;
}
.list-item.last {
border-bottom: none;
}
</style>
vue
<!-- 父组件使用 -->
<script setup>
import { ref } from 'vue'
import DataList from './DataList.vue'
const items = ref([
{ id: 1, name: 'Item 1', category: 'A' },
{ id: 2, name: 'Item 2', category: 'B' },
{ id: 3, name: 'Item 3', category: 'A' }
])
</script>
<template>
<DataList :items="items" v-slot="slotProps">
<div class="item-content">
<h4>{{ slotProps.formattedData }}</h4>
<p>Category: {{ slotProps.item.category }}</p>
<span class="index">Index: {{ slotProps.index }}</span>
<span v-if="slotProps.is-even" class="badge">Even</span>
</div>
</DataList>
</template>
<style>
.item-content {
display: flex;
align-items: center;
gap: 1rem;
}
.badge {
background-color: #007bff;
color: white;
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-size: 0.8rem;
}
.index {
font-size: 0.8rem;
color: #666;
}
</style>
插槽的默认内容
插槽可以包含默认内容,当父组件没有提供内容时显示:
vue
<!-- 子组件 AlertComponent.vue -->
<template>
<div class="alert" :class="`alert-${type}`">
<slot>
<span class="default-message">Default alert message</span>
</slot>
</div>
</template>
<script>
export default {
props: {
type: {
type: String,
default: 'info'
}
}
}
</script>
<style>
.alert {
padding: 1rem;
margin: 1rem 0;
border: 1px solid transparent;
border-radius: 4px;
}
.alert-info {
color: #0c5460;
background-color: #d1ecf1;
border-color: #bee5eb;
}
.alert-warning {
color: #856404;
background-color: #fff3cd;
border-color: #ffeaa7;
}
.alert-error {
color: #721c24;
background-color: #f8d7da;
border-color: #f5c6cb;
}
</style>
vue
<!-- 父组件使用 -->
<template>
<div>
<!-- 使用默认内容 -->
<AlertComponent />
<!-- 提供自定义内容 -->
<AlertComponent type="warning">
<strong>Warning!</strong> This is a warning message.
</AlertComponent>
<!-- 提供复杂内容 -->
<AlertComponent type="error">
<div class="custom-alert">
<h4>Error Occurred</h4>
<p>Please check your input and try again.</p>
<button @click="handleRetry">Retry</button>
</div>
</AlertComponent>
</div>
</template>
动态插槽名
Vue 2.6.0+ 支持动态插槽名,使用方括号语法:
vue
<!-- 子组件 DynamicLayout.vue -->
<template>
<div class="dynamic-layout">
<header>
<slot :name="headerSlotName"></slot>
</header>
<main>
<slot></slot>
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>
</template>
<script>
export default {
props: {
headerSlotName: {
type: String,
default: 'header'
}
}
}
</script>
vue
<!-- 父组件使用 -->
<template>
<DynamicLayout :header-slot-name="dynamicHeader">
<p>Main content</p>
<template #[dynamicHeader]>
<h1>Dynamic Header Content</h1>
</template>
<template #footer>
<p>Footer content</p>
</template>
</DynamicLayout>
</template>
<script>
export default {
data() {
return {
dynamicHeader: 'main-header'
}
}
}
</script>
</template>
插槽高级用法
插槽与条件渲染
vue
<!-- 子组件 ConditionalCard.vue -->
<template>
<div class="conditional-card">
<div v-if="$slots.header" class="card-header">
<slot name="header" />
</div>
<div class="card-body">
<slot />
</div>
<div v-if="showFooter" class="card-footer">
<slot name="footer">
<p>Default footer</p>
</slot>
</div>
</div>
</template>
<script>
export default {
props: {
showFooter: {
type: Boolean,
default: true
}
}
}
</script>
插槽与循环
vue
<!-- 子组件 TabContainer.vue -->
<template>
<div class="tab-container">
<div class="tab-headers">
<button
v-for="(tab, index) in tabs"
:key="index"
@click="activeTab = index"
:class="{ active: activeTab === index }"
>
{{ tab.title }}
</button>
</div>
<div class="tab-content">
<slot
v-for="(tab, index) in tabs"
:name="`tab-${index}`"
:tab="tab"
:active="activeTab === index"
/>
</div>
</div>
</template>
<script>
export default {
data() {
return {
activeTab: 0,
tabs: [
{ title: 'Tab 1', content: 'Content 1' },
{ title: 'Tab 2', content: 'Content 2' },
{ title: 'Tab 3', content: 'Content 3' }
]
}
}
}
</script>
插槽最佳实践
1. 插槽命名规范
vue
<!-- 推荐:使用有意义的插槽名 -->
<template>
<component>
<template #header>Header Content</template>
<template #sidebar>Sidebar Content</template>
<template #main>Primary Content</template>
<template #footer>Footer Content</template>
</component>
</template>
2. 提供清晰的插槽文档
vue
<!-- 组件 Card.vue -->
<template>
<!--
插槽:
- header: 卡片头部内容
- default: 卡片主体内容
- footer: 卡片底部内容 (可选)
- actions: 操作按钮区域 (可选)
-->
<div class="card">
<header v-if="$slots.header" class="card-header">
<slot name="header" />
</header>
<div class="card-body">
<slot />
</div>
<footer v-if="$slots.footer" class="card-footer">
<slot name="footer" />
</footer>
<div v-if="$slots.actions" class="card-actions">
<slot name="actions" />
</div>
</div>
</template>
3. 使用作用域插槽提供灵活的数据访问
vue
<!-- 列表组件 List.vue -->
<script setup>
import { computed } from 'vue'
const props = defineProps({
items: Array
})
const processedItems = computed(() => {
return props.items.map((item, index) => ({
...item,
index,
isEven: index % 2 === 0,
isOdd: index % 2 === 1,
isFirst: index === 0,
isLast: index === props.items.length - 1
}))
})
</script>
<template>
<ul>
<li
v-for="item in processedItems"
:key="item.id"
:class="{ even: item.isEven, odd: item.isOdd }"
>
<slot
:item="item"
:index="item.index"
:is-first="item.isFirst"
:is-last="item.isLast"
:formatted-value="item.value ? item.value.toUpperCase() : ''"
/>
</li>
</ul>
</template>
实际应用示例
模态框组件
vue
<!-- Modal.vue -->
<script setup>
import { computed } from 'vue'
const props = defineProps({
modelValue: Boolean,
title: String,
showClose: {
type: Boolean,
default: true
}
})
const emit = defineEmits(['update:modelValue'])
const showModal = computed({
get: () => props.modelValue,
set: (value) => emit('update:modelValue', value)
})
const hasHeaderSlot = computed(() => !!slots.header)
const hasFooterSlot = computed(() => !!slots.footer)
</script>
<template>
<Teleport to="body">
<div v-if="showModal" class="modal-overlay" @click="showModal = false">
<div class="modal-content" @click.stop>
<div v-if="hasHeaderSlot" class="modal-header">
<slot name="header" />
<button v-if="showClose" @click="showModal = false" class="close-btn">✕</button>
</div>
<div v-else-if="title" class="modal-header">
<h3>{{ title }}</h3>
<button v-if="showClose" @click="showModal = false" class="close-btn">✕</button>
</div>
<div class="modal-body">
<slot />
</div>
<div v-if="hasFooterSlot" class="modal-footer">
<slot name="footer" />
</div>
</div>
</div>
</Teleport>
</template>
<style>
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.modal-content {
background: white;
border-radius: 8px;
max-width: 500px;
width: 90%;
max-height: 90vh;
overflow-y: auto;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem;
border-bottom: 1px solid #eee;
}
.modal-body {
padding: 1rem;
}
.modal-footer {
padding: 1rem;
border-top: 1px solid #eee;
text-align: right;
}
.close-btn {
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
}
</style>
vue
<!-- 使用模态框 -->
<script setup>
import { ref } from 'vue'
import Modal from './Modal.vue'
const showModal = ref(false)
</script>
<template>
<div>
<button @click="showModal = true">Open Modal</button>
<Modal v-model="showModal" title="Confirmation">
<p>Are you sure you want to proceed?</p>
<template #footer>
<button @click="showModal = false" class="btn-secondary">Cancel</button>
<button @click="confirmAction" class="btn-primary">Confirm</button>
</template>
</Modal>
</div>
</template>
插槽是Vue.js中非常强大的功能,它使得组件能够以灵活的方式组合和复用,是构建可维护和可扩展Vue应用的重要工具。