1. 学习 Vue3 插槽 slot:默认插槽、具名插槽、作用域插槽
Vue3 中的插槽(slot)是组件化开发中实现内容分发的核心机制。它允许父组件向子组件传递模板片段,极大地增强了组件的灵活性和复用性。
1.1 默认插槽
默认插槽是最基础的插槽形式。当子组件内部只有一个插槽时,父组件传入的任何内容都会渲染在该位置。
子组件 ChildComponent.vue:
vue
<template>
<div class="child">
<h3>子组件标题</h3>
<!-- 默认插槽出口 -->
<slot></slot>
</div>
</template>
父组件使用:
vue
<template>
<ChildComponent>
<!-- 传入默认插槽的内容 -->
<p>这是父组件传入的一段描述文本。</p>
<button>点击我</button>
</ChildComponent>
</template>
渲染结果中,<p> 和 <button> 会出现在子组件 <slot> 标签的位置。
1.2 具名插槽
当子组件需要多个内容分发出口时,就需要使用具名插槽。通过 name 属性为插槽命名,父组件使用 v-slot 指令(或其简写 #)来指定内容插入哪个插槽。
子组件 LayoutComponent.vue:
vue
<template>
<div class="layout">
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot> <!-- 默认插槽 -->
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>
</template>
父组件使用:
vue
<template>
<LayoutComponent>
<!-- 简写 #header 等同于 v-slot:header -->
<template #header>
<h1>网站标题</h1>
</template>
<!-- 未指定 name 的内容会进入默认插槽 -->
<p>这是页面的主要内容区域。</p>
<template v-slot:footer>
<p>© 2025 版权所有</p>
</template>
</LayoutComponent>
</template>
1.3 作用域插槽
作用域插槽是插槽功能的进阶,它允许子组件将数据"传递回"给父组件,让父组件可以决定如何渲染这些数据。这是实现"可复用渲染逻辑"组件(如列表、表格)的关键。
子组件 TodoList.vue:
vue
<script setup>
const todos = ref([
{ id: 1, text: '学习 Vue3', done: true },
{ id: 2, text: '掌握插槽', done: false },
{ id: 3, text: '完成项目', done: false }
])
</script>
<template>
<ul>
<li v-for="todo in todos" :key="todo.id">
<!-- 将 todo 对象作为 slot 的 props 传递出去 -->
<slot :todo="todo"></slot>
</li>
</ul>
</template>
父组件使用:
vue
<template>
<TodoList v-slot="slotProps">
<!-- slotProps 包含了子组件传递过来的数据 -->
<span :class="{ 'done': slotProps.todo.done }">
{{ slotProps.todo.text }}
</span>
</TodoList>
</template>
<!-- 或者使用解构语法,更简洁 -->
<TodoList v-slot="{ todo }">
<span :class="{ 'done': todo.done }">
{{ todo.text }}
<input type="checkbox" :checked="todo.done" />
</span>
</TodoList>
通过作用域插槽,父组件完全掌控了每个待办项的渲染样式和交互逻辑,而子组件只负责管理和提供数据。
2. 掌握 defineOptions 与组件 name 配置
在 Vue3 的 <script setup> 语法糖中,组件的选项(如 name, inheritAttrs)无法直接定义。defineOptions 宏就是为了解决这个问题而引入的。
2.1 使用 defineOptions 配置组件名
为组件显式命名是一个好习惯,便于调试(在 Vue DevTools 中显示清晰的组件名)和递归组件自引用。
vue
<script setup>
import { defineOptions } from 'vue'
defineOptions({
name: 'MyButton', // 配置组件名称
inheritAttrs: false // 其他选项...
})
</script>
<template>
<button>
<slot />
</button>
</template>
2.2 其他常用选项配置
除了 name,defineOptions 还可以配置其他组件选项。
vue
<script setup>
import { defineOptions } from 'vue'
defineOptions({
name: 'CustomForm',
inheritAttrs: false, // 禁止根元素自动继承 attribute
// 自定义组件继承的"风格"(实验性功能)
// compilerOptions: {
// isCustomElement: (tag) => tag.includes('-')
// }
})
</script>
注意: 像 data, computed, methods, lifecycle hooks 等 Composition API 相关的功能,应在 <script setup> 内直接使用响应式 API 和生命周期函数,而不是在 defineOptions 中定义。
3. 学习动态组件 & keep-alive
3.1 动态组件
动态组件让你可以根据条件或状态,动态地切换渲染不同的组件,使用 Vue 内置的 <component> 元素和 is 属性实现。
vue
<script setup>
import { ref, shallowRef } from 'vue'
import Home from './Home.vue'
import About from './About.vue'
import Contact from './Contact.vue'
const currentTab = ref('Home')
const tabs = {
Home,
About,
Contact
}
</script>
<template>
<div>
<button @click="currentTab = 'Home'">首页</button>
<button @click="currentTab = 'About'">关于</button>
<button @click="currentTab = 'Contact'">联系</button>
<!-- 动态组件:is 的值可以是组件选项对象或注册的组件名 -->
<component :is="tabs[currentTab]" />
</div>
</template>
3.2 使用 keep-alive 缓存组件状态
默认情况下,切换动态组件时,不活跃的组件实例会被销毁。使用 <KeepAlive> 内置组件可以缓存这些实例,保留其状态(如数据、滚动位置),避免重复渲染。
vue
<template>
<KeepAlive>
<!-- 只有当前活跃的 Home, About, Contact 组件会被缓存 -->
<component :is="tabs[currentTab]" />
</KeepAlive>
</template>
<KeepAlive> 的常用属性:
include:字符串或正则表达式,只有名称匹配的组件会被缓存。exclude:字符串或正则表达式,名称匹配的组件不会被缓存。max:数字,最多可缓存多少个组件实例。
vue
<!-- 只缓存 Home 和 About 组件 -->
<KeepAlive :include="['Home', 'About']">
<component :is="currentComponent" />
</KeepAlive>
<!-- 最多缓存 5 个组件实例,超出时使用 LRU 策略淘汰 -->
<KeepAlive :max="5">
<component :is="currentComponent" />
</KeepAlive>
生命周期钩子变化: 被 <KeepAlive> 缓存的组件会触发 onActivated(激活时)和 onDeactivated(失活时)这两个特有的生命周期钩子,可以用于执行恢复或清理任务。
4. 完成一个多组件嵌套小案例:简易标签页系统
我们将综合运用以上知识点,构建一个功能完整的简易标签页(Tabs)系统。
4.1 项目结构
src/
├── components/
│ ├── Tabs.vue // 外层容器,管理当前激活的标签
│ ├── TabList.vue // 渲染标签标题列表
│ └── TabPanel.vue // 渲染标签内容面板
└── App.vue // 根组件,使用 Tabs
4.2 核心组件实现
1. Tabs.vue (提供共享状态的上下文)
vue
<script setup>
import { provide, ref } from 'vue'
const activeTab = ref('tab1') // 当前激活的标签名
// 向子孙组件提供当前激活的标签名和切换函数
provide('activeTab', activeTab)
const setActiveTab = (tabName) => {
activeTab.value = tabName
}
provide('setActiveTab', setActiveTab)
</script>
<template>
<div class="tabs">
<!-- 默认插槽,用于放置 TabList 和 TabPanel -->
<slot />
</div>
</template>
2. TabList.vue (使用作用域插槽传递标题数据)
vue
<script setup>
import { inject } from 'vue'
const activeTab = inject('activeTab')
const setActiveTab = inject('setActiveTab')
</script>
<template>
<div class="tab-list" role="tablist">
<!-- 作用域插槽:将每个标题的点击事件和激活状态传递给父组件 -->
<slot :activeTab="activeTab" :setActiveTab="setActiveTab" />
</div>
</template>
3. TabPanel.vue (根据 name 决定是否显示)
vue
<script setup>
import { inject, defineOptions } from 'vue'
defineOptions({
name: 'TabPanel' // 使用 defineOptions 定义组件名
})
const props = defineProps({
name: { // 面板的唯一标识,与标签对应
type: String,
required: true
}
})
const activeTab = inject('activeTab')
</script>
<template>
<!-- 只有当前面板的 name 与激活的标签名一致时才渲染 -->
<div v-if="name === activeTab" class="tab-panel" role="tabpanel">
<slot />
</div>
</template>
4.3 在 App.vue 中组合使用
vue
<script setup>
import Tabs from './components/Tabs.vue'
import TabList from './components/TabList.vue'
import TabPanel from './components/TabPanel.vue'
</script>
<template>
<Tabs>
<!-- 使用 TabList 和作用域插槽定义标题 -->
<TabList v-slot="{ activeTab, setActiveTab }">
<button
@click="() => setActiveTab('info')"
:class="{ active: activeTab === 'info' }"
role="tab"
>
基本信息
</button>
<button
@click="() => setActiveTab('settings')"
:class="{ active: activeTab === 'settings' }"
role="tab"
>
设置
</button>
<button
@click="() => setActiveTab('advanced')"
:class="{ active: activeTab === 'advanced' }"
role="tab"
>
高级选项
</button>
</TabList>
<!-- 使用动态组件和 KeepAlive 缓存面板内容 -->
<KeepAlive>
<TabPanel name="info">
<h3>基本信息</h3>
<p>这里是用户的个人信息配置区域。</p>
</TabPanel>
<TabPanel name="settings">
<h3>设置</h3>
<p>系统偏好设置选项。</p>
</TabPanel>
<TabPanel name="advanced">
<h3>高级选项</h3>
<p>仅供高级用户使用的配置项。</p>
</TabPanel>
</KeepAlive>
</Tabs>
</template>
<style scoped>
.active {
font-weight: bold;
border-bottom: 2px solid blue;
}
</style>
4.4 案例总结
通过这个案例,你实践了:
- 组件通信 :使用
provide/inject在嵌套组件间共享状态。 - 插槽的全面应用 :默认插槽 (
Tabs)、作用域插槽 (TabList)。 - defineOptions :为
TabPanel组件显式命名。 - 动态组件与 KeepAlive :通过
TabPanel的v-if模拟动态组件切换,并用<KeepAlive>包裹以缓存面板状态。
这个可复用的 Tabs 组件体系体现了 Vue3 组合式 API 和插槽系统的强大与灵活。