内置组件是 Vue.js 核心库提供的特殊组件,它们以特定的方式扩展了 Vue 的功能。这些组件不需要注册,可以在任何组件的模板中直接使用。
动态组件,用于在不同组件之间动态切换
内容分发插槽,实现组件内容的自定义
过渡效果,为元素添加进入/离开动画
列表过渡,为多个元素添加动画效果
组件缓存,保留组件状态避免重新渲染
DOM传送,将组件渲染到DOM的其他位置
异步组件,处理异步组件的加载状态
<component> 组件用于动态地渲染不同的组件,通过 :is 属性指定要渲染的组件。
<template>
<div>
<h3>动态组件演示</h3>
<!-- 切换按钮 -->
<div class="btn-group mb-3" role="group">
<button @click="currentComponent = 'HomePage'"
class="btn btn-primary">
主页
</button>
<button @click="currentComponent = 'AboutPage'"
class="btn btn-success">
关于
</button>
<button @click="currentComponent = 'ContactPage'"
class="btn btn-info">
联系
</button>
</div>
<!-- 动态组件 -->
<component :is="currentComponent"></component>
<!-- 使用组件对象 -->
<div class="mt-4">
<h4>使用组件对象</h4>
<button @click="toggleDynamicComponent" class="btn btn-warning">
切换动态组件
</button>
<component :is="dynamicComponent"></component>
</div>
</div>
</template>
<script>
// 定义组件
const HomePage = {
template: `<div class="p-3 bg-primary text-white">
<h4>主页组件</h4>
<p>欢迎来到主页!</p>
</div>`
};
const AboutPage = {
template: `<div class="p-3 bg-success text-white">
<h4>关于组件</h4>
<p>这是关于页面。</p>
</div>`
};
const ContactPage = {
template: `<div class="p-3 bg-info text-white">
<h4>联系组件</h4>
<p>联系方式: example@email.com</p>
</div>`
};
// 动态组件对象
const DynamicComponentA = {
template: `<div class="p-3 bg-warning">
<h5>动态组件A</h5>
<p>这是动态加载的组件A</p>
</div>`
};
const DynamicComponentB = {
template: `<div class="p-3 bg-danger text-white">
<h5>动态组件B</h5>
<p>这是动态加载的组件B</p>
</div>`
};
export default {
components: {
HomePage,
AboutPage,
ContactPage,
DynamicComponentA,
DynamicComponentB
},
data() {
return {
currentComponent: 'HomePage',
dynamicComponent: DynamicComponentA,
isComponentA: true
};
},
methods: {
toggleDynamicComponent() {
this.isComponentA = !this.isComponentA;
this.dynamicComponent = this.isComponentA
? DynamicComponentA
: DynamicComponentB;
}
}
};
</script>
<template>
<div>
<h3>动态组件传递Props</h3>
<!-- 控制面板 -->
<div class="mb-3">
<button @click="currentTab = 'UserProfile'" class="btn btn-primary">
用户资料
</button>
<button @click="currentTab = 'UserSettings'" class="btn btn-success">
用户设置
</button>
<div class="mt-2">
<label>用户名:</label>
<input v-model="user.name" type="text" class="form-control">
</div>
</div>
<!-- 动态组件传递props -->
<component
:is="currentTab"
:user="user"
@update-user="updateUser">
</component>
</div>
</template>
<script>
const UserProfile = {
props: ['user'],
template: `<div class="p-3 border">
<h4>用户资料</h4>
<p>用户名: {{ user.name }}</p>
<p>邮箱: {{ user.email }}</p>
<p>年龄: {{ user.age }}</p>
</div>`
};
const UserSettings = {
props: ['user'],
emits: ['update-user'],
template: `<div class="p-3 border">
<h4>用户设置</h4>
<div class="mb-2">
<label>修改用户名:</label>
<input v-model="localUser.name" type="text" class="form-control">
</div>
<button @click="saveChanges" class="btn btn-primary">
保存更改
</button>
</div>`,
data() {
return {
localUser: { ...this.user }
};
},
methods: {
saveChanges() {
this.$emit('update-user', this.localUser);
}
}
};
export default {
components: {
UserProfile,
UserSettings
},
data() {
return {
currentTab: 'UserProfile',
user: {
name: '张三',
email: 'zhangsan@example.com',
age: 25
}
};
},
methods: {
updateUser(newUser) {
this.user = { ...newUser };
alert('用户信息已更新!');
}
}
};
</script>
<template>
<div>
<h3>动态组件事件监听</h3>
<!-- 组件选择 -->
<div class="btn-group mb-3">
<button @click="activeComponent = 'Counter'" class="btn btn-primary">
计数器
</button>
<button @click="activeComponent = 'TodoList'" class="btn btn-success">
待办事项
</button>
</div>
<!-- 消息显示 -->
<div v-if="message" class="alert alert-info">
{{ message }}
</div>
<!-- 动态组件监听事件 -->
<component
:is="activeComponent"
@increment="handleIncrement"
@add-todo="handleAddTodo"
@clear-todos="handleClearTodos">
</component>
</div>
</template>
<script>
const Counter = {
emits: ['increment'],
template: `<div class="p-3 border">
<h4>计数器组件</h4>
<button @click="increment" class="btn btn-primary">
点击增加计数
</button>
</div>`,
methods: {
increment() {
this.$emit('increment', { time: new Date().toLocaleTimeString() });
}
}
};
const TodoList = {
emits: ['add-todo', 'clear-todos'],
data() {
return {
newTodo: ''
};
},
template: `<div class="p-3 border">
<h4>待办事项组件</h4>
<div class="mb-2">
<input v-model="newTodo" type="text"
placeholder="输入待办事项" class="form-control">
</div>
<button @click="addTodo" class="btn btn-success me-2">
添加待办
</button>
<button @click="clearTodos" class="btn btn-danger">
清空所有
</button>
</div>`,
methods: {
addTodo() {
if (this.newTodo.trim()) {
this.$emit('add-todo', this.newTodo);
this.newTodo = '';
}
},
clearTodos() {
this.$emit('clear-todos');
}
}
};
export default {
components: {
Counter,
TodoList
},
data() {
return {
activeComponent: 'Counter',
message: ''
};
},
methods: {
handleIncrement(data) {
this.message = `计数器在 ${data.time} 被点击了`;
setTimeout(() => {
this.message = '';
}, 2000);
},
handleAddTodo(todo) {
this.message = `添加了待办事项: "${todo}"`;
setTimeout(() => {
this.message = '';
}, 2000);
},
handleClearTodos() {
this.message = '所有待办事项已清空';
setTimeout(() => {
this.message = '';
}, 2000);
}
}
};
</script>
欢迎来到主页!
这是动态加载的组件A
| 特性 | 描述 | 示例 |
|---|---|---|
| 动态渲染 | 根据条件渲染不同的组件 | <component :is="currentView"></component> |
| 传递Props | 可以向动态组件传递props | <component :is="comp" :data="data"> |
| 监听事件 | 可以监听动态组件发出的事件 | <component :is="comp" @event="handler"> |
| 组件对象 | :is可以接受组件选项对象 | :is="{ template: '<div>Hi</div>' }" |
| 组件名称 | :is可以接受已注册组件的名称 | :is="'MyComponent'" |
| HTML元素 | :is也可以接受HTML元素名称 | :is="'div'" (Vue 3) |
<component> 组件非常适合实现标签页(Tabs)、动态表单、多步骤向导、条件渲染不同UI组件等场景。它可以显著减少模板中的条件渲染逻辑。
<slot> 是 Vue 的内容分发 API,允许父组件向子组件传递模板内容。
<!-- 子组件: Card.vue -->
<template>
<div class="card">
<div class="card-header">
<slot name="header">
<!-- 默认内容 -->
<h4 class="mb-0">默认标题</h4>
</slot>
</div>
<div class="card-body">
<!-- 默认插槽 -->
<slot>
<p>这是默认内容。</p>
</slot>
</div>
<div class="card-footer">
<slot name="footer">
<!-- 默认内容 -->
<small class="text-muted">默认页脚</small>
</slot>
</div>
</div>
</template>
<!-- 父组件使用 -->
<template>
<div>
<h3>基本插槽示例</h3>
<!-- 使用默认插槽 -->
<Card>
<p>这是通过插槽传递的内容。</p>
<p>父组件可以完全控制这里的内容。</p>
</Card>
<!-- 使用具名插槽 -->
<Card>
<template v-slot:header>
<h4 class="mb-0 text-primary">自定义标题</h4>
</template>
<!-- 默认插槽内容 -->
<p>这是卡片的主体内容。</p>
<ul>
<li>列表项1</li>
<li>列表项2</li>
<li>列表项3</li>
</ul>
<template v-slot:footer>
<small class="text-success">自定义页脚内容</small>
</template>
</Card>
<!-- 使用默认值 -->
<Card />
</div>
</template>
<script>
import Card from './Card.vue';
export default {
components: { Card }
};
</script>
<style>
.card {
border: 1px solid #dee2e6;
border-radius: 0.375rem;
margin-bottom: 1rem;
}
.card-header {
background-color: #f8f9fa;
padding: 0.75rem 1.25rem;
border-bottom: 1px solid #dee2e6;
}
.card-body {
padding: 1.25rem;
}
.card-footer {
background-color: #f8f9fa;
padding: 0.75rem 1.25rem;
border-top: 1px solid #dee2e6;
}
</style>
<!-- 子组件: Layout.vue -->
<template>
<div class="layout">
<header class="layout-header">
<slot name="header"></slot>
</header>
<div class="layout-container">
<aside class="layout-sidebar">
<slot name="sidebar"></slot>
</aside>
<main class="layout-main">
<slot name="main"></slot>
</main>
</div>
<footer class="layout-footer">
<slot name="footer"></slot>
</footer>
</div>
</template>
<style scoped>
.layout {
display: flex;
flex-direction: column;
min-height: 400px;
border: 1px solid #dee2e6;
border-radius: 0.375rem;
}
.layout-header {
background-color: #3498db;
color: white;
padding: 1rem;
}
.layout-container {
display: flex;
flex: 1;
}
.layout-sidebar {
width: 200px;
background-color: #f8f9fa;
padding: 1rem;
border-right: 1px solid #dee2e6;
}
.layout-main {
flex: 1;
padding: 1rem;
}
.layout-footer {
background-color: #2c3e50;
color: white;
padding: 1rem;
}
</style>
<!-- 父组件使用 -->
<template>
<div>
<h3>具名插槽示例</h3>
<Layout>
<template v-slot:header>
<h2>网站标题</h2>
<p>欢迎来到我的网站</p>
</template>
<template v-slot:sidebar>
<h4>导航菜单</h4>
<ul>
<li><a href="#">首页</a></li>
<li><a href="#">关于</a></li>
<li><a href="#">服务</a></li>
<li><a href="#">联系</a></li>
</ul>
</template>
<template v-slot:main>
<h3>主要内容区域</h3>
<p>这里是网站的主要内容。</p>
<p>具名插槽允许我们将内容分发到指定的位置。</p>
</template>
<template v-slot:footer>
<p>© 2023 我的网站. 保留所有权利.</p>
</template>
</Layout>
<!-- 简写语法 -->
<Layout>
<template #header>
<h2>简写语法示例</h2>
</template>
<template #sidebar>
<p>使用#代替v-slot:</p>
</template>
<template #main>
<p>这是主要内容。</p>
</template>
<template #footer>
<p>页脚内容</p>
</template>
</Layout>
</div>
</template>
<script>
import Layout from './Layout.vue';
export default {
components: { Layout }
};
</script>
<!-- 子组件: DataList.vue -->
<template>
<div class="data-list">
<h3>数据列表</h3>
<!-- 作用域插槽 -->
<ul>
<li v-for="(item, index) in items" :key="item.id">
<!-- 向父组件暴露数据 -->
<slot :item="item" :index="index">
<!-- 默认渲染 -->
{{ index + 1 }}. {{ item.name }}
</slot>
</li>
</ul>
<!-- 多个作用域插槽示例 -->
<div class="user-table">
<slot name="table-header" :count="items.length"></slot>
<table class="table">
<thead>
<slot name="thead">
<tr>
<th>ID</th>
<th>姓名</th>
<th>年龄</th>
<th>操作</th>
</tr>
</slot>
</thead>
<tbody>
<tr v-for="user in items" :key="user.id">
<!-- 作用域插槽 -->
<slot name="row" :user="user">
<td>{{ user.id }}</td>
<td>{{ user.name }}</td>
<td>{{ user.age }}</td>
<td>--</td>
</slot>
</tr>
</tbody>
</table>
<slot name="table-footer" :total-age="totalAge"></slot>
</div>
</div>
</template>
<script>
export default {
props: {
items: {
type: Array,
default: () => []
}
},
computed: {
totalAge() {
return this.items.reduce((sum, user) => sum + user.age, 0);
}
}
};
</script>
<!-- 父组件使用 -->
<template>
<div>
<h3>作用域插槽示例</h3>
<DataList :items="users">
<!-- 使用作用域插槽 -->
<template v-slot:default="slotProps">
<span :class="{ 'text-success': slotProps.item.age >= 18 }">
{{ slotProps.index + 1 }}. {{ slotProps.item.name }}
({{ slotProps.item.age }}岁)
</span>
</template>
</DataList>
<DataList :items="users">
<!-- 解构语法 -->
<template v-slot:default="{ item, index }">
<span :class="{ 'text-danger': item.age < 18 }">
[#{{ index + 1 }}] {{ item.name.toUpperCase() }}
</span>
</template>
</DataList>
<DataList :items="users">
<!-- 具名作用域插槽 -->
<template #table-header="{ count }">
<h4>用户列表 (共{{ count }}人)</h4>
</template>
<template #thead>
<tr>
<th>ID</th>
<th>姓名</th>
<th>年龄</th>
<th>状态</th>
<th>操作</th>
</tr>
</template>
<template #row="{ user }">
<td>{{ user.id }}</td>
<td><strong>{{ user.name }}</strong></td>
<td>{{ user.age }}</td>
<td>
<span :class="user.age >= 18 ? 'text-success' : 'text-warning'">
{{ user.age >= 18 ? '成年人' : '未成年人' }}
</span>
</td>
<td>
<button @click="editUser(user)" class="btn btn-sm btn-primary">
编辑
</button>
</td>
</template>
<template #table-footer="{ totalAge }">
<div class="alert alert-info mt-2">
总年龄: {{ totalAge }}岁,平均年龄: {{ (totalAge / users.length).toFixed(1) }}岁
</div>
</template>
</DataList>
</div>
</template>
<script>
import DataList from './DataList.vue';
export default {
components: { DataList },
data() {
return {
users: [
{ id: 1, name: '张三', age: 25 },
{ id: 2, name: '李四', age: 17 },
{ id: 3, name: '王五', age: 30 },
{ id: 4, name: '赵六', age: 22 },
{ id: 5, name: '孙七', age: 16 }
]
};
},
methods: {
editUser(user) {
alert(`编辑用户: ${user.name}`);
}
}
};
</script>
插槽是Vue中强大的内容分发机制。以下展示不同类型的插槽:
这是通过插槽传递的内容。
父组件可以完全控制这里的内容。
这是卡片的主体内容。
| 插槽类型 | 描述 | 语法 | 使用场景 |
|---|---|---|---|
| 默认插槽 | 未命名的插槽,接收所有未指定插槽的内容 | <slot></slot> |
简单的内容分发 |
| 具名插槽 | 有名称的插槽,用于将内容分发到特定位置 | <slot name="header"></slot> |
布局组件、复杂UI结构 |
| 作用域插槽 | 允许子组件向父组件传递数据 | <slot :item="item"></slot> |
数据列表、可定制渲染 |
| 动态插槽名 | 动态指定插槽名称 | <slot :name="slotName"></slot> |
动态布局、条件渲染 |
| 后备内容 | 插槽的默认内容 | <slot>默认内容</slot> |
提供默认UI,增强组件可用性 |
<transition> 组件为元素的进入/离开提供过渡动画效果。
<template>
<div>
<h3>基本过渡示例</h3>
<button @click="show = !show" class="btn btn-primary">
切换显示/隐藏
</button>
<!-- 基本过渡 -->
<transition>
<div v-if="show" class="fade-box">
这是一个会淡入淡出的元素
</div>
</transition>
<!-- 自定义类名 -->
<transition name="slide">
<div v-if="show" class="slide-box">
这是一个会滑入滑出的元素
</div>
</transition>
<!-- 初始渲染过渡 -->
<transition appear>
<div class="appear-box">
页面加载时会执行过渡动画
</div>
</transition>
</div>
</template>
<script>
export default {
data() {
return {
show: true
};
}
};
</script>
<style>
.fade-box {
padding: 20px;
background-color: #3498db;
color: white;
margin: 10px 0;
border-radius: 5px;
}
.slide-box {
padding: 20px;
background-color: #2ecc71;
color: white;
margin: 10px 0;
border-radius: 5px;
}
.appear-box {
padding: 20px;
background-color: #9b59b6;
color: white;
margin: 10px 0;
border-radius: 5px;
}
/* 默认过渡类名 */
.v-enter-active,
.v-leave-active {
transition: opacity 0.5s ease;
}
.v-enter-from,
.v-leave-to {
opacity: 0;
}
/* 自定义过渡类名 */
.slide-enter-active,
.slide-leave-active {
transition: all 0.5s ease;
}
.slide-enter-from {
opacity: 0;
transform: translateX(-100%);
}
.slide-leave-to {
opacity: 0;
transform: translateX(100%);
}
/* 初始渲染动画 */
.v-enter-active.appear-box {
animation: bounce-in 0.5s;
}
@keyframes bounce-in {
0% {
transform: scale(0);
}
50% {
transform: scale(1.2);
}
100% {
transform: scale(1);
}
}
</style>
<template>
<div>
<h3>CSS过渡示例</h3>
<div class="mb-3">
<button @click="showFade = !showFade" class="btn btn-primary me-2">
淡入淡出
</button>
<button @click="showSlide = !showSlide" class="btn btn-success me-2">
滑动
</button>
<button @click="showBounce = !showBounce" class="btn btn-info">
弹跳
</button>
</div>
<!-- 淡入淡出过渡 -->
<transition name="fade">
<div v-if="showFade" class="demo-box fade-box">
淡入淡出效果
</div>
</transition>
<!-- 滑动过渡 -->
<transition name="slide-fade">
<div v-if="showSlide" class="demo-box slide-box">
滑动淡入效果
</div>
</transition>
<!-- 弹跳过渡 -->
<transition name="bounce">
<div v-if="showBounce" class="demo-box bounce-box">
弹跳效果
</div>
</transition>
<!-- 多个元素的过渡 -->
<transition name="list">
<ul v-if="showList" key="list" class="list-group mt-3">
<li class="list-group-item">项目1</li>
<li class="list-group-item">项目2</li>
<li class="list-group-item">项目3</li>
</ul>
<div v-else key="message" class="alert alert-warning mt-3">
列表已被隐藏
</div>
</transition>
<button @click="showList = !showList" class="btn btn-warning mt-2">
切换列表/消息
</button>
</div>
</template>
<script>
export default {
data() {
return {
showFade: true,
showSlide: true,
showBounce: true,
showList: true
};
}
};
</script>
<style>
.demo-box {
padding: 20px;
color: white;
margin: 10px 0;
border-radius: 5px;
text-align: center;
}
.fade-box { background-color: #3498db; }
.slide-box { background-color: #2ecc71; }
.bounce-box { background-color: #e74c3c; }
/* 淡入淡出过渡 */
.fade-enter-active, .fade-leave-active {
transition: opacity 0.5s;
}
.fade-enter-from, .fade-leave-to {
opacity: 0;
}
/* 滑动淡入过渡 */
.slide-fade-enter-active {
transition: all 0.3s ease-out;
}
.slide-fade-leave-active {
transition: all 0.3s cubic-bezier(1, 0.5, 0.8, 1);
}
.slide-fade-enter-from {
transform: translateX(20px);
opacity: 0;
}
.slide-fade-leave-to {
transform: translateX(-20px);
opacity: 0;
}
/* 弹跳过渡 */
.bounce-enter-active {
animation: bounce-in 0.5s;
}
.bounce-leave-active {
animation: bounce-in 0.5s reverse;
}
@keyframes bounce-in {
0% {
transform: scale(0);
}
50% {
transform: scale(1.2);
}
100% {
transform: scale(1);
}
}
/* 列表过渡 */
.list-enter-active,
.list-leave-active {
transition: all 0.5s ease;
}
.list-enter-from {
opacity: 0;
transform: translateY(30px);
}
.list-leave-to {
opacity: 0;
transform: translateY(-30px);
}
</style>
<template>
<div>
<h3>JavaScript动画示例</h3>
<button @click="show = !show" class="btn btn-primary mb-3">
切换显示 (使用JavaScript动画)
</button>
<!-- 使用JavaScript钩子 -->
<transition
@before-enter="beforeEnter"
@enter="enter"
@after-enter="afterEnter"
@enter-cancelled="enterCancelled"
@before-leave="beforeLeave"
@leave="leave"
@after-leave="afterLeave"
@leave-cancelled="leaveCancelled"
:css="false">
<div v-if="show" class="js-animation-box">
JavaScript动画
</div>
</transition>
<!-- 结合第三方动画库 -->
<button @click="showGsap = !showGsap" class="btn btn-success mt-3">
切换GSAP动画
</button>
<transition
@enter="gsapEnter"
@leave="gsapLeave"
:css="false">
<div v-if="showGsap" class="gsap-box">
GSAP动画效果
</div>
</transition>
</div>
</template>
<script>
// 假设已经引入了GSAP库
// import gsap from 'gsap';
export default {
data() {
return {
show: true,
showGsap: true
};
},
methods: {
// JavaScript动画钩子
beforeEnter(el) {
console.log('beforeEnter');
el.style.opacity = 0;
el.style.transform = 'scale(0)';
},
enter(el, done) {
console.log('enter');
// 使用requestAnimationFrame实现动画
let start = null;
const duration = 600; // 600ms
function animate(timestamp) {
if (!start) start = timestamp;
const progress = timestamp - start;
const percentage = Math.min(progress / duration, 1);
// 缓动函数
const easeOutCubic = t => 1 - Math.pow(1 - t, 3);
const eased = easeOutCubic(percentage);
el.style.opacity = eased;
el.style.transform = `scale(${eased})`;
if (progress < duration) {
requestAnimationFrame(animate);
} else {
done(); // 动画完成
}
}
requestAnimationFrame(animate);
},
afterEnter(el) {
console.log('afterEnter');
el.style.opacity = '';
el.style.transform = '';
},
enterCancelled(el) {
console.log('enterCancelled');
},
beforeLeave(el) {
console.log('beforeLeave');
},
leave(el, done) {
console.log('leave');
let start = null;
const duration = 600;
function animate(timestamp) {
if (!start) start = timestamp;
const progress = timestamp - start;
const percentage = Math.min(progress / duration, 1);
// 缓动函数
const easeInCubic = t => t * t * t;
const eased = easeInCubic(percentage);
el.style.opacity = 1 - eased;
el.style.transform = `scale(${1 - eased})`;
if (progress < duration) {
requestAnimationFrame(animate);
} else {
done(); // 动画完成
}
}
requestAnimationFrame(animate);
},
afterLeave(el) {
console.log('afterLeave');
},
leaveCancelled(el) {
console.log('leaveCancelled');
},
// GSAP动画
gsapEnter(el, done) {
// GSAP动画
// gsap.from(el, {
// duration: 0.6,
// opacity: 0,
// scale: 0,
// ease: "back.out(1.7)",
// onComplete: done
// });
// 使用CSS动画模拟GSAP效果
el.style.animation = 'gsapEnter 0.6s ease-out';
setTimeout(done, 600);
},
gsapLeave(el, done) {
// gsap.to(el, {
// duration: 0.6,
// opacity: 0,
// scale: 0,
// ease: "back.in(1.7)",
// onComplete: done
// });
el.style.animation = 'gsapLeave 0.6s ease-in';
setTimeout(done, 600);
}
}
};
</script>
<style>
.js-animation-box {
padding: 20px;
background-color: #3498db;
color: white;
border-radius: 5px;
text-align: center;
}
.gsap-box {
padding: 20px;
background-color: #e74c3c;
color: white;
border-radius: 5px;
text-align: center;
margin-top: 10px;
}
@keyframes gsapEnter {
0% {
opacity: 0;
transform: scale(0) rotate(-180deg);
}
70% {
transform: scale(1.2) rotate(10deg);
}
100% {
opacity: 1;
transform: scale(1) rotate(0deg);
}
}
@keyframes gsapLeave {
0% {
opacity: 1;
transform: scale(1) rotate(0deg);
}
30% {
transform: scale(1.2) rotate(-10deg);
}
100% {
opacity: 0;
transform: scale(0) rotate(180deg);
}
}
</style>
<template>
<div>
<h3>过渡模式示例</h3>
<div class="mb-3">
<button @click="toggleView" class="btn btn-primary">
切换视图 (当前: {{ currentView }})
</button>
</div>
<!-- 默认模式 (同时进行) -->
<h4>默认模式 (同时进行)</h4>
<transition name="fade">
<div v-if="currentView === 'A'" key="A" class="view-box view-a">
视图 A
</div>
<div v-else key="B" class="view-box view-b">
视图 B
</div>
</transition>
<!-- out-in 模式 -->
<h4 class="mt-4">out-in 模式</h4>
<transition name="slide" mode="out-in">
<div v-if="currentView === 'A'" key="A" class="view-box view-a">
视图 A
</div>
<div v-else key="B" class="view-box view-b">
视图 B
</div>
</transition>
<!-- in-out 模式 -->
<h4 class="mt-4">in-out 模式</h4>
<transition name="slide" mode="in-out">
<div v-if="currentView === 'A'" key="A" class="view-box view-a">
视图 A
</div>
<div v-else key="B" class="view-box view-b">
视图 B
</div>
</transition>
<!-- 动态过渡 -->
<h4 class="mt-4">动态过渡</h4>
<transition :name="transitionName">
<div v-if="currentView === 'A'" key="A" class="view-box view-a">
视图 A
</div>
<div v-else key="B" class="view-box view-b">
视图 B
</div>
</transition>
<div class="mt-2">
<button @click="transitionName = 'fade'" class="btn btn-sm btn-primary">
使用淡入淡出
</button>
<button @click="transitionName = 'slide'" class="btn btn-sm btn-success">
使用滑动
</button>
<button @click="transitionName = 'bounce'" class="btn btn-sm btn-info">
使用弹跳
</button>
</div>
</div>
</template>
<script>
export default {
data() {
return {
currentView: 'A',
transitionName: 'fade'
};
},
methods: {
toggleView() {
this.currentView = this.currentView === 'A' ? 'B' : 'A';
}
}
};
</script>
<style>
.view-box {
padding: 30px;
color: white;
border-radius: 5px;
text-align: center;
height: 100px;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.5rem;
}
.view-a {
background-color: #3498db;
}
.view-b {
background-color: #2ecc71;
}
/* 淡入淡出过渡 */
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.5s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
/* 滑动过渡 */
.slide-enter-active,
.slide-leave-active {
transition: all 0.5s ease;
}
.slide-enter-from {
opacity: 0;
transform: translateX(100%);
}
.slide-leave-to {
opacity: 0;
transform: translateX(-100%);
}
/* in-out模式时,进入元素立即显示 */
.slide-enter-to {
position: absolute;
}
/* 弹跳过渡 */
.bounce-enter-active {
animation: bounce-in 0.5s;
}
.bounce-leave-active {
animation: bounce-in 0.5s reverse;
}
@keyframes bounce-in {
0% {
transform: scale(0);
}
50% {
transform: scale(1.2);
}
100% {
transform: scale(1);
}
}
</style>
默认模式(同时进行)、out-in(先出后进)和 in-out(先进后出)。对于视图切换等场景,建议使用 out-in 模式,这样可以在旧元素完全离开后再开始新元素的进入动画,避免视觉上的冲突。
<transition-group> 组件用于为 v-for 列表中的多个元素添加过渡效果。
<template>
<div>
<h3>基本列表过渡</h3>
<div class="mb-3">
<button @click="addItem" class="btn btn-primary me-2">
添加项目
</button>
<button @click="removeItem" class="btn btn-danger me-2">
移除项目
</button>
<button @click="shuffleItems" class="btn btn-warning">
随机排序
</button>
</div>
<!-- 列表过渡 -->
<transition-group name="list" tag="ul" class="list-group">
<li v-for="item in items"
:key="item.id"
class="list-group-item d-flex justify-content-between align-items-center">
{{ item.name }}
<button @click="removeSpecificItem(item.id)" class="btn btn-sm btn-danger">
×
</button>
</li>
</transition-group>
<p class="mt-2">项目数量: {{ items.length }}</p>
</div>
</template>
<script>
export default {
data() {
return {
items: [
{ id: 1, name: '项目 1' },
{ id: 2, name: '项目 2' },
{ id: 3, name: '项目 3' },
{ id: 4, name: '项目 4' },
{ id: 5, name: '项目 5' }
],
nextId: 6
};
},
methods: {
addItem() {
this.items.push({
id: this.nextId++,
name: `项目 ${this.nextId - 1}`
});
},
removeItem() {
if (this.items.length > 0) {
this.items.pop();
}
},
removeSpecificItem(id) {
this.items = this.items.filter(item => item.id !== id);
},
shuffleItems() {
// 随机排序
this.items = this.items.sort(() => Math.random() - 0.5);
}
}
};
</script>
<style>
/* 进入和离开动画 */
.list-enter-active,
.list-leave-active {
transition: all 0.5s ease;
}
.list-enter-from {
opacity: 0;
transform: translateY(30px);
}
.list-leave-to {
opacity: 0;
transform: translateX(-100%);
}
/* 确保离开的元素不占用布局空间 */
.list-leave-active {
position: absolute;
width: 100%;
}
/* 移动过渡 */
.list-move {
transition: transform 0.5s ease;
}
</style>
<template>
<div>
<h3>移动过渡示例</h3>
<div class="mb-3">
<button @click="shuffle" class="btn btn-primary me-2">
随机排序
</button>
<button @click="add" class="btn btn-success me-2">
添加数字
</button>
<button @click="reset" class="btn btn-warning">
重置
</button>
</div>
<transition-group name="flip-list" tag="div" class="number-list">
<div v-for="item in items"
:key="item"
class="number-item">
{{ item }}
</div>
</transition-group>
<h4 class="mt-4">可拖拽列表</h4>
<transition-group name="drag-list" tag="div" class="draggable-list">
<div v-for="item in draggableItems"
:key="item.id"
class="draggable-item"
draggable="true"
@dragstart="dragStart(item.id)"
@dragover.prevent
@drop="drop(item.id)">
{{ item.name }}
<span class="drag-handle">☰</span>
</div>
</transition-group>
</div>
</template>
<script>
export default {
data() {
return {
items: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
draggableItems: [
{ id: 1, name: '任务 1' },
{ id: 2, name: '任务 2' },
{ id: 3, name: '任务 3' },
{ id: 4, name: '任务 4' },
{ id: 5, name: '任务 5' }
],
draggedItemId: null
};
},
methods: {
shuffle() {
this.items = this.items.sort(() => Math.random() - 0.5);
},
add() {
const max = Math.max(...this.items);
this.items.push(max + 1);
},
reset() {
this.items = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
},
dragStart(id) {
this.draggedItemId = id;
},
drop(targetId) {
if (this.draggedItemId === null) return;
const draggedIndex = this.draggableItems.findIndex(
item => item.id === this.draggedItemId
);
const targetIndex = this.draggableItems.findIndex(
item => item.id === targetId
);
if (draggedIndex !== -1 && targetIndex !== -1) {
// 移动元素
const item = this.draggableItems[draggedIndex];
this.draggableItems.splice(draggedIndex, 1);
this.draggableItems.splice(targetIndex, 0, item);
}
this.draggedItemId = null;
}
}
};
</script>
<style>
.number-list {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.number-item {
width: 50px;
height: 50px;
background-color: #3498db;
color: white;
display: flex;
align-items: center;
justify-content: center;
border-radius: 5px;
font-size: 1.2rem;
font-weight: bold;
}
/* FLIP动画 */
.flip-list-move {
transition: transform 0.5s ease;
}
.draggable-list {
display: flex;
flex-direction: column;
gap: 5px;
}
.draggable-item {
padding: 10px 15px;
background-color: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 5px;
cursor: move;
display: flex;
justify-content: space-between;
align-items: center;
transition: all 0.3s ease;
}
.draggable-item:hover {
background-color: #e9ecef;
transform: translateX(5px);
}
.drag-handle {
color: #6c757d;
cursor: grab;
}
.drag-list-move {
transition: transform 0.3s ease;
}
</style>
<template>
<div>
<h3>交错动画示例</h3>
<div class="mb-3">
<button @click="addMultiple" class="btn btn-primary me-2">
添加5个项目
</button>
<button @click="removeMultiple" class="btn btn-danger me-2">
移除5个项目
</button>
<button @click="toggleList" class="btn btn-warning">
{{ showList ? '隐藏' : '显示' }}列表
</button>
</div>
<!-- 使用JavaScript钩子实现交错动画 -->
<transition-group
tag="div"
class="stagger-list"
@before-enter="beforeEnter"
@enter="enter"
@leave="leave"
:css="false">
<div v-for="(item, index) in items"
v-show="showList"
:key="item.id"
:data-index="index"
class="stagger-item">
{{ item.name }}
</div>
</transition-group>
<!-- 使用CSS animation-delay实现交错动画 -->
<transition-group
v-if="showList"
name="stagger"
tag="div"
class="stagger-list-css">
<div v-for="(item, index) in cssItems"
:key="item.id"
:data-index="index"
class="stagger-css-item">
{{ item.name }}
</div>
</transition-group>
</div>
</template>
<script>
export default {
data() {
return {
items: [
{ id: 1, name: '项目 1' },
{ id: 2, name: '项目 2' },
{ id: 3, name: '项目 3' },
{ id: 4, name: '项目 4' },
{ id: 5, name: '项目 5' }
],
cssItems: [
{ id: 1, name: 'CSS 项目 1' },
{ id: 2, name: 'CSS 项目 2' },
{ id: 3, name: 'CSS 项目 3' },
{ id: 4, name: 'CSS 项目 4' },
{ id: 5, name: 'CSS 项目 5' }
],
nextId: 6,
nextCssId: 6,
showList: true
};
},
methods: {
addMultiple() {
for (let i = 0; i < 5; i++) {
this.items.push({
id: this.nextId++,
name: `项目 ${this.nextId - 1}`
});
this.cssItems.push({
id: this.nextCssId++,
name: `CSS 项目 ${this.nextCssId - 1}`
});
}
},
removeMultiple() {
for (let i = 0; i < 5; i++) {
if (this.items.length > 0) this.items.pop();
if (this.cssItems.length > 0) this.cssItems.pop();
}
},
toggleList() {
this.showList = !this.showList;
},
// JavaScript交错动画
beforeEnter(el) {
el.style.opacity = 0;
el.style.transform = 'translateY(20px)';
},
enter(el, done) {
const delay = el.dataset.index * 100; // 根据索引设置延迟
setTimeout(() => {
// 使用Web Animations API
el.animate([
{
opacity: 0,
transform: 'translateY(20px)'
},
{
opacity: 1,
transform: 'translateY(0)'
}
], {
duration: 300,
easing: 'ease-out',
fill: 'forwards'
}).onfinish = done;
}, delay);
},
leave(el, done) {
const delay = el.dataset.index * 100;
setTimeout(() => {
el.animate([
{
opacity: 1,
transform: 'translateY(0)'
},
{
opacity: 0,
transform: 'translateY(20px)'
}
], {
duration: 300,
easing: 'ease-in',
fill: 'forwards'
}).onfinish = done;
}, delay);
}
}
};
</script>
<style>
.stagger-list {
display: flex;
flex-direction: column;
gap: 10px;
margin-bottom: 20px;
}
.stagger-item {
padding: 15px;
background-color: #3498db;
color: white;
border-radius: 5px;
text-align: center;
}
.stagger-list-css {
display: flex;
flex-direction: column;
gap: 10px;
}
.stagger-css-item {
padding: 15px;
background-color: #2ecc71;
color: white;
border-radius: 5px;
text-align: center;
}
/* CSS交错动画 */
.stagger-enter-active {
animation: stagger-enter 0.5s ease-out;
animation-fill-mode: both;
}
.stagger-leave-active {
animation: stagger-leave 0.5s ease-in;
animation-fill-mode: both;
}
.stagger-move {
transition: transform 0.5s ease;
}
/* 为每个元素设置不同的动画延迟 */
.stagger-enter-active:nth-child(1) { animation-delay: 0.1s; }
.stagger-enter-active:nth-child(2) { animation-delay: 0.2s; }
.stagger-enter-active:nth-child(3) { animation-delay: 0.3s; }
.stagger-enter-active:nth-child(4) { animation-delay: 0.4s; }
.stagger-enter-active:nth-child(5) { animation-delay: 0.5s; }
.stagger-leave-active:nth-child(1) { animation-delay: 0.1s; }
.stagger-leave-active:nth-child(2) { animation-delay: 0.2s; }
.stagger-leave-active:nth-child(3) { animation-delay: 0.3s; }
.stagger-leave-active:nth-child(4) { animation-delay: 0.4s; }
.stagger-leave-active:nth-child(5) { animation-delay: 0.5s; }
@keyframes stagger-enter {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes stagger-leave {
from {
opacity: 1;
transform: translateY(0);
}
to {
opacity: 0;
transform: translateY(20px);
}
}
</style>
| 特性 | <transition> | <transition-group> |
|---|---|---|
| 用途 | 单个元素/组件的过渡 | 多个元素(列表)的过渡 |
| 渲染元素 | 不渲染为DOM元素 | 渲染为真实的DOM元素(默认是span) |
| tag属性 | 不支持 | 支持,用于指定渲染的标签 |
| key属性 | 不需要 | 必须为每个子元素设置key |
| 移动过渡 | 不支持 | 支持,使用v-move类 |
| FLIP动画 | 不支持 | 内置支持,优化性能 |
<transition-group> 时,确保为每个元素设置唯一的 key,这样 Vue 可以更高效地跟踪元素的变化。对于非常长的列表,考虑使用虚拟滚动等技术来优化性能。
<keep-alive> 组件用于缓存不活动的组件实例,避免重复渲染,保持组件状态。
<template>
<div>
<h3>组件缓存示例</h3>
<div class="mb-3">
<button @click="currentTab = 'TabA'" class="btn btn-primary me-2">
标签页 A
</button>
<button @click="currentTab = 'TabB'" class="btn btn-success me-2">
标签页 B
</button>
<button @click="currentTab = 'TabC'" class="btn btn-info">
标签页 C
</button>
</div>
<div class="mb-3">
<div class="form-check form-check-inline">
<input v-model="useKeepAlive" type="checkbox" id="keepAliveCheck" class="form-check-input">
<label for="keepAliveCheck" class="form-check-label">
使用 keep-alive 缓存组件
</label>
</div>
</div>
<!-- 不使用 keep-alive -->
<div v-if="!useKeepAlive" class="border p-3">
<h4>不使用 keep-alive</h4>
<component :is="currentTab"></component>
</div>
<!-- 使用 keep-alive -->
<div v-else class="border p-3">
<h4>使用 keep-alive</h4>
<keep-alive>
<component :is="currentTab"></component>
</keep-alive>
</div>
<div class="mt-3">
<p>当前标签页: {{ currentTab }}</p>
<p>组件状态: {{ useKeepAlive ? '已缓存' : '未缓存' }}</p>
</div>
</div>
</template>
<script>
// 定义标签页组件
const TabA = {
name: 'TabA',
template: `<div class="tab-content">
<h5>标签页 A</h5>
<p>这是一个计数器和输入框示例。</p>
<div class="mb-2">
<p>计数器: {{ count }}</p>
<button @click="count++" class="btn btn-sm btn-primary">增加</button>
</div>
<div>
<label>输入内容:</label>
<input v-model="text" type="text" class="form-control">
<p>输入的内容: {{ text }}</p>
</div>
</div>`,
data() {
return {
count: 0,
text: ''
};
},
created() {
console.log('TabA created');
},
mounted() {
console.log('TabA mounted');
},
activated() {
console.log('TabA activated');
},
deactivated() {
console.log('TabA deactivated');
},
destroyed() {
console.log('TabA destroyed');
}
};
const TabB = {
name: 'TabB',
template: `<div class="tab-content">
<h5>标签页 B</h5>
<p>这是一个待办事项列表示例。</p>
<div class="mb-2">
<input v-model="newTodo" type="text"
placeholder="输入待办事项" class="form-control">
<button @click="addTodo" class="btn btn-sm btn-success mt-2">添加</button>
</div>
<ul>
<li v-for="(todo, index) in todos" :key="index">
{{ todo }}
<button @click="removeTodo(index)" class="btn btn-sm btn-danger">×</button>
</li>
</ul>
</div>`,
data() {
return {
newTodo: '',
todos: ['学习Vue', '练习组件', '完成项目']
};
},
methods: {
addTodo() {
if (this.newTodo.trim()) {
this.todos.push(this.newTodo);
this.newTodo = '';
}
},
removeTodo(index) {
this.todos.splice(index, 1);
}
},
created() {
console.log('TabB created');
},
mounted() {
console.log('TabB mounted');
},
activated() {
console.log('TabB activated');
},
deactivated() {
console.log('TabB deactivated');
},
destroyed() {
console.log('TabB destroyed');
}
};
const TabC = {
name: 'TabC',
template: `<div class="tab-content">
<h5>标签页 C</h5>
<p>这是一个表单示例。</p>
<form @submit.prevent="submitForm">
<div class="mb-2">
<label>用户名:</label>
<input v-model="form.username" type="text" class="form-control" required>
</div>
<div class="mb-2">
<label>邮箱:</label>
<input v-model="form.email" type="email" class="form-control" required>
</div>
<div class="mb-2">
<label>消息:</label>
<textarea v-model="form.message" class="form-control" rows="3"></textarea>
</div>
<button type="submit" class="btn btn-primary">提交</button>
</form>
<div v-if="submitted" class="alert alert-success mt-2">
表单已提交!
</div>
</div>`,
data() {
return {
form: {
username: '',
email: '',
message: ''
},
submitted: false
};
},
methods: {
submitForm() {
console.log('表单提交:', this.form);
this.submitted = true;
}
},
created() {
console.log('TabC created');
},
mounted() {
console.log('TabC mounted');
},
activated() {
console.log('TabC activated');
},
deactivated() {
console.log('TabC deactivated');
},
destroyed() {
console.log('TabC destroyed');
}
};
export default {
components: {
TabA,
TabB,
TabC
},
data() {
return {
currentTab: 'TabA',
useKeepAlive: true
};
}
};
</script>
<style>
.tab-content {
padding: 15px;
background-color: #f8f9fa;
border-radius: 5px;
min-height: 200px;
}
</style>
<template>
<div>
<h3>keep-alive 生命周期</h3>
<div class="mb-3">
<button @click="toggleComponent" class="btn btn-primary">
{{ showComponent ? '隐藏' : '显示' }}组件
</button>
</div>
<div class="lifecycle-log">
<h5>生命周期事件日志</h5>
<ul>
<li v-for="(event, index) in events" :key="index">
{{ event }}
</li>
</ul>
</div>
<keep-alive>
<LifecycleDemo v-if="showComponent" @log="addEvent" />
</keep-alive>
</div>
</template>
<script>
const LifecycleDemo = {
name: 'LifecycleDemo',
template: `<div class="demo-component">
<h5>生命周期演示组件</h5>
<p>计数器: {{ count }}</p>
<button @click="count++" class="btn btn-sm btn-primary">增加</button>
</div>`,
data() {
return {
count: 0
};
},
beforeCreate() {
this.$emit('log', 'beforeCreate - 实例初始化之前');
},
created() {
this.$emit('log', 'created - 实例创建完成');
},
beforeMount() {
this.$emit('log', 'beforeMount - 挂载之前');
},
mounted() {
this.$emit('log', 'mounted - 挂载完成');
},
beforeUpdate() {
this.$emit('log', 'beforeUpdate - 更新之前');
},
updated() {
this.$emit('log', 'updated - 更新完成');
},
activated() {
this.$emit('log', 'activated - 被 keep-alive 缓存的组件激活时');
},
deactivated() {
this.$emit('log', 'deactivated - 被 keep-alive 缓存的组件停用时');
},
beforeUnmount() {
this.$emit('log', 'beforeUnmount - 卸载之前');
},
unmounted() {
this.$emit('log', 'unmounted - 卸载完成');
}
};
export default {
components: {
LifecycleDemo
},
data() {
return {
showComponent: true,
events: []
};
},
methods: {
toggleComponent() {
this.showComponent = !this.showComponent;
},
addEvent(event) {
this.events.unshift(`[${new Date().toLocaleTimeString()}] ${event}`);
// 保持日志数量不超过10条
if (this.events.length > 10) {
this.events.pop();
}
}
}
};
</script>
<style>
.lifecycle-log {
max-height: 300px;
overflow-y: auto;
background-color: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 5px;
padding: 15px;
margin-bottom: 15px;
}
.lifecycle-log ul {
list-style-type: none;
padding-left: 0;
margin-bottom: 0;
}
.lifecycle-log li {
padding: 5px 0;
border-bottom: 1px dashed #dee2e6;
font-family: 'Courier New', monospace;
font-size: 0.9rem;
}
.lifecycle-log li:last-child {
border-bottom: none;
}
.demo-component {
padding: 15px;
background-color: #e8f4ff;
border: 1px solid #3498db;
border-radius: 5px;
}
</style>
<template>
<div>
<h3>条件缓存与缓存控制</h3>
<div class="mb-3">
<button @click="currentView = 'Home'" class="btn btn-primary me-2">
首页
</button>
<button @click="currentView = 'Profile'" class="btn btn-success me-2">
个人资料
</button>
<button @click="currentView = 'Settings'" class="btn btn-info me-2">
设置
</button>
<button @click="currentView = 'Help'" class="btn btn-warning">
帮助
</button>
</div>
<!-- 条件缓存 -->
<keep-alive :include="cachedComponents" :exclude="excludedComponents" :max="maxCache">
<component :is="currentView"></component>
</keep-alive>
<div class="mt-3">
<h5>缓存控制</h5>
<div class="row">
<div class="col-md-6">
<h6>包含的组件 (include):</h6>
<div class="form-check" v-for="component in allComponents" :key="component">
<input v-model="cachedComponents"
:value="component"
type="checkbox"
:id="'include-' + component"
class="form-check-input">
<label :for="'include-' + component" class="form-check-label">
{{ component }}
</label>
</div>
</div>
<div class="col-md-6">
<h6>排除的组件 (exclude):</h6>
<div class="form-check" v-for="component in allComponents" :key="component">
<input v-model="excludedComponents"
:value="component"
type="checkbox"
:id="'exclude-' + component"
class="form-check-input">
<label :for="'exclude-' + component" class="form-check-label">
{{ component }}
</label>
</div>
</div>
</div>
<div class="mt-3">
<label>最大缓存数 (max):</label>
<input v-model.number="maxCache" type="range" min="1" max="5" class="form-range">
<span>{{ maxCache }}</span>
</div>
</div>
<div class="mt-3">
<h5>缓存状态</h5>
<p>当前组件: {{ currentView }}</p>
<p>已缓存组件: {{ cachedComponents.join(', ') || '无' }}</p>
<p>排除组件: {{ excludedComponents.join(', ') || '无' }}</p>
<p>缓存数量限制: {{ maxCache }}</p>
</div>
</div>
</template>
<script>
// 定义组件
const Home = {
name: 'Home',
template: `<div class="component-box">
<h5>首页</h5>
<p>访问次数: {{ count }}</p>
<button @click="count++" class="btn btn-sm btn-primary">访问</button>
</div>`,
data() {
return {
count: 0
};
},
activated() {
console.log('Home activated');
},
deactivated() {
console.log('Home deactivated');
}
};
const Profile = {
name: 'Profile',
template: `<div class="component-box">
<h5>个人资料</h5>
<div class="mb-2">
<label>用户名:</label>
<input v-model="username" type="text" class="form-control">
</div>
<p>当前用户名: {{ username }}</p>
</div>`,
data() {
return {
username: '用户' + Math.floor(Math.random() * 1000)
};
},
activated() {
console.log('Profile activated');
},
deactivated() {
console.log('Profile deactivated');
}
};
const Settings = {
name: 'Settings',
template: `<div class="component-box">
<h5>设置</h5>
<p>设置项: {{ settings }}</p>
<button @click="addSetting" class="btn btn-sm btn-success">添加设置项</button>
</div>`,
data() {
return {
settings: ['主题', '语言']
};
},
methods: {
addSetting() {
this.settings.push('设置项' + (this.settings.length + 1));
}
},
activated() {
console.log('Settings activated');
},
deactivated() {
console.log('Settings deactivated');
}
};
const Help = {
name: 'Help',
template: `<div class="component-box">
<h5>帮助</h5>
<p>帮助内容加载时间: {{ loadTime }}</p>
</div>`,
data() {
return {
loadTime: new Date().toLocaleTimeString()
};
},
activated() {
console.log('Help activated');
},
deactivated() {
console.log('Help deactivated');
}
};
export default {
components: {
Home,
Profile,
Settings,
Help
},
data() {
return {
currentView: 'Home',
allComponents: ['Home', 'Profile', 'Settings', 'Help'],
cachedComponents: ['Home', 'Profile'], // 默认缓存Home和Profile
excludedComponents: [], // 默认不排除任何组件
maxCache: 3 // 默认最大缓存3个组件
};
}
};
</script>
<style>
.component-box {
padding: 20px;
background-color: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 5px;
min-height: 150px;
}
</style>
使用 <keep-alive> 可以保持组件状态:
使用 keep-alive 时:
activated 和 deactivated 生命周期钩子created、mounted、unmounted 等钩子不使用 keep-alive 时:
| 属性 | 描述 | 类型 | 默认值 | 示例 |
|---|---|---|---|---|
include |
只有名称匹配的组件会被缓存 | String, RegExp, Array | - | include="Home,Profile" |
exclude |
名称匹配的组件不会被缓存 | String, RegExp, Array | - | exclude="Settings" |
max |
最多可以缓存的组件实例数量 | Number | - | :max="5" |
<keep-alive> 缓存的组件不会触发 unmounted 生命周期钩子,而是触发 deactivated 钩子。当组件再次被显示时,会触发 activated 钩子而不是 mounted 钩子。
<teleport> 组件允许我们将子组件渲染到DOM中的其他位置,而不受父组件DOM结构的限制。
下面的按钮会将模态框传送到上面的红色区域。
<template>
<div>
<h3>基本 Teleport 示例</h3>
<!-- 传送目标 -->
<div id="modal-container" class="teleport-target">
<h5>模态框容器</h5>
</div>
<!-- 应用主体 -->
<div class="app-content">
<h4>应用内容</h4>
<p>点击按钮将模态框传送到上面的容器中。</p>
<button @click="showModal = true" class="btn btn-primary">
显示模态框
</button>
<!-- 使用 teleport 传送模态框 -->
<teleport to="#modal-container">
<Modal v-if="showModal" @close="showModal = false">
<template #header>
<h5>这是一个模态框</h5>
</template>
<template #body>
<p>这个模态框被传送到 #modal-container 元素中。</p>
<p>即使 Modal 组件在 App 组件中定义,它也会被渲染到指定的目标位置。</p>
</template>
</Modal>
</teleport>
</div>
<!-- 传送到 body -->
<button @click="showToast = true" class="btn btn-success mt-3">
显示 Toast 通知
</button>
<teleport to="body">
<Toast v-if="showToast" @close="showToast = false">
这是一个 Toast 通知,被传送到 body 元素。
</Toast>
</teleport>
</div>
</template>
<script>
// 模态框组件
const Modal = {
props: ['show'],
emits: ['close'],
template: `<div class="modal-overlay" @click.self="$emit('close')">
<div class="modal-content">
<div class="modal-header">
<slot name="header"></slot>
<button @click="$emit('close')" class="close-btn">×</button>
</div>
<div class="modal-body">
<slot name="body"></slot>
</div>
<div class="modal-footer">
<button @click="$emit('close')" class="btn btn-primary">关闭</button>
</div>
</div>
</div>`
};
// Toast 组件
const Toast = {
emits: ['close'],
template: `<div class="toast">
<div class="toast-content">
<slot></slot>
<button @click="$emit('close')" class="toast-close">×</button>
</div>
</div>`,
mounted() {
// 3秒后自动关闭
setTimeout(() => {
this.$emit('close');
}, 3000);
}
};
export default {
components: {
Modal,
Toast
},
data() {
return {
showModal: false,
showToast: false
};
}
};
</script>
<style>
.teleport-target {
position: relative;
padding: 20px;
background-color: #e74c3c;
color: white;
border-radius: 5px;
margin-bottom: 20px;
min-height: 100px;
}
.app-content {
padding: 20px;
background-color: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 5px;
}
/* 模态框样式 */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal-content {
background-color: white;
border-radius: 5px;
width: 500px;
max-width: 90%;
max-height: 80%;
overflow: auto;
}
.modal-header {
padding: 15px 20px;
border-bottom: 1px solid #dee2e6;
display: flex;
justify-content: space-between;
align-items: center;
}
.modal-body {
padding: 20px;
}
.modal-footer {
padding: 15px 20px;
border-top: 1px solid #dee2e6;
text-align: right;
}
.close-btn {
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
color: #6c757d;
}
.close-btn:hover {
color: #343a40;
}
/* Toast 样式 */
.toast {
position: fixed;
bottom: 20px;
right: 20px;
z-index: 1050;
}
.toast-content {
background-color: #333;
color: white;
padding: 15px 20px;
border-radius: 5px;
display: flex;
align-items: center;
justify-content: space-between;
min-width: 300px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.toast-close {
background: none;
border: none;
color: white;
font-size: 1.2rem;
cursor: pointer;
margin-left: 15px;
}
</style>
<template>
<div>
<h3>条件传送示例</h3>
<div class="mb-3">
<div class="form-check form-check-inline">
<input v-model="useTeleport" type="checkbox" id="useTeleport" class="form-check-input">
<label for="useTeleport" class="form-check-label">使用 teleport</label>
</div>
<div class="form-check form-check-inline">
<input v-model="disableTeleport" type="checkbox" id="disableTeleport" class="form-check-input">
<label for="disableTeleport" class="form-check-label">禁用 teleport</label>
</div>
</div>
<div class="mb-3">
<label>选择传送目标:</label>
<select v-model="teleportTo" class="form-select">
<option value="#target-a">目标 A</option>
<option value="#target-b">目标 B</option>
<option value="body">body 元素</option>
</select>
</div>
<!-- 传送目标 -->
<div class="row">
<div class="col-md-6">
<div id="target-a" class="teleport-target target-a">
<h5>目标 A</h5>
</div>
</div>
<div class="col-md-6">
<div id="target-b" class="teleport-target target-b">
<h5>目标 B</h5>
</div>
</div>
</div>
<!-- 应用内容 -->
<div class="app-content mt-3">
<h4>应用内容区域</h4>
<button @click="showContent = !showContent" class="btn btn-primary">
{{ showContent ? '隐藏' : '显示' }}内容
</button>
<!-- 条件传送 -->
<template v-if="showContent">
<template v-if="useTeleport">
<teleport :to="teleportTo" :disabled="disableTeleport">
<TeleportedContent />
</teleport>
</template>
<template v-else>
<TeleportedContent />
</template>
</template>
</div>
<div class="mt-3">
<p>当前配置:</p>
<ul>
<li>使用 teleport: {{ useTeleport ? '是' : '否' }}</li>
<li>传送目标: {{ teleportTo }}</li>
<li>禁用 teleport: {{ disableTeleport ? '是' : '否' }}</li>
<li>内容显示: {{ showContent ? '是' : '否' }}</li>
</ul>
</div>
</div>
</template>
<script>
const TeleportedContent = {
name: 'TeleportedContent',
template: `<div class="content-box">
<h5>可传送的内容</h5>
<p>计数器: {{ count }}</p>
<button @click="count++" class="btn btn-sm btn-primary">增加</button>
<p class="mt-2">这个组件可以被传送到不同的位置。</p>
</div>`,
data() {
return {
count: 0
};
},
created() {
console.log('TeleportedContent created');
},
mounted() {
console.log('TeleportedContent mounted');
},
unmounted() {
console.log('TeleportedContent unmounted');
}
};
export default {
components: {
TeleportedContent
},
data() {
return {
useTeleport: true,
disableTeleport: false,
teleportTo: '#target-a',
showContent: true
};
}
};
</script>
<style>
.target-a {
background-color: #e74c3c;
}
.target-b {
background-color: #3498db;
}
.content-box {
padding: 20px;
background-color: #2ecc71;
color: white;
border-radius: 5px;
margin-top: 10px;
}
</style>
<template>
<div>
<h3>多个 Teleport 示例</h3>
<!-- 多个传送目标 -->
<div class="teleport-targets">
<div id="header-target" class="target-header">
<h5>头部区域</h5>
</div>
<div id="sidebar-target" class="target-sidebar">
<h5>侧边栏区域</h5>
</div>
<div id="footer-target" class="target-footer">
<h5>页脚区域</h5>
</div>
</div>
<!-- 应用内容 -->
<div class="app-main">
<h4>应用主内容</h4>
<p>这个组件管理多个可传送到不同位置的内容。</p>
<div class="mb-3">
<button @click="toggleHeader" class="btn btn-primary me-2">
{{ showHeader ? '隐藏' : '显示' }}头部内容
</button>
<button @click="toggleSidebar" class="btn btn-success me-2">
{{ showSidebar ? '隐藏' : '显示' }}侧边栏内容
</button>
<button @click="toggleFooter" class="btn btn-info">
{{ showFooter ? '隐藏' : '显示' }}页脚内容
</button>
</div>
<!-- 传送到头部 -->
<teleport to="#header-target">
<HeaderContent v-if="showHeader" />
</teleport>
<!-- 传送到侧边栏 -->
<teleport to="#sidebar-target">
<SidebarContent v-if="showSidebar" />
</teleport>
<!-- 传送到页脚 -->
<teleport to="#footer-target">
<FooterContent v-if="showFooter" />
</teleport>
<!-- 多个 teleport 到同一个目标 -->
<div class="mt-4">
<h5>多个内容传送到同一个目标</h5>
<button @click="addNotification" class="btn btn-warning">
添加通知
</button>
<!-- 通知容器 -->
<div id="notification-container"></div>
<!-- 多个 teleport 到同一个目标 -->
<teleport v-for="(notification, index) in notifications"
:key="notification.id"
to="#notification-container">
<Notification :message="notification.message"
@close="removeNotification(notification.id)" />
</teleport>
</div>
</div>
</div>
</template>
<script>
// 头部内容组件
const HeaderContent = {
template: `<div class="header-content">
<span>网站标题 | 用户: 张三 | </span>
<button @click="$emit('logout')" class="btn btn-sm btn-outline-light">
退出登录
</button>
</div>`
};
// 侧边栏内容组件
const SidebarContent = {
template: `<div class="sidebar-content">
<ul>
<li><a href="#">首页</a></li>
<li><a href="#">个人资料</a></li>
<li><a href="#">设置</a></li>
<li><a href="#">帮助</a></li>
</ul>
</div>`
};
// 页脚内容组件
const FooterContent = {
template: `<div class="footer-content">
<p>© 2023 我的网站. 保留所有权利.</p>
<p><a href="#">隐私政策</a> | <a href="#">使用条款</a></p>
</div>`
};
// 通知组件
const Notification = {
props: ['message'],
emits: ['close'],
template: `<div class="notification">
<span>{{ message }}</span>
<button @click="$emit('close')" class="close-btn">×</button>
</div>`,
mounted() {
// 5秒后自动关闭
setTimeout(() => {
this.$emit('close');
}, 5000);
}
};
export default {
components: {
HeaderContent,
SidebarContent,
FooterContent,
Notification
},
data() {
return {
showHeader: true,
showSidebar: true,
showFooter: true,
notifications: [],
nextNotificationId: 1
};
},
methods: {
toggleHeader() {
this.showHeader = !this.showHeader;
},
toggleSidebar() {
this.showSidebar = !this.showSidebar;
},
toggleFooter() {
this.showFooter = !this.showFooter;
},
addNotification() {
this.notifications.push({
id: this.nextNotificationId++,
message: `通知 #${this.nextNotificationId - 1}: ${new Date().toLocaleTimeString()}`
});
},
removeNotification(id) {
this.notifications = this.notifications.filter(n => n.id !== id);
}
}
};
</script>
<style>
.teleport-targets {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 10px;
margin-bottom: 20px;
}
.target-header {
background-color: #2c3e50;
color: white;
padding: 15px;
border-radius: 5px;
}
.target-sidebar {
background-color: #34495e;
color: white;
padding: 15px;
border-radius: 5px;
}
.target-footer {
background-color: #7f8c8d;
color: white;
padding: 15px;
border-radius: 5px;
}
.app-main {
padding: 20px;
background-color: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 5px;
}
.header-content {
display: flex;
align-items: center;
gap: 10px;
}
.sidebar-content ul {
list-style-type: none;
padding-left: 0;
margin-bottom: 0;
}
.sidebar-content li {
padding: 5px 0;
}
.sidebar-content a {
color: white;
text-decoration: none;
}
.sidebar-content a:hover {
text-decoration: underline;
}
.footer-content {
text-align: center;
}
.footer-content a {
color: white;
}
#notification-container {
position: fixed;
top: 20px;
right: 20px;
z-index: 1000;
display: flex;
flex-direction: column;
gap: 10px;
}
.notification {
background-color: #333;
color: white;
padding: 10px 15px;
border-radius: 5px;
display: flex;
align-items: center;
justify-content: space-between;
min-width: 250px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
}
.close-btn {
background: none;
border: none;
color: white;
font-size: 1.2rem;
cursor: pointer;
margin-left: 10px;
}
</style>
| 场景 | 描述 | 示例 |
|---|---|---|
| 模态框/对话框 | 避免模态框受到父组件CSS的影响 | <teleport to="body"><Modal></teleport> |
| 通知/Toast | 全局通知显示在页面固定位置 | <teleport to="#notifications"> |
| 工具提示 | 确保提示框显示在正确层级 | <teleport to="#tooltip-root"> |
| 加载遮罩 | 全屏加载动画不受布局限制 | <teleport to="body"><Loading></teleport> |
| 上下文菜单 | 确保菜单显示在正确位置 | <teleport to="#context-menu"> |
<teleport> 时,组件在逻辑上仍然是父组件的子组件(可以访问父组件的props、provide/inject等),但在DOM结构上被移动到了目标位置。这使得你可以保持组件逻辑的完整性,同时解决一些CSS层级和布局问题。
<suspense> 组件用于处理异步组件的加载状态,提供更好的用户体验。
<template>
<div>
<h3>基本 Suspense 示例</h3>
<div class="mb-3">
<button @click="loadComponent" class="btn btn-primary" :disabled="isLoading">
{{ isLoading ? '加载中...' : '加载异步组件' }}
</button>
<button @click="reset" class="btn btn-secondary">重置</button>
</div>
<!-- 使用 Suspense -->
<Suspense v-if="showComponent">
<!-- 默认插槽:要渲染的异步组件 -->
<template #default>
<AsyncComponent />
</template>
<!-- fallback 插槽:加载状态 -->
<template #fallback>
<div class="loading-state">
<div class="spinner"></div>
<p>正在加载组件,请稍候...</p>
</div>
</template>
</Suspense>
<!-- 模拟的异步组件 -->
<Suspense>
<template #default>
<MockAsyncComponent />
</template>
<template #fallback>
<div class="loading-state">
<p>模拟组件加载中...</p>
</div>
</template>
</Suspense>
</div>
</template>
<script>
// 异步组件定义
const AsyncComponent = {
name: 'AsyncComponent',
// 异步 setup 函数
async setup() {
console.log('AsyncComponent setup开始');
// 模拟异步数据加载
const data = await new Promise(resolve => {
setTimeout(() => {
resolve({
title: '异步加载的组件',
content: '这个组件是异步加载的。',
items: ['项目1', '项目2', '项目3', '项目4', '项目5']
});
}, 2000);
});
console.log('AsyncComponent setup完成');
// 返回响应式数据
return {
...data,
count: 0
};
},
template: `<div class="async-component">
<h4>{{ title }}</h4>
<p>{{ content }}</p>
<p>计数器: {{ count }}</p>
<button @click="count++" class="btn btn-sm btn-primary">增加</button>
<ul class="mt-3">
<li v-for="(item, index) in items" :key="index">
{{ item }}
</li>
</ul>
</div>`
};
// 模拟异步组件
const MockAsyncComponent = {
name: 'MockAsyncComponent',
async setup() {
// 等待1秒
await new Promise(resolve => setTimeout(resolve, 1000));
return {
message: '模拟异步组件已加载完成!',
loadedAt: new Date().toLocaleTimeString()
};
},
template: `<div class="mock-async">
<h5>模拟异步组件</h5>
<p>{{ message }}</p>
<p>加载时间: {{ loadedAt }}</p>
</div>`
};
export default {
components: {
AsyncComponent,
MockAsyncComponent
},
data() {
return {
showComponent: false,
isLoading: false
};
},
methods: {
async loadComponent() {
this.isLoading = true;
this.showComponent = false;
// 模拟网络延迟
await new Promise(resolve => setTimeout(resolve, 500));
this.showComponent = true;
this.isLoading = false;
},
reset() {
this.showComponent = false;
this.isLoading = false;
}
}
};
</script>
<style>
.loading-state {
padding: 40px;
text-align: center;
background-color: #f8f9fa;
border: 1px dashed #dee2e6;
border-radius: 5px;
margin: 20px 0;
}
.spinner {
width: 40px;
height: 40px;
border: 4px solid #f3f3f3;
border-top: 4px solid #3498db;
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto 15px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.async-component {
padding: 20px;
background-color: #e8f4ff;
border: 1px solid #3498db;
border-radius: 5px;
margin: 20px 0;
}
.mock-async {
padding: 20px;
background-color: #e8f6ef;
border: 1px solid #2ecc71;
border-radius: 5px;
margin: 20px 0;
}
</style>
<template>
<div>
<h3>多个异步组件示例</h3>
<div class="mb-3">
<button @click="loadAll" class="btn btn-primary me-2" :disabled="isLoading">
加载所有组件
</button>
<button @click="loadOneByOne" class="btn btn-success me-2" :disabled="isLoading">
逐个加载组件
</button>
<button @click="reset" class="btn btn-secondary">重置</button>
</div>
<!-- 嵌套的 Suspense -->
<Suspense v-if="showComponents">
<template #default>
<div class="multiple-components">
<Suspense>
<template #default>
<UserProfile />
</template>
<template #fallback>
<div class="component-loading">
<p>加载用户资料...</p>
</div>
</template>
</Suspense>
<Suspense>
<template #default>
<UserPosts />
</template>
<template #fallback>
<div class="component-loading">
<p>加载用户帖子...</p>
</div>
</template>
</Suspense>
<Suspense>
<template #default>
<UserStats />
</template>
<template #fallback>
<div class="component-loading">
<p>加载用户统计...</p>
</div>
</template>
</Suspense>
</div>
</template>
<template #fallback>
<div class="main-loading">
<div class="spinner"></div>
<p>正在加载页面内容...</p>
</div>
</template>
</Suspense>
<!-- 显示加载状态 -->
<div class="mt-3">
<h5>加载状态</h5>
<ul>
<li v-for="(status, component) in loadingStatus" :key="component">
{{ component }}: {{ status ? '加载中' : '已加载' }}
</li>
</ul>
</div>
</div>
</template>
<script>
// 模拟异步数据获取
function fetchData(endpoint, delay = 1000) {
return new Promise(resolve => {
setTimeout(() => {
const mockData = {
'/api/user': {
name: '张三',
email: 'zhangsan@example.com',
joinDate: '2023-01-15'
},
'/api/posts': [
{ id: 1, title: '第一篇帖子', date: '2023-10-01' },
{ id: 2, title: '第二篇帖子', date: '2023-10-05' },
{ id: 3, title: '第三篇帖子', date: '2023-10-10' }
],
'/api/stats': {
posts: 15,
followers: 120,
following: 85,
likes: 450
}
};
resolve(mockData[endpoint] || {});
}, delay);
});
}
// 用户资料组件
const UserProfile = {
name: 'UserProfile',
async setup() {
console.log('UserProfile 开始加载');
const data = await fetchData('/api/user', 1500);
console.log('UserProfile 加载完成');
return { user: data };
},
template: `<div class="component-box profile-box">
<h5>用户资料</h5>
<p>姓名: {{ user.name }}</p>
<p>邮箱: {{ user.email }}</p>
<p>加入日期: {{ user.joinDate }}</p>
</div>`
};
// 用户帖子组件
const UserPosts = {
name: 'UserPosts',
async setup() {
console.log('UserPosts 开始加载');
const data = await fetchData('/api/posts', 2000);
console.log('UserPosts 加载完成');
return { posts: data };
},
template: `<div class="component-box posts-box">
<h5>用户帖子</h5>
<ul>
<li v-for="post in posts" :key="post.id">
{{ post.title }} ({{ post.date }})
</li>
</ul>
</div>`
};
// 用户统计组件
const UserStats = {
name: 'UserStats',
async setup() {
console.log('UserStats 开始加载');
const data = await fetchData('/api/stats', 1000);
console.log('UserStats 加载完成');
return { stats: data };
},
template: `<div class="component-box stats-box">
<h5>用户统计</h5>
<div class="stats-grid">
<div class="stat-item">
<div class="stat-value">{{ stats.posts }}</div>
<div class="stat-label">帖子</div>
</div>
<div class="stat-item">
<div class="stat-value">{{ stats.followers }}</div>
<div class="stat-label">粉丝</div>
</div>
<div class="stat-item">
<div class="stat-value">{{ stats.following }}</div>
<div class="stat-label">关注</div>
</div>
<div class="stat-item">
<div class="stat-value">{{ stats.likes }}</div>
<div class="stat-label">点赞</div>
</div>
</div>
</div>`
};
export default {
components: {
UserProfile,
UserPosts,
UserStats
},
data() {
return {
showComponents: false,
isLoading: false,
loadingStatus: {
UserProfile: false,
UserPosts: false,
UserStats: false
}
};
},
methods: {
async loadAll() {
this.isLoading = true;
this.showComponents = false;
// 重置加载状态
Object.keys(this.loadingStatus).forEach(key => {
this.loadingStatus[key] = true;
});
await new Promise(resolve => setTimeout(resolve, 500));
this.showComponents = true;
// 模拟加载完成
setTimeout(() => {
Object.keys(this.loadingStatus).forEach(key => {
this.loadingStatus[key] = false;
});
this.isLoading = false;
}, 2500);
},
async loadOneByOne() {
this.isLoading = true;
this.showComponents = false;
// 重置加载状态
Object.keys(this.loadingStatus).forEach(key => {
this.loadingStatus[key] = false;
});
await new Promise(resolve => setTimeout(resolve, 500));
this.showComponents = true;
// 逐个标记为加载中
const components = Object.keys(this.loadingStatus);
for (let i = 0; i < components.length; i++) {
const component = components[i];
this.loadingStatus[component] = true;
// 模拟每个组件加载时间不同
await new Promise(resolve => setTimeout(resolve, 800));
this.loadingStatus[component] = false;
}
this.isLoading = false;
},
reset() {
this.showComponents = false;
this.isLoading = false;
Object.keys(this.loadingStatus).forEach(key => {
this.loadingStatus[key] = false;
});
}
}
};
</script>
<style>
.multiple-components {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
}
.component-box {
padding: 20px;
border-radius: 5px;
min-height: 150px;
}
.profile-box {
background-color: #e8f4ff;
border: 1px solid #3498db;
}
.posts-box {
background-color: #e8f6ef;
border: 1px solid #2ecc71;
}
.stats-box {
background-color: #fef9e7;
border: 1px solid #f1c40f;
}
.component-loading {
padding: 20px;
text-align: center;
background-color: #f8f9fa;
border: 1px dashed #dee2e6;
border-radius: 5px;
min-height: 150px;
display: flex;
align-items: center;
justify-content: center;
}
.main-loading {
padding: 60px;
text-align: center;
background-color: #f8f9fa;
border: 2px dashed #dee2e6;
border-radius: 5px;
margin: 20px 0;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 15px;
margin-top: 15px;
}
.stat-item {
text-align: center;
padding: 10px;
background-color: white;
border-radius: 5px;
}
.stat-value {
font-size: 1.5rem;
font-weight: bold;
color: #2c3e50;
}
.stat-label {
font-size: 0.9rem;
color: #7f8c8d;
}
</style>
<template>
<div>
<h3>Suspense 错误处理</h3>
<div class="mb-3">
<button @click="loadWithError" class="btn btn-danger me-2">
加载会出错的组件
</button>
<button @click="loadNormal" class="btn btn-success me-2">
加载正常组件
</button>
<button @click="reset" class="btn btn-secondary">
重置
</button>
</div>
<!-- 错误边界组件 -->
<ErrorBoundary>
<Suspense v-if="showComponent">
<template #default>
<DynamicComponent :type="componentType" />
</template>
<template #fallback>
<div class="loading-state">
<div class="spinner"></div>
<p>正在加载组件...</p>
</div>
</template>
</Suspense>
</ErrorBoundary>
<!-- 错误信息 -->
<div v-if="error" class="alert alert-danger mt-3">
<h5>组件加载出错</h5>
<p>错误信息: {{ error }}</p>
<button @click="clearError" class="btn btn-sm btn-warning">清除错误</button>
</div>
</div>
</template>
<script>
// 错误边界组件
const ErrorBoundary = {
name: 'ErrorBoundary',
data() {
return {
error: null
};
},
errorCaptured(err, instance, info) {
console.error('错误被捕获:', err, instance, info);
this.error = err.message;
// 返回 false 阻止错误继续向上传播
return false;
},
render() {
// 如果有错误,显示错误UI
if (this.error) {
return this.$slots.error
? this.$slots.error({ error: this.error })
: this.$slots.default
? this.$slots.default()[0]
: null;
}
// 没有错误,渲染默认插槽
return this.$slots.default ? this.$slots.default()[0] : null;
}
};
// 动态组件
const DynamicComponent = {
props: ['type'],
async setup(props) {
console.log(`加载组件类型: ${props.type}`);
if (props.type === 'error') {
// 模拟加载失败
await new Promise(resolve => setTimeout(resolve, 1000));
throw new Error('组件加载失败:模拟网络错误或服务器错误');
}
if (props.type === 'slow') {
// 模拟慢速加载
await new Promise(resolve => setTimeout(resolve, 3000));
} else {
// 正常加载
await new Promise(resolve => setTimeout(resolve, 1000));
}
return {
message: props.type === 'error'
? '这行代码不会执行'
: `组件 "${props.type}" 加载成功!`,
timestamp: new Date().toLocaleTimeString()
};
},
template: `<div class="dynamic-component">
<h5>动态组件</h5>
<p>{{ message }}</p>
<p>加载时间: {{ timestamp }}</p>
</div>`
};
export default {
components: {
ErrorBoundary,
DynamicComponent
},
data() {
return {
showComponent: false,
componentType: 'normal',
error: null
};
},
methods: {
loadWithError() {
this.reset();
this.componentType = 'error';
this.showComponent = true;
},
loadNormal() {
this.reset();
this.componentType = 'normal';
this.showComponent = true;
},
reset() {
this.showComponent = false;
this.error = null;
},
clearError() {
this.error = null;
}
}
};
</script>
<style>
.dynamic-component {
padding: 20px;
background-color: #e8f4ff;
border: 1px solid #3498db;
border-radius: 5px;
margin: 20px 0;
}
.loading-state {
padding: 40px;
text-align: center;
background-color: #f8f9fa;
border: 1px dashed #dee2e6;
border-radius: 5px;
margin: 20px 0;
}
</style>
#fallback 内容#default 内容<suspense> 主要与 Vue 3 的组合式 API 一起使用,特别是与异步 setup() 函数配合。它可以显著改善异步组件加载的用户体验,但要注意错误处理和适当的加载状态设计。
<!-- 组合使用多个内置组件 -->
<keep-alive>
<transition name="fade" mode="out-in">
<Suspense>
<template #default>
<teleport to="#modal-container">
<component :is="currentModal">
<slot name="modal-content"></slot>
</component>
</teleport>
</template>
<template #fallback>
<div>加载中...</div>
</template>
</Suspense>
</transition>
</keep-alive>
这种组合可以实现:
| 内置组件 | 最佳实践 | 常见错误 |
|---|---|---|
| <component> | 使用 :key 强制重新渲染,避免状态混乱 |
忘记处理动态组件的生命周期 |
| <slot> | 提供有意义的默认内容,使用作用域插槽传递数据 | 插槽嵌套过深导致难以维护 |
| <transition> | 使用适当的过渡模式,为移动设备优化性能 | 过渡时间过长影响用户体验 |
| <transition-group> | 为列表项设置唯一 :key,使用 v-move 类 |
大型列表中使用过渡导致性能问题 |
| <keep-alive> | 合理使用 include/exclude,避免内存泄漏 |
缓存过多组件导致内存占用过高 |
| <teleport> | 确保目标元素在DOM中存在,避免SSR问题 | 忘记处理组件卸载时的清理 |
| <suspense> | 提供有意义的加载状态,处理加载失败情况 | 错误处理不完善导致应用崩溃 |
<keep-alive>;对于异步组件,使用代码分割减少初始加载时间。
Vue.js 内置组件提供了强大的功能来增强应用的能力和用户体验:
这些内置组件是 Vue.js 生态系统的重要组成部分,掌握它们可以让你构建更强大、更灵活、用户体验更好的应用程序。