Vue Teleport 和 Suspense
Vue 3 引入的两个特殊组件,用于处理特殊的渲染场景。
Teleport 传送门
基本概念
Teleport 提供了一种将子组件渲染到 DOM 树外部的方式,常用于:
- 模态框(Modal)
- 通知提示(Toast)
- 下拉菜单(Dropdown)
- 全屏覆盖层
为什么需要 Teleport?
基本用法
<template>
<div class="parent">
<h1>父组件</h1>
<button @click="showModal = true">打开模态框</button>
<Teleport to="body">
<div v-if="showModal" class="modal">
<div class="modal-content">
<h2>模态框</h2>
<p>这是使用 Teleport 传送的内容</p>
<button @click="showModal = false">关闭</button>
</div>
</div>
</Teleport>
</div>
</template>
<script setup>
import { ref } from 'vue'
const showModal = ref(false)
</script>
<style scoped>
.modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
}
.modal-content {
background: white;
padding: 20px;
border-radius: 8px;
}
</style>
动态目标
<template>
<Teleport :to="targetSelector">
<div>内容</div>
</Teleport>
</template>
<script setup>
import { ref } from 'vue'
const targetSelector = ref('#modal-root')
</script>
禁用 Teleport
<Teleport to="body" :disabled="isMobile">
<div>内容</div>
</Teleport>
Suspense 异步组件
基本概念
Suspense 允许你等待异步组件加载时显示 fallback 内容:
┌─────────────────────────────────────────────────────────┐
│ Suspense 工作流程 │
├─────────────────────────────────────────────────────────┤
│ │
│ <Suspense> │
│ ├─ #default: 异步组件 │
│ │ │
│ └─ #fallback: 加载状态 │
│ │
│ 状态流程: │
│ 加载中 ──▶ Suspense 显示 #fallback │
│ │ │
│ ▼ │
│ 加载完成 ──▶ Suspense 显示 #default │
│ │
└─────────────────────────────────────────────────────────┘
基本用法
<template>
<Suspense>
<template #default>
<AsyncUserProfile />
</template>
<template #fallback>
<div>加载中...</div>
</template>
</Suspense>
</template>
<script setup>
import { defineAsyncComponent } from 'vue'
const AsyncUserProfile = defineAsyncComponent(() =>
import('./components/UserProfile.vue')
)
</script>
带参数的异步组件
<!-- UserProfile.vue -->
<script setup>
const props = defineProps({
userId: String
})
// setup 可以是 async 函数
const userData = await fetchUser(props.userId)
</script>
多层 Suspense
<template>
<Suspense>
<template #default>
<ParentComponent />
</template>
<template #fallback>
<div>加载中...</div>
</template>
</Suspense>
</template>
<!-- ParentComponent.vue -->
<template>
<div>
<h1>父组件</h1>
<Suspense>
<template #default>
<ChildComponent />
</template>
<template #fallback>
<div>加载子组件...</div>
</template>
</Suspense>
</div>
</template>
实际应用
Modal 组件
<!-- Modal.vue -->
<script setup>
import { watch } from 'vue'
const props = defineProps({
modelValue: Boolean
})
const emit = defineEmits(['update:modelValue'])
function close() {
emit('update:modelValue', false)
}
</script>
<template>
<Teleport to="body">
<Transition name="modal">
<div v-if="modelValue" class="modal-overlay" @click.self="close">
<div class="modal-content">
<slot />
<button class="close-btn" @click="close">×</button>
</div>
</div>
</Transition>
</Teleport>
</template>
<style scoped>
.modal-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
}
.modal-content {
background: white;
padding: 20px;
border-radius: 8px;
position: relative;
}
.close-btn {
position: absolute;
top: 10px;
right: 10px;
border: none;
background: none;
font-size: 24px;
cursor: pointer;
}
.modal-enter-active,
.modal-leave-active {
transition: opacity 0.3s;
}
.modal-enter-from,
.modal-leave-to {
opacity: 0;
}
</style>
<!-- 使用 Modal -->
<script setup>
import { ref } from 'vue'
import Modal from './Modal.vue'
const showModal = ref(false)
</script>
<template>
<button @click="showModal = true">打开模态框</button>
<Modal v-model="showModal">
<h2>标题</h2>
<p>内容</p>
</Modal>
</template>
异步数据加载
<!-- AsyncDataList.vue -->
<script setup>
import { ref, defineAsyncComponent } from 'vue'
const props = defineProps({
type: String
})
const dataList = defineAsyncComponent({
loader: async () => {
const response = await fetch(`/api/${props.type}`)
return {
template: `
<ul>
<li v-for="item in data" :key="item.id">{{ item.name }}</li>
</ul>
`,
setup() {
const data = ref(null)
onMounted(async () => {
const res = await fetch(`/api/${props.type}`)
data.value = await res.json()
})
return { data }
}
}
},
loadingComponent: {
template: '<div>加载中...</div>'
},
errorComponent: {
template: '<div>加载失败</div>'
},
delay: 200,
timeout: 3000
})
</script>
<template>
<Suspense>
<template #default>
<component :is="dataList" />
</template>
<template #fallback>
<div class="loading">
<div class="spinner"></div>
<p>正在加载数据...</p>
</div>
</template>
</Suspense>
</template>
<style scoped>
.loading {
display: flex;
flex-direction: column;
align-items: center;
padding: 20px;
}
.spinner {
width: 40px;
height: 40px;
border: 4px solid #f3f3f3;
border-top: 4px solid #42b983;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>
小结
本章我们详细学习了 Vue 特殊组件的内容:
- Teleport:将内容传送到 DOM 树外部
- 模态框应用:使用 Teleport 实现模态框
- Suspense:处理异步组件的加载状态
- 多层 Suspense:嵌套使用 Suspense
- 异步数据加载:结合 Suspense 加载数据
练习
- 创建一个 Toast 通知组件
- 创建一个带加载状态的异步列表组件
- 实现一个无限滚动加载组件
准备好继续学习了吗?