Skip to content
On this page

插槽 (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中,可以使用useSlotsuseAttrs来处理插槽和属性:

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应用的重要工具。