Vue?3跨組件傳參之非父子層級通信解決方案
前言
在現(xiàn)代前端開發(fā)中,組件化架構(gòu)已經(jīng)成為標準實踐。Vue 3 作為當(dāng)前最流行的前端框架之一,提供了多種優(yōu)雅的跨組件通信方案。特別是當(dāng)組件之間不存在直接的父子關(guān)系時,如何高效、可靠地傳遞數(shù)據(jù)就成為了每個 Vue 開發(fā)者必須掌握的技能
本文詳細介紹 Vue 3中 5種常用的非父子組件傳參方式,幫助你在不同場景下選擇最合適的解決方案
一、Proviod / Inject - 官方推薦的跨層級方案
Proviod / Inject 是Vue 3 官方推薦的跨層級傳參方式,特別適用于祖先組件向任意后代組件(不限層級深度)傳遞數(shù)據(jù)的場景
基本原理:這種方式基于依賴注入的設(shè)計模式
① Provide:祖先組件提供數(shù)據(jù)
② Inject:后代組件注入并使用數(shù)據(jù)
步驟1:祖先組件提供數(shù)據(jù)
<!-- ParentComponent.vue -->
<script setup>
import { provide, ref, reactive } from 'vue'
// 提供響應(yīng)式數(shù)據(jù)
const user = ref({
name: 'Alice',
age: 25,
email: 'alice@example.com'
})
const theme = reactive({
color: 'blue',
fontSize: '16px'
})
// 使用provide提供數(shù)據(jù)
provide('user', user)
provide('theme', theme)
provide('appTitle', '我的應(yīng)用') // 也可以提供非響應(yīng)式數(shù)據(jù)
</script>
<template>
<div>
<h2>祖先組件</h2>
<p>用戶名:{{ user.name }}</p>
<ChildComponent />
</div>
</template>步驟2:后代組件注入數(shù)據(jù)
<!-- GrandChildComponent.vue -->
<script setup>
import { inject } from 'vue'
// 注入數(shù)據(jù)
const user = inject('user')
const theme = inject('theme')
const appTitle = inject('appTitle')
// 可以直接修改響應(yīng)式數(shù)據(jù)
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>核心特點:
① 響應(yīng)式傳遞:直接傳遞 ref 或 reactive 對象即可保持響應(yīng)性
② 無層級限制:無論組件嵌套多深,都可以輕松獲取數(shù)據(jù)
③ 類型安全:配合 TypeScript 可以實現(xiàn)完整的類型檢查
二、Pinia - 現(xiàn)代化的全局狀態(tài)管理
Pinia 是 Vue 3 官方推薦的狀態(tài)管理庫,它替代了 Vuex,提供了更簡潔的 API 和更好的 TypeScript 支持
為什么選擇 Pinia?
① 更簡潔的 API 設(shè)計 ② 更好的 TypeScript 集成
③ 完善的開發(fā)工具支持 ④ 模塊化的 store 設(shè)計
步驟1:安裝Pinia
npm install pinia # 或 yarn add pinia
步驟2:創(chuàng)建 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', {
// 狀態(tài)
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調(diào)用
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保持響應(yīng)性
const { name, age, fullInfo, isLoggedIn } = storeToRefs(userStore)
// 直接解構(gòu)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
可以根據(jù)功能模塊創(chuàng)建多個 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()
}
// 執(zhí)行結(jié)賬邏輯
console.log('結(jié)賬商品:', cartStore.items)
}
</script>三、mitt - 輕量級事件總線
mitt 是一個輕量級的事件總線庫,適用于簡單的組件間通信場景
為什么選擇 mitt?
① 體積小巧 ② API 簡潔易用 ③ 良好的 TypeScript ④ 高性能
步驟1:安裝 mitt
npm install mitt # 或 yarn add mitt
步驟2:創(chuàng)建事件總線實例
// utils/eventBus.js import mitt from 'mitt' // 創(chuàng)建事件總線實例 const eventBus = mitt() export default eventBus
步驟3:發(fā)送事件
<!-- ComponentA.vue -->
<script setup>
import eventBus from '@/utils/eventBus'
const sendMessage = () => {
// 發(fā)送事件
eventBus.emit('message', {
text: 'Hello from Component A',
timestamp: new Date()
})
}
const updateUser = () => {
// 發(fā)送帶參數(shù)的事件
eventBus.emit('user:update', {
name: 'New Name',
age: 30
})
}
</script>
<template>
<div>
<h3>組件A</h3>
<button @click="sendMessage">發(fā)送消息</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('')
// 事件處理函數(shù)
const handleMessage = (data) => {
messages.value.push(data)
}
const handleUserUpdate = (userData) => {
userName.value = userData.name
}
// 組件掛載時注冊事件監(jiān)聽
onMounted(() => {
eventBus.on('message', handleMessage)
eventBus.on('user:update', handleUserUpdate)
})
// 組件卸載時移除事件監(jiān)聽
onUnmounted(() => {
eventBus.off('message', handleMessage)
eventBus.off('user:update', handleUserUpdate)
})
</script>
<template>
<div>
<h3>組件B</h3>
<p>當(dāng)前用戶:{{ userName }}</p>
<div v-for="msg in messages" :key="msg.timestamp">
<p>{{ msg.text }} - {{ msg.timestamp.toLocaleTimeString() }}</p>
</div>
</div>
</template>高級用法:
事件命名規(guī)范
建議使用命名空間來組織事件
// 好的命名方式
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)一次性事件
// 只監(jiān)聽一次事件
eventBus.once('message', (data) => {
console.log('只執(zhí)行一次:', data)
})清楚所有事件
// 清除所有事件監(jiān)聽 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'
})
// 注冊全局工具函數(shù)
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'
// 獲取當(dāng)前組件實例
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('用戶數(shù)據(jù):', response.data)
} catch (error) {
console.error('請求失敗:', error)
}
}
// 使用全局工具函數(shù)
const formattedDate = $formatDate(new Date())
// 使用全局配置
console.log('API地址:', $appConfig.apiUrl)
console.log('應(yīng)用版本:', $appConfig.version)
</script>
<template>
<div>
<h3>全局屬性示例</h3>
<p>當(dāng)前時間:{{ formattedDate }}</p>
<p>應(yīng)用版本:{{ $appConfig.version }}</p>
<button @click="fetchData">獲取數(shù)據(jù)</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
}
}
}五、瀏覽器存儲 - 持久化數(shù)據(jù)方案
瀏覽器提供的localStorage和sessionStorage是實現(xiàn)數(shù)據(jù)持久化的簡單方案
localStorage vs sessionStorage
特性 | localStorage | sessionStorage |
生命周期 | 永久存儲,除非手動清除 | 會話期間,頁面關(guān)閉后清除 |
存儲大小 | 約 5MB | 約 5MB |
作用域 | 同源所有頁面共享 | 僅當(dāng)前標簽頁 |
網(wǎng)絡(luò)請求 | 會隨 HTTP 請求發(fā)送到服務(wù)器 | 不會隨 HTTP 請求發(fā)送 |
基礎(chǔ)使用示例:
<!-- StorageComponent.vue -->
<script setup>
import { ref, watch } from 'vue'
// 用戶設(shè)置
const userSettings = ref({
theme: 'light',
language: 'zh-CN',
fontSize: '16px'
})
// 用戶登錄狀態(tài)
const user = ref({
isLoggedIn: false,
name: '',
token: ''
})
// 初始化:從localStorage讀取數(shù)據(jù)
const initData = () => {
try {
// 讀取用戶設(shè)置
const savedSettings = localStorage.getItem('userSettings')
if (savedSettings) {
userSettings.value = JSON.parse(savedSettings)
}
// 讀取用戶登錄狀態(tài)
const savedUser = localStorage.getItem('user')
if (savedUser) {
user.value = JSON.parse(savedUser)
}
} catch (error) {
console.error('讀取存儲數(shù)據(jù)失敗:', error)
}
}
// 保存到localStorage
const saveToLocalStorage = () => {
try {
localStorage.setItem('userSettings', JSON.stringify(userSettings.value))
localStorage.setItem('user', JSON.stringify(user.value))
} catch (error) {
console.error('保存數(shù)據(jù)失?。?, error)
}
}
// 監(jiān)聽數(shù)據(jù)變化,自動保存
watch(userSettings, saveToLocalStorage, { deep: true })
watch(user, saveToLocalStorage, { deep: true })
// 登錄方法
const login = (username, password) => {
// 模擬登錄API調(diào)用
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()
// 重置狀態(tài)
userSettings.value = {
theme: 'light',
language: 'zh-CN',
fontSize: '16px'
}
user.value = {
isLoggedIn: false,
name: '',
token: ''
}
}
// 初始化數(shù)據(jù)
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>用戶設(shè)置</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(`設(shè)置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(`設(shè)置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響應(yīng)式封裝:
<!-- ReactiveStorage.vue -->
<script setup>
import { ref, watch } from 'vue'
import StorageUtil from '@/utils/storage'
// 創(chuàng)建響應(yīng)式存儲
const createReactiveStorage = (key, defaultValue, useSession = false) => {
const data = ref(StorageUtil[useSession ? 'getSession' : 'getLocal'](key, defaultValue))
// 監(jiān)聽數(shù)據(jù)變化,自動保存
watch(data, (newValue) => {
StorageUtil[useSession ? 'setSession' : 'setLocal'](key, newValue)
}, { deep: true })
return data
}
// 使用響應(yīng)式存儲
const user = createReactiveStorage('user', {
isLoggedIn: false,
name: ''
})
const settings = createReactiveStorage('settings', {
theme: 'light'
})
// 會話級存儲
const tempData = createReactiveStorage('tempData', {}, true)
</script>方案對比與選擇指南
方式 | 適用場景 | 響應(yīng)式 | 復(fù)雜度 | 推薦度 |
Provide/Inject | 祖先→后代的跨層級傳遞 | ? | ?? | ???? |
Pinia | 任意組件的復(fù)雜狀態(tài)共享 | ? | ??? | ????? |
mitt 事件總線 | 簡單的組件間通信 | ? | ? | ??? |
全局屬性 | 工具類和配置的全局共享 | ? | ? | ?? |
瀏覽器存儲 | 數(shù)據(jù)持久化 | ? | ? | ??? |
總結(jié)
到此這篇關(guān)于Vue 3跨組件傳參之非父子層級通信解決方案的文章就介紹到這了,更多相關(guān)Vue3跨組件傳參非父子層級通信內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Vue如何使用ElementUI對表單元素進行自定義校驗及踩坑
有一些驗證不是通過input select這樣的受控組件來觸發(fā)驗證條件的 ,可以通過自定義驗證的方法來觸發(fā),下面這篇文章主要給大家介紹了關(guān)于Vue如何使用ElementUI對表單元素進行自定義校驗及踩坑的相關(guān)資料,需要的朋友可以參考下2023-02-02
vue3中vue.config.js配置Element-plus組件和Icon圖標實現(xiàn)按需自動引入實例代碼
這篇文章主要給大家介紹了關(guān)于vue3中vue.config.js配置Element-plus組件和Icon圖標實現(xiàn)按需自動引入的相關(guān)資料,在Vue 3中可以通過配置vue.config.js文件來進行按需自動引入,需要的朋友可以參考下2024-02-02
vue-router中query和params的區(qū)別解析
vue-router是Vue.js官方的路由插件,它和vue.js是深度集成的,適合用于構(gòu)建單頁面應(yīng)用,這篇文章主要介紹了vue-router中query和params的區(qū)別 ,需要的朋友可以參考下2022-10-10
vue+vuex+json-seiver實現(xiàn)數(shù)據(jù)展示+分頁功能
這篇文章主要介紹了vue+vuex+json-seiver實現(xiàn)數(shù)據(jù)展示+分頁功能,非常不錯,具有一定的參考借鑒價值,需要的朋友可以參考下2019-04-04
vue3集成Element-Plus之全局導(dǎo)入和按需導(dǎo)入
這篇文章主要給大家介紹了關(guān)于vue3集成Element-Plus之全局導(dǎo)入和按需導(dǎo)入的相關(guān)資料,element-plus正是element-ui針對于vue3開發(fā)的一個UI組件庫,?它的使用方式和很多其他的組件庫是一樣的,需要的朋友可以參考下2023-07-07
Vue?Router?實現(xiàn)登錄后跳轉(zhuǎn)到之前想要訪問的頁面
這篇文章主要介紹了Vue?Router?實現(xiàn)登錄后跳轉(zhuǎn)到之前相要訪問的頁面,本文僅演示路由跳轉(zhuǎn)和導(dǎo)航守衛(wèi)相關(guān)代碼的實現(xiàn),不包含具體的權(quán)限驗證和登錄請求,需要的朋友可以參考下2022-12-12

