← 返回主页

第8课: 插槽与动态组件

插槽基础

插槽(Slot)允许父组件向子组件传递模板内容。

默认插槽

<!-- Button.vue -->
<template>
  <button class="custom-button">
    <slot>默认按钮文本</slot>
  </button>
</template>
<!-- 使用 -->
<template>
  <div>
    <Button>点击我</Button>
    <Button>提交</Button>
    <Button /> <!-- 显示默认文本 -->
  </div>
</template>

具名插槽

使用多个插槽时,需要给插槽命名。

<!-- Card.vue -->
<template>
  <div class="card">
    <div class="card-header">
      <slot name="header">默认标题</slot>
    </div>
    <div class="card-body">
      <slot>默认内容</slot>
    </div>
    <div class="card-footer">
      <slot name="footer">默认页脚</slot>
    </div>
  </div>
</template>
<!-- 使用 -->
<template>
  <Card>
    <template #header>
      <h3>卡片标题</h3>
    </template>

    <p>这是卡片的主要内容</p>

    <template #footer>
      <button>确定</button>
      <button>取消</button>
    </template>
  </Card>
</template>
提示:#header 是 v-slot:header 的简写形式。

作用域插槽

子组件可以向插槽传递数据,父组件可以访问这些数据。

<!-- List.vue -->
<template>
  <ul>
    <li v-for="item in items" :key="item.id">
      <slot :item="item" :index="item.id">
        {{ item.name }}
      </slot>
    </li>
  </ul>
</template>

<script setup>
defineProps(['items'])
</script>
<!-- 使用 -->
<template>
  <List :items="users">
    <template #default="{ item, index }">
      <strong>{{ index }}.</strong> {{ item.name }} - {{ item.age }}岁
    </template>
  </List>
</template>

<script setup>
import { ref } from 'vue'
import List from './List.vue'

const users = ref([
  { id: 1, name: '张三', age: 25 },
  { id: 2, name: '李四', age: 30 }
])
</script>

动态插槽名

<template>
  <Card>
    <template #[dynamicSlotName]>
      动态插槽内容
    </template>
  </Card>
</template>

<script setup>
import { ref } from 'vue'

const dynamicSlotName = ref('header')
</script>

动态组件

使用component标签和is属性动态切换组件。

<template>
  <div>
    <button @click="currentTab = 'Home'">首页</button>
    <button @click="currentTab = 'About'">关于</button>
    <button @click="currentTab = 'Contact'">联系</button>

    <component :is="currentTab" />
  </div>
</template>

<script setup>
import { ref } from 'vue'
import Home from './Home.vue'
import About from './About.vue'
import Contact from './Contact.vue'

const currentTab = ref('Home')
</script>

keep-alive缓存组件

使用keep-alive包裹动态组件,保持组件状态。

<template>
  <div>
    <button @click="currentView = 'ComponentA'">A</button>
    <button @click="currentView = 'ComponentB'">B</button>

    <keep-alive>
      <component :is="currentView" />
    </keep-alive>
  </div>
</template>

<script setup>
import { ref } from 'vue'
import ComponentA from './ComponentA.vue'
import ComponentB from './ComponentB.vue'

const currentView = ref('ComponentA')
</script>

keep-alive属性

<template>
  <div>
    <!-- 只缓存name为a或b的组件 -->
    <keep-alive include="a,b">
      <component :is="view" />
    </keep-alive>

    <!-- 不缓存name为c的组件 -->
    <keep-alive exclude="c">
      <component :is="view" />
    </keep-alive>

    <!-- 最多缓存10个组件 -->
    <keep-alive :max="10">
      <component :is="view" />
    </keep-alive>
  </div>
</template>

keep-alive生命周期

<template>
  <div>缓存组件</div>
</template>

<script setup>
import { onActivated, onDeactivated } from 'vue'

// 组件被激活时调用
onActivated(() => {
  console.log('组件激活')
})

// 组件被停用时调用
onDeactivated(() => {
  console.log('组件停用')
})
</script>

