Vue 3跨组件传参之非父子层级通信解决方案
作者:Cherry Zack
前言
在现代前端开发中,组件化架构已经成为标准实践。Vue 3 作为当前最流行的前端框架之一,提供了多种优雅的跨组件通信方案。特别是当组件之间不存在直接的父子关系时,如何高效、可靠地传递数据就成为了每个 Vue 开发者必须掌握的技能
本文详细介绍 Vue 3中 5种常用的非父子组件传参方式,帮助你在不同场景下选择最合适的解决方案
一、Proviod / Inject - 官方推荐的跨层级方案
Proviod / Inject 是Vue 3 官方推荐的跨层级传参方式,特别适用于祖先组件向任意后代组件(不限层级深度)传递数据的场景
基本原理:这种方式基于依赖注入的设计模式
① Provide:祖先组件提供数据
② Inject:后代组件注入并使用数据
步骤1:祖先组件提供数据
<!-- ParentComponent.vue -->
<script setup>
import { provide, ref, reactive } from 'vue'
// 提供响应式数据
const user = ref({
name: 'Alice',
age: 25,
email: 'alice@example.com'
})
const theme = reactive({
color: 'blue',
fontSize: '16px'
})
// 使用provide提供数据
provide('user', user)
provide('theme', theme)
provide('appTitle', '我的应用') // 也可以提供非响应式数据
</script>
<template>
<div>
<h2>祖先组件</h2>
<p>用户名:{{ user.name }}</p>
<ChildComponent />
</div>
</template>步骤2:后代组件注入数据
<!-- GrandChildComponent.vue -->
<script setup>
import { inject } from 'vue'
// 注入数据
const user = inject('user')
const theme = inject('theme')
const appTitle = inject('appTitle')
// 可以直接修改响应式数据
const updateUserName = (newName) => {
user.value.name = newName
}
const changeTheme = () => {
theme.color = theme.color === 'blue' ? 'red' : 'blue'
}
</script>
<template>
<div :style="{ color: theme.color, fontSize: theme.fontSize }">
<h3>{{ appTitle }}</h3>
<p>用户信息:{{ user.name }} ({{ user.age }}岁)</p>
<button @click="updateUserName('Bob')">修改用户名</button>
<button @click="changeTheme">切换主题</button>
</div>
</template>核心特点:
① 响应式传递:直接传递 ref 或 reactive 对象即可保持响应性
② 无层级限制:无论组件嵌套多深,都可以轻松获取数据
③ 类型安全:配合 TypeScript 可以实现完整的类型检查
二、Pinia - 现代化的全局状态管理
Pinia 是 Vue 3 官方推荐的状态管理库,它替代了 Vuex,提供了更简洁的 API 和更好的 TypeScript 支持
为什么选择 Pinia?
① 更简洁的 API 设计 ② 更好的 TypeScript 集成
③ 完善的开发工具支持 ④ 模块化的 store 设计
步骤1:安装Pinia
npm install pinia # 或 yarn add pinia
步骤2:创建 Pinia 实例并注册
// main.js
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
const app = createApp(App)
const pinia = createPinia()
app.use(pinia)
app.mount('#app')步骤3:定义 Store
// stores/user.js
import { defineStore } from 'pinia'
export const useUserStore = defineStore('user', {
// 状态
state: () => ({
name: 'Alice',
age: 25,
email: 'alice@example.com',
isLoggedIn: false
}),
// 计算属性
getters: {
fullInfo: (state) => `${state.name} (${state.age}岁)`,
isAdult: (state) => state.age >= 18
},
// 方法
actions: {
updateName(newName) {
this.name = newName
},
updateAge(newAge) {
if (newAge >= 0) {
this.age = newAge
}
},
login(email, password) {
// 模拟API调用
return new Promise((resolve) => {
setTimeout(() => {
this.isLoggedIn = true
this.email = email
resolve(true)
}, 1000)
})
},
logout() {
this.isLoggedIn = false
this.email = ''
}
}
})步骤4:在组件中使用 Store
<!-- UserProfile.vue -->
<script setup>
import { useUserStore } from '@/stores/user'
import { storeToRefs } from 'pinia'
// 获取store实例
const userStore = useUserStore()
// 使用storeToRefs保持响应性
const { name, age, fullInfo, isLoggedIn } = storeToRefs(userStore)
// 直接解构actions
const { updateName, updateAge, login, logout } = userStore
// 组件方法
const handleLogin = async () => {
const success = await login('alice@example.com', 'password123')
if (success) {
console.log('登录成功')
}
}
</script>
<template>
<div class="user-profile">
<h2>用户资料</h2>
<div v-if="isLoggedIn">
<p>欢迎,{{ fullInfo }}</p>
<p>邮箱:{{ userStore.email }}</p>
<div>
<label>修改用户名:</label>
<input v-model="name" />
<button @click="updateName(name)">保存</button>
</div>
<div>
<label>修改年龄:</label>
<input v-model.number="age" type="number" />
<button @click="updateAge(age)">保存</button>
</div>
<button @click="logout">退出登录</button>
</div>
<div v-else>
<button @click="handleLogin">登录</button>
</div>
</div>
</template>高级特性
模块化 Store
可以根据功能模块创建多个 store:
// stores/cart.js
export const useCartStore = defineStore('cart', {
state: () => ({
items: [],
total: 0
}),
actions: {
addItem(product) {
this.items.push(product)
this.calculateTotal()
},
calculateTotal() {
this.total = this.items.reduce((sum, item) => sum + item.price, 0)
}
}
})Store 组合使用
<script setup>
import { useUserStore } from '@/stores/user'
import { useCartStore } from '@/stores/cart'
const userStore = useUserStore()
const cartStore = useCartStore()
const checkout = async () => {
if (!userStore.isLoggedIn) {
await userStore.login()
}
// 执行结账逻辑
console.log('结账商品:', cartStore.items)
}
</script>三、mitt - 轻量级事件总线
mitt 是一个轻量级的事件总线库,适用于简单的组件间通信场景
为什么选择 mitt?
① 体积小巧 ② API 简洁易用 ③ 良好的 TypeScript ④ 高性能
步骤1:安装 mitt
npm install mitt # 或 yarn add mitt
步骤2:创建事件总线实例
// utils/eventBus.js import mitt from 'mitt' // 创建事件总线实例 const eventBus = mitt() export default eventBus
步骤3:发送事件
<!-- ComponentA.vue -->
<script setup>
import eventBus from '@/utils/eventBus'
const sendMessage = () => {
// 发送事件
eventBus.emit('message', {
text: 'Hello from Component A',
timestamp: new Date()
})
}
const updateUser = () => {
// 发送带参数的事件
eventBus.emit('user:update', {
name: 'New Name',
age: 30
})
}
</script>
<template>
<div>
<h3>组件A</h3>
<button @click="sendMessage">发送消息</button>
<button @click="updateUser">更新用户</button>
</div>
</template>步骤4:接收事件
<!-- ComponentB.vue -->
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import eventBus from '@/utils/eventBus'
const messages = ref([])
const userName = ref('')
// 事件处理函数
const handleMessage = (data) => {
messages.value.push(data)
}
const handleUserUpdate = (userData) => {
userName.value = userData.name
}
// 组件挂载时注册事件监听
onMounted(() => {
eventBus.on('message', handleMessage)
eventBus.on('user:update', handleUserUpdate)
})
// 组件卸载时移除事件监听
onUnmounted(() => {
eventBus.off('message', handleMessage)
eventBus.off('user:update', handleUserUpdate)
})
</script>
<template>
<div>
<h3>组件B</h3>
<p>当前用户:{{ userName }}</p>
<div v-for="msg in messages" :key="msg.timestamp">
<p>{{ msg.text }} - {{ msg.timestamp.toLocaleTimeString() }}</p>
</div>
</div>
</template>高级用法:
事件命名规范
建议使用命名空间来组织事件
// 好的命名方式
eventBus.emit('user:created', userData)
eventBus.emit('user:updated', userData)
eventBus.emit('user:deleted', userId)
eventBus.emit('order:placed', orderData)
eventBus.emit('order:cancelled', orderId)一次性事件
// 只监听一次事件
eventBus.once('message', (data) => {
console.log('只执行一次:', data)
})清楚所有事件
// 清除所有事件监听 eventBus.all.clear()
四、全局属性 - 工具类的全局共享
Vue 3 提供了app.config.globalProperties 来注册全局属性,适用于工具类和配置的全局共享
步骤1:注册全局属性
// main.js
import { createApp } from 'vue'
import axios from 'axios'
import App from './App.vue'
const app = createApp(App)
// 注册全局HTTP客户端
app.config.globalProperties.$http = axios.create({
baseURL: 'https://api.example.com'
})
// 注册全局工具函数
app.config.globalProperties.$formatDate = (date) => {
return new Intl.DateTimeFormat('zh-CN').format(date)
}
// 注册全局配置
app.config.globalProperties.$appConfig = {
apiUrl: 'https://api.example.com',
version: '1.0.0',
theme: 'dark'
}
app.mount('#app')步骤2:在组件中使用全局属性
<!-- AnyComponent.vue -->
<script setup>
import { getCurrentInstance } from 'vue'
// 获取当前组件实例
const instance = getCurrentInstance()
// 通过proxy访问全局属性
const $http = instance?.proxy?.$http
const $formatDate = instance?.proxy?.$formatDate
const $appConfig = instance?.proxy?.$appConfig
// 使用全局HTTP客户端
const fetchData = async () => {
try {
const response = await $http.get('/users')
console.log('用户数据:', response.data)
} catch (error) {
console.error('请求失败:', error)
}
}
// 使用全局工具函数
const formattedDate = $formatDate(new Date())
// 使用全局配置
console.log('API地址:', $appConfig.apiUrl)
console.log('应用版本:', $appConfig.version)
</script>
<template>
<div>
<h3>全局属性示例</h3>
<p>当前时间:{{ formattedDate }}</p>
<p>应用版本:{{ $appConfig.version }}</p>
<button @click="fetchData">获取数据</button>
</div>
</template>TypeScript 支持
为了在 TypeScript 中获得类型提示,需要扩展 Vue 的类型定义:
// shims-vue.d.ts
import { AxiosInstance } from 'axios'
declare module 'vue' {
interface ComponentCustomProperties {
$http: AxiosInstance
$formatDate: (date: Date) => string
$appConfig: {
apiUrl: string
version: string
theme: string
}
}
}五、浏览器存储 - 持久化数据方案
浏览器提供的localStorage和sessionStorage是实现数据持久化的简单方案
localStorage vs sessionStorage
特性 | localStorage | sessionStorage |
生命周期 | 永久存储,除非手动清除 | 会话期间,页面关闭后清除 |
存储大小 | 约 5MB | 约 5MB |
作用域 | 同源所有页面共享 | 仅当前标签页 |
网络请求 | 会随 HTTP 请求发送到服务器 | 不会随 HTTP 请求发送 |
基础使用示例:
<!-- StorageComponent.vue -->
<script setup>
import { ref, watch } from 'vue'
// 用户设置
const userSettings = ref({
theme: 'light',
language: 'zh-CN',
fontSize: '16px'
})
// 用户登录状态
const user = ref({
isLoggedIn: false,
name: '',
token: ''
})
// 初始化:从localStorage读取数据
const initData = () => {
try {
// 读取用户设置
const savedSettings = localStorage.getItem('userSettings')
if (savedSettings) {
userSettings.value = JSON.parse(savedSettings)
}
// 读取用户登录状态
const savedUser = localStorage.getItem('user')
if (savedUser) {
user.value = JSON.parse(savedUser)
}
} catch (error) {
console.error('读取存储数据失败:', error)
}
}
// 保存到localStorage
const saveToLocalStorage = () => {
try {
localStorage.setItem('userSettings', JSON.stringify(userSettings.value))
localStorage.setItem('user', JSON.stringify(user.value))
} catch (error) {
console.error('保存数据失败:', error)
}
}
// 监听数据变化,自动保存
watch(userSettings, saveToLocalStorage, { deep: true })
watch(user, saveToLocalStorage, { deep: true })
// 登录方法
const login = (username, password) => {
// 模拟登录API调用
user.value = {
isLoggedIn: true,
name: username,
token: 'fake-jwt-token'
}
}
// 退出登录
const logout = () => {
user.value = {
isLoggedIn: false,
name: '',
token: ''
}
// 可选:清除特定存储
localStorage.removeItem('user')
}
// 清除所有存储
const clearAllStorage = () => {
localStorage.clear()
sessionStorage.clear()
// 重置状态
userSettings.value = {
theme: 'light',
language: 'zh-CN',
fontSize: '16px'
}
user.value = {
isLoggedIn: false,
name: '',
token: ''
}
}
// 初始化数据
initData()
</script>
<template>
<div>
<h3>浏览器存储示例</h3>
<div v-if="user.isLoggedIn">
<p>欢迎,{{ user.name }}!</p>
<button @click="logout">退出登录</button>
</div>
<div v-else>
<button @click="login('Alice', 'password')">登录</button>
</div>
<div>
<h4>用户设置</h4>
<div>
<label>主题:</label>
<select v-model="userSettings.theme">
<option value="light">浅色</option>
<option value="dark">深色</option>
</select>
</div>
<div>
<label>语言:</label>
<select v-model="userSettings.language">
<option value="zh-CN">中文</option>
<option value="en-US">English</option>
</select>
</div>
</div>
<button @click="clearAllStorage" style="margin-top: 20px;">
清除所有存储
</button>
</div>
</template>封装存储工具类
// utils/storage.js
class StorageUtil {
// localStorage操作
static setLocal(key, value) {
try {
localStorage.setItem(key, JSON.stringify(value))
} catch (error) {
console.error(`设置localStorage失败 (${key}):`, error)
}
}
static getLocal(key, defaultValue = null) {
try {
const value = localStorage.getItem(key)
return value ? JSON.parse(value) : defaultValue
} catch (error) {
console.error(`获取localStorage失败 (${key}):`, error)
return defaultValue
}
}
static removeLocal(key) {
try {
localStorage.removeItem(key)
} catch (error) {
console.error(`删除localStorage失败 (${key}):`, error)
}
}
// sessionStorage操作
static setSession(key, value) {
try {
sessionStorage.setItem(key, JSON.stringify(value))
} catch (error) {
console.error(`设置sessionStorage失败 (${key}):`, error)
}
}
static getSession(key, defaultValue = null) {
try {
const value = sessionStorage.getItem(key)
return value ? JSON.parse(value) : defaultValue
} catch (error) {
console.error(`获取sessionStorage失败 (${key}):`, error)
return defaultValue
}
}
static removeSession(key) {
try {
sessionStorage.removeItem(key)
} catch (error) {
console.error(`删除sessionStorage失败 (${key}):`, error)
}
}
// 清除所有存储
static clearAll() {
try {
localStorage.clear()
sessionStorage.clear()
} catch (error) {
console.error('清除存储失败:', error)
}
}
}
export default StorageUtil响应式封装:
<!-- ReactiveStorage.vue -->
<script setup>
import { ref, watch } from 'vue'
import StorageUtil from '@/utils/storage'
// 创建响应式存储
const createReactiveStorage = (key, defaultValue, useSession = false) => {
const data = ref(StorageUtil[useSession ? 'getSession' : 'getLocal'](key, defaultValue))
// 监听数据变化,自动保存
watch(data, (newValue) => {
StorageUtil[useSession ? 'setSession' : 'setLocal'](key, newValue)
}, { deep: true })
return data
}
// 使用响应式存储
const user = createReactiveStorage('user', {
isLoggedIn: false,
name: ''
})
const settings = createReactiveStorage('settings', {
theme: 'light'
})
// 会话级存储
const tempData = createReactiveStorage('tempData', {}, true)
</script>方案对比与选择指南
方式 | 适用场景 | 响应式 | 复杂度 | 推荐度 |
Provide/Inject | 祖先→后代的跨层级传递 | ✅ | ⭐⭐ | ⭐⭐⭐⭐ |
Pinia | 任意组件的复杂状态共享 | ✅ | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
mitt 事件总线 | 简单的组件间通信 | ❌ | ⭐ | ⭐⭐⭐ |
全局属性 | 工具类和配置的全局共享 | ❌ | ⭐ | ⭐⭐ |
浏览器存储 | 数据持久化 | ❌ | ⭐ | ⭐⭐⭐ |
总结
到此这篇关于Vue 3跨组件传参之非父子层级通信解决方案的文章就介绍到这了,更多相关Vue3跨组件传参非父子层级通信内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
