四、组件通信
4.1 Props 与 Emits
vue
<!-- 父组件 Parent.vue -->
<template>
<Child
:name="userName"
:age="userAge"
@update="handleUpdate"
@change:name="handleNameChange"
/>
</template>
<script setup>
import { ref } from 'vue'
import Child from './Child.vue'
const userName = ref('张三')
const userAge = ref(25)
const handleUpdate = (value) => {
console.log('收到更新:', value)
}
const handleNameChange = (value) => {
userName.value = value
}
</script>
vue
<!-- 子组件 Child.vue -->
<template>
<div>
<p>姓名: {
{ name }}</p>
<p>年龄: {
{ age }}</p>
<button @click="emitUpdate">更新</button>
<button @click="changeName">修改名字</button>
</div>
</template>
<script setup>
// 定义 props
const props = defineProps({
name: {
type: String,
required: true,
default: '匿名'
},
age: {
type: Number,
default: 0,
validator: (value) => value >= 0 && value <= 150
}
})
// 定义 emits
const emit = defineEmits(['update', 'change:name'])
const emitUpdate = () => {
emit('update', { name: props.name, age: props.age })
}
const changeName = () => {
emit('change:name', '李四')
}
</script>
4.2 v-model 双向绑定
vue
<!-- 父组件 -->
<template>
<!-- v-model 语法糖 -->
<CustomInput v-model="message" />
<!-- 等价于 -->
<CustomInput :modelValue="message" @update:modelValue="message = $event" />
<!-- 多个 v-model -->
<CustomForm
v-model:name="name"
v-model:age="age"
/>
</template>
<script setup>
import { ref } from 'vue'
import CustomInput from './CustomInput.vue'
import CustomForm from './CustomForm.vue'
const message = ref('Hello')
const name = ref('张三')
const age = ref(25)
</script>
vue
<!-- CustomInput.vue -->
<template>
<input
:value="modelValue"
@input="$emit('update:modelValue', $event.target.value)"
/>
</template>
<script setup>
defineProps(['modelValue'])
defineEmits(['update:modelValue'])
</script>
vue
<!-- CustomForm.vue -->
<template>
<input
:value="name"
@input="$emit('update:name', $event.target.value)"
placeholder="姓名"
/>
<input
:value="age"
@input="$emit('update:age', $event.target.value)"
placeholder="年龄"
/>
</template>
<script setup>
defineProps(['name', 'age'])
defineEmits(['update:name', 'update:age'])
</script>
4.3 provide / inject
vue
<!-- 祖先组件 -->
<template>
<ChildComponent />
</template>
<script setup>
import { provide, ref, readonly } from 'vue'
import ChildComponent from './ChildComponent.vue'
// 提供响应式数据
const user = ref({ name: '张三', age: 25 })
const updateUser = (newName) => {
user.value.name = newName
}
// 提供数据
provide('user', readonly(user))
provide('updateUser', updateUser)
provide('config', {
theme: 'dark',
language: 'zh-CN'
})
</script>
vue
<!-- 后代组件 -->
<template>
<div>
<p>姓名: {
{ user.name }}</p>
<p>年龄: {
{ user.age }}</p>
<p>主题: {
{ config.theme }}</p>
<button @click="updateUser('李四')">修改名字</button>
</div>
</template>
<script setup>
import { inject } from 'vue'
// 注入数据
const user = inject('user')
const updateUser = inject('updateUser')
const config = inject('config', { theme: 'light', language: 'en' }) // 提供默认值
// 注入时使用工厂函数
const logger = inject('logger', () => ({
log: (msg) => console.log(msg)
}))
</script>
4.4 事件总线(mitt)
bash
npm install mitt
javascript
// utils/eventBus.js
import mitt from 'mitt'
const emitter = mitt()
export default emitter
vue
<!-- ComponentA.vue -->
<template>
<button @click="sendEvent">发送事件</button>
</template>
<script setup>
import emitter from '@/utils/eventBus'
const sendEvent = () => {
emitter.emit('custom-event', { message: 'Hello from Component A' })
emitter.emit('update', '数据更新')
}
</script>
vue
<!-- ComponentB.vue -->
<script setup>
import { onMounted, onUnmounted } from 'vue'
import emitter from '@/utils/eventBus'
const handleCustomEvent = (data) => {
console.log('收到事件:', data)
}
const handleUpdate = (data) => {
console.log('更新:', data)
}
onMounted(() => {
emitter.on('custom-event', handleCustomEvent)
emitter.on('update', handleUpdate)
// 监听所有事件
emitter.on('*', (type, event) => {
console.log(`事件 ${type} 触发`, event)
})
})
onUnmounted(() => {
emitter.off('custom-event', handleCustomEvent)
emitter.off('update', handleUpdate)
emitter.all.clear() // 清除所有事件
})
</script>
五、内置组件
5.1 Teleport
vue
<template>
<div>
<!-- 将模态框渲染到 body 中 -->
<Teleport to="body">
<div v-if="showModal" class="modal">
<div class="modal-content">
<h3>模态框</h3>
<p>这是通过 Teleport 渲染的</p>
<button @click="closeModal">关闭</button>
</div>
</div>
</Teleport>
<!-- 渲染到指定选择器 -->
<Teleport to="#app-footer">
<p>渲染到页脚</p>
</Teleport>
<!-- 禁用 Teleport -->
<Teleport to="body" :disabled="isMobile">
<div>移动端不禁用</div>
</Teleport>
<!-- 多个 Teleport 到同一目标 -->
<Teleport to="#modals">
<div>模态框1</div>
</Teleport>
<Teleport to="#modals">
<div>模态框2</div>
</Teleport>
</div>
</template>
<script setup>
import { ref } from 'vue'
const showModal = ref(false)
const isMobile = ref(false)
const openModal = () => {
showModal.value = true
}
const closeModal = () => {
showModal.value = false
}
</script>
<style scoped>
.modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal-content {
background: white;
padding: 20px;
border-radius: 8px;
min-width: 300px;
}
</style>
5.2 Suspense
vue
<template>
<Suspense>
<!-- 异步组件 -->
<template #default>
<AsyncComponent />
</template>
<!-- 加载状态 -->
<template #fallback>
<div>加载中...</div>
</template>
</Suspense>
</template>
<script setup>
import { defineAsyncComponent } from 'vue'
// 异步组件
const AsyncComponent = defineAsyncComponent({
loader: () => import('./HeavyComponent.vue'),
loadingComponent: LoadingComponent,
errorComponent: ErrorComponent,
delay: 200, // 延迟显示加载组件
timeout: 3000 // 超时时间
})
</script>
vue
<!-- AsyncComponent.vue -->
<template>
<div>
<h1>{
{ data.title }}</h1>
<p>{
{ data.content }}</p>
</div>
</template>
<script setup>
// 异步组件可以使用 await
const data = await fetchData()
async function fetchData() {
const response = await fetch('/api/data')
return response.json()
}
</script>
5.3 Fragment
vue
<!-- Vue 3 支持多个根节点 -->
<template>
<header>头部</header>
<main>主要内容</main>
<footer>页脚</footer>
</template>
<!-- 如果需要显式使用 Fragment -->
<template>
<Fragment>
<div>内容1</div>
<div>内容2</div>
</Fragment>
</template>
六、路由(Vue Router 4)
6.1 基本配置
javascript
// router/index.js
import { createRouter, createWebHistory } from 'vue-router'
// 定义路由
const routes = [
{
path: '/',
name: 'Home',
component: () => import('@/views/Home.vue')
},
{
path: '/about',
name: 'About',
component: () => import('@/views/About.vue')
},
{
path: '/user/:id',
name: 'User',
component: () => import('@/views/User.vue'),
props: true // 将路由参数作为 props 传递给组件
},
{
path: '/posts',
name: 'Posts',
component: () => import('@/views/Posts.vue'),
children: [
{
path: ':id',
component: () => import('@/views/PostDetail.vue')
}
]
}
]
// 创建路由实例
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes,
// 滚动行为
scrollBehavior(to, from, savedPosition) {
if (savedPosition) {
return savedPosition
} else {
return { top: 0 }
}
}
})
// 路由守卫
router.beforeEach((to, from, next) => {
// 权限验证
if (to.meta.requiresAuth && !isAuthenticated()) {
next('/login')
} else {
next()
}
})
router.afterEach((to, from) => {
// 页面标题设置
document.title = to.meta.title || 'Vue App'
})
export default router
6.2 路由使用
vue
<template>
<div>
<!-- 路由链接 -->
<nav>
<router-link to="/">首页</router-link>
<router-link :to="{ name: 'About' }">关于</router-link>
<router-link :to="{ path: '/user/123' }">用户123</router-link>
<router-link :to="{ name: 'User', params: { id: 456 } }">用户456</router-link>
<!-- 带查询参数 -->
<router-link :to="{ path: '/search', query: { keyword: 'vue' } }">
搜索
</router-link>
<!-- 自定义样式 -->
<router-link
to="/profile"
active-class="active"
exact-active-class="exact-active"
>
个人资料
</router-link>
</nav>
<!-- 路由出口 -->
<router-view />
<!-- 命名视图 -->
<router-view name="sidebar" />
<router-view name="header" />
</div>
</template>
<script setup>
import { useRouter, useRoute } from 'vue-router'
const router = useRouter()
const route = useRoute()
// 编程式导航
const goToHome = () => {
router.push('/')
}
const goToUser = (id) => {
router.push({ name: 'User', params: { id } })
}
const goBack = () => {
router.back()
}
const goForward = () => {
router.forward()
}
const go = () => {
router.go(-2)
}
// 替换导航(不留下历史记录)
const replaceToHome = () => {
router.replace('/')
}
// 获取路由信息
console.log(route.params)
console.log(route.query)
console.log(route.hash)
console.log(route.fullPath)
console.log(route.path)
console.log(route.name)
// 监听路由变化
watch(() => route.params, (newParams) => {
console.log('路由参数变化:', newParams)
})
// 路由守卫(组件内)
import { onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router'
onBeforeRouteLeave((to, from) => {
// 离开当前组件前
return confirm('确定要离开吗?')
})
onBeforeRouteUpdate((to, from) => {
// 路由参数变化时
console.log('路由更新:', to.params.id)
})
</script>