异步组件

使用defineAsyncComponent定义异步组件,实现代码分割和懒加载。

<template>
  <div>
    <AsyncComponent />
  </div>
</template>

<script setup>
import { defineAsyncComponent } from 'vue'

// 简单用法
const AsyncComponent = defineAsyncComponent(() =>
  import('./components/HeavyComponent.vue')
)

// 高级用法
const AsyncComponentAdvanced = defineAsyncComponent({
  loader: () => import('./components/HeavyComponent.vue'),
  loadingComponent: LoadingComponent,
  errorComponent: ErrorComponent,
  delay: 200,
  timeout: 3000
})
</script>

Suspense组件

Suspense用于协调异步依赖的加载状态。

<template>
  <Suspense>
    <template #default>
      <AsyncComponent />
    </template>
    <template #fallback>
      <div>加载中...</div>
    </template>
  </Suspense>
</template>

<script setup>
import { defineAsyncComponent } from 'vue'

const AsyncComponent = defineAsyncComponent(() =>
  import('./AsyncComponent.vue')
)
</script>

实战示例:标签页组件

<!-- Tabs.vue -->
<template>
  <div class="tabs">
    <div class="tabs-header">
      <button
        v-for="tab in tabs"
        :key="tab"
        :class="{ active: activeTab === tab }"
        @click="activeTab = tab"
      >
        {{ tab }}
      </button>
    </div>
    <div class="tabs-content">
      <slot :name="activeTab"></slot>
    </div>
  </div>
</template>

<script setup>
import { ref } from 'vue'

defineProps(['tabs'])
const activeTab = ref('tab1')
</script>

<style scoped>
.tabs-header button {
  padding: 10px 20px;
  border: none;
  background: #f0f0f0;
  cursor: pointer;
}

.tabs-header button.active {
  background: #42b883;
  color: white;
}

.tabs-content {
  padding: 20px;
  border: 1px solid #ddd;
}
</style>
<!-- 使用 -->
<template>
  <Tabs :tabs="['tab1', 'tab2', 'tab3']">
    <template #tab1>
      <h3>标签页1内容</h3>
      <p>这是第一个标签页</p>
    </template>
    <template #tab2>
      <h3>标签页2内容</h3>
      <p>这是第二个标签页</p>
    </template>
    <template #tab3>
      <h3>标签页3内容</h3>
      <p>这是第三个标签页</p>
    </template>
  </Tabs>
</template>

实战示例:列表渲染器

<!-- DataList.vue -->
<template>
  <div class="data-list">
    <div v-if="loading">
      <slot name="loading">加载中...</slot>
    </div>
    <div v-else-if="error">
      <slot name="error" :error="error">加载失败</slot>
    </div>
    <div v-else-if="items.length === 0">
      <slot name="empty">暂无数据</slot>
    </div>
    <div v-else>
      <div v-for="item in items" :key="item.id">
        <slot :item="item">{{ item }}</slot>
      </div>
    </div>
  </div>
</template>

<script setup>
defineProps(['items', 'loading', 'error'])
</script>
<!-- 使用 -->
<template>
  <DataList :items="users" :loading="isLoading" :error="error">
    <template #default="{ item }">
      <div class="user-card">
        <h4>{{ item.name }}</h4>
        <p>{{ item.email }}</p>
      </div>
    </template>
    <template #loading>
      <div class="spinner">正在加载用户数据...</div>
    </template>
    <template #empty>
      <div>没有找到用户</div>
    </template>
    <template #error="{ error }">
      <div class="error">错误: {{ error.message }}</div>
    </template>
  </DataList>
</template>

练习

  1. 创建一个对话框组件,使用具名插槽实现标题、内容和按钮区域
  2. 创建一个表格组件,使用作用域插槽自定义单元格渲染
  3. 实现一个标签页组件,使用动态组件和keep-alive
  4. 创建一个列表组件,支持自定义加载、空状态和错误状态