周三:Vue3高级组件特性

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 其他常用选项配置

除了 namedefineOptions 还可以配置其他组件选项。

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 案例总结

通过这个案例,你实践了:

  1. 组件通信 :使用 provide/inject 在嵌套组件间共享状态。
  2. 插槽的全面应用 :默认插槽 (Tabs)、作用域插槽 (TabList)。
  3. defineOptions :为 TabPanel 组件显式命名。
  4. 动态组件与 KeepAlive :通过 TabPanelv-if 模拟动态组件切换,并用 <KeepAlive> 包裹以缓存面板状态。

这个可复用的 Tabs 组件体系体现了 Vue3 组合式 API 和插槽系统的强大与灵活。

相关推荐
英俊潇洒美少年1 小时前
前端 Jest 单元测试零基础实战:模板、提效、避坑、面试题(Vue 项目可用)
前端·vue.js·单元测试
happyprince1 小时前
10-Hugging Face Transformers 量化系统深度分析
java·前端·数据库
AskHarries1 小时前
如何使用 OpenClaw Skill
前端
AI周红伟1 小时前
Agent Skills生产级Skills 案例实操-周红伟
前端·chrome·react.js·langchain
Java编程爱好者1 小时前
Spring AI 1.0 实战:从原理到落地的完整指南
javascript
nickel3691 小时前
Qoder相关使用
java·开发语言·vue.js·spring boot
用户86284129549441 小时前
Flutter rxflare 响应式进阶:Map/List 精准字段更新(高性能实战)
前端·flutter
横木沉1 小时前
高并发场景下的前端缓存与降级策略
大数据·前端·缓存
三翼鸟数字化技术团队2 小时前
十万条数据怎么办?Vue3虚拟列表让你纵享丝滑
vue.js