vue?不完美的多標(biāo)簽頁存在問題及解決方案
背景
多標(biāo)簽頁多用在中后臺(tái)管理系統(tǒng),能夠讓用戶同時(shí)打開多個(gè)標(biāo)簽頁,而且不會(huì)丟失之前填寫的內(nèi)容,操作起來會(huì)比較方便。雖然部分開源項(xiàng)目有多標(biāo)簽頁的功能,但就體驗(yàn)來看,算不上特別好。
目標(biāo)
- 可以通過router.push實(shí)現(xiàn)打開標(biāo)簽頁
- 同一路由組件可以多開并且數(shù)據(jù)能夠緩存下來
- 不需要處理是否緩存導(dǎo)致的生命周期不一致的問題
- 多標(biāo)簽頁可以關(guān)閉,同時(shí)KeepAlive中的緩存清除
存在的問題
要實(shí)現(xiàn)多標(biāo)簽頁的緩存,最簡單的方法就是用RouterView配合KeepAlive。
<RouterView v-slot="{ Component }">
<KeepAlive>
<component :is="Component" />
</KeepAlive>
</RouterView>然而,這個(gè)方案存在幾個(gè)問題:
- 不能重復(fù)打開同一個(gè)路由,而是原有的組件被激活
- 組件生命周期發(fā)生變化
不能重復(fù)打開路由
如果給路由添加參數(shù),打開第一次沒有任何問題,但如果換另一個(gè)參數(shù)打開,還會(huì)是之前的頁面,因?yàn)榻M件被緩存下來了。
例如:
新增一個(gè)路由 counter,在頁面上添加RouterLink,并使用不同的參數(shù)
<template>
<header>
<img alt="Vue logo" class="logo" src="@/assets/logo.svg" width="125" height="125" />
<div class="wrapper">
<HelloWorld msg="You did it!" />
<nav>
<RouterLink to="/home">Home</RouterLink>
<RouterLink to="/about">About</RouterLink>
<RouterLink to="/counter?id=1">Counter 1</RouterLink>
<RouterLink to="/counter?id=2">Counter 2</RouterLink>
</nav>
</div>
</header>
<RouterView v-slot="{ Component }">
<KeepAlive>
<component :is="Component" />
</KeepAlive>
</RouterView>
</template>然后再Counter組件中獲取id參數(shù),分別點(diǎn)擊Counter 1和Counter 2,會(huì)發(fā)現(xiàn)點(diǎn)擊Counter 1時(shí)獲取到的id是1,點(diǎn)擊Counter 2時(shí)卻沒有任何變化,而且兩個(gè)RouterLink同時(shí)是激活狀態(tài)。

組件生命周期變化
和上一個(gè)問題有所關(guān)聯(lián),因?yàn)榻M件沒有重新加載,在需要重新獲取數(shù)據(jù)時(shí),KeepAlive改變了組件的生命周期,添加了onActivated和onDeactivated生命周期。
添加一個(gè)組件測試生命周期:
<template>
<div class="about">
<h1>This is an about page</h1>
</div>
</template>
<script setup>
import { onMounted, onUpdated, onUnmounted, onBeforeMount, onBeforeUpdate, onBeforeUnmount, onActivated, onDeactivated } from 'vue'
onMounted(() => { console.log("onMounted") })
onUpdated(() => { console.log("onUpdated") })
onUnmounted(() => { console.log("onUnmounted") })
onBeforeMount(() => { console.log("onBeforeMount") })
onBeforeUpdate(() => { console.log("onBeforeUpdate") })
onBeforeUnmount(() => { console.log("onBeforeUnmount") })
onActivated(() => { console.log("onActivated") })
onDeactivated(() => { console.log("onDeactivated") })
</script>
<style>
@media (min-width: 1024px) {
.about {
min-height: 100vh;
display: flex;
align-items: center;
}
}
</style>再修改App.vue
<template>
<header>
<img alt="Vue logo" class="logo" src="@/assets/logo.svg" width="125" height="125" />
<div class="wrapper">
<HelloWorld msg="You did it!" />
<nav>
<RouterLink to="/home">Home</RouterLink>
<RouterLink to="/about">About</RouterLink>
<RouterLink to="/counter?id=1">Counter 1</RouterLink>
<RouterLink to="/counter?id=2">Counter 2</RouterLink>
</nav>
</div>
</header>
<RouterView v-slot="{ Component }">
<!-- <KeepAlive> -->
<component :is="Component" />
<!-- </KeepAlive> -->
</RouterView>
</template>
<script setup>
import { watch } from 'vue'
import { RouterLink, RouterView, useRoute } from 'vue-router'
import HelloWorld from './components/HelloWorld.vue'
const route = useRoute()
watch(route, () => {
console.log("頁面切換", route.fullPath)
})
</script>先從Home切換到About再切換回Home再切換回About。
查看在不使用KeepAlive切換頁面時(shí)候的輸出,onBeforeMount -> onMounted -> onBeforeUnmount -> onUnMounted 循環(huán)

使用KeepAlive的情況,情況就復(fù)雜很多,每次切換到頁面時(shí)會(huì)激活onActivated鉤子,正常情況下可以通過onActivated鉤子獲取路由參數(shù),重新獲取數(shù)據(jù)。
問題在于:如果組件可以在緩存與不緩存中切換,在獲取數(shù)據(jù)時(shí),需要考慮是寫在onMounted里還是onActivated里,寫在onMounted中時(shí)如果組件會(huì)被服用,需要處理路由參數(shù)變化重新獲取數(shù)據(jù);寫在onActivated里,需要考慮組件不緩存了鉤子函數(shù)不會(huì)被調(diào)用的情況。

解決方案
重復(fù)打開組件 & 生命周期變化
這個(gè)問題很好解決,只需要給KeepAlive中的component加上不同的key就可以實(shí)現(xiàn),key可以通過router.fullPath來計(jì)算,這樣KeepAlive中就可以緩存同一個(gè)組件多次。
<RouterView v-slot="{ Component, route }">
<KeepAlive>
<component :is="Component" :key="route.fullPath" />
</KeepAlive>
</RouterView>
同時(shí),修改下Counter組件,查看生命周期
<template>
<div> ID = {{ id }}</div>
</template>
<script setup>
import { useRoute } from 'vue-router'
import { onMounted, onUpdated, onUnmounted, onBeforeMount, onBeforeUpdate, onBeforeUnmount, onActivated, onDeactivated } from 'vue'
const route = useRoute()
const id = route.query.id
onMounted(() => { console.log(route.fullPath, "onMounted") })
onUpdated(() => { console.log(route.fullPath, "onUpdated") })
onUnmounted(() => { console.log(route.fullPath, "onUnmounted") })
onBeforeMount(() => { console.log(route.fullPath, "onBeforeMount") })
onBeforeUpdate(() => { console.log(route.fullPath, "onBeforeUpdate") })
onBeforeUnmount(() => { console.log(route.fullPath, "onBeforeUnmount") })
onActivated(() => { console.log(route.fullPath, "onActivated") })
onDeactivated(() => { console.log(route.fullPath, "onDeactivated") })
</script>會(huì)發(fā)現(xiàn),雖然是同一個(gè)組件,但生命周期也獨(dú)立了,也就不需要考慮路由參數(shù)變化時(shí)重新獲取數(shù)據(jù),只需要在onMounted時(shí)獲取一次數(shù)據(jù)就可以了。

關(guān)閉標(biāo)簽頁
上面的問題好像一下就解決了,但第三個(gè)目標(biāo)沒有實(shí)現(xiàn),這也是最難的一個(gè)問題。
KeepAlive可以通過給component添加不同的key達(dá)到路由多開的效果,但是卻不能用key刪除,KeepAlive只能通過exclude參數(shù)使用組件名稱刪除緩存。
這下問題麻煩了,雖然使用不同的key多開了路由,但路由的組件名稱是相同的,也就是說,就算能多開了,關(guān)閉卻只能全部關(guān)閉,這種是不行的。
思索后,想到了下面的方案:
不使用KeepAlive,通過監(jiān)聽route,變化后就向list中添加達(dá)到打開標(biāo)簽頁的功能,渲染list中的所有組件,然后為了讓組件數(shù)據(jù)緩存下來,不能使用v-if而是使用v-show來隱藏組件。
驗(yàn)證方案
監(jiān)聽route,將訪問過的路由都保存下來作為打開過的標(biāo)簽頁,當(dāng)前route作為激活的標(biāo)簽頁
編寫一個(gè)TagView組件,替代RouterView+KeepAlive,關(guān)閉的時(shí)候直接刪除tagView就可以
<template>
<div class="tags">
<div class="tag" v-for="tagView in tagViews" :class="{ active: tagView.key === currentTagView?.key }"
@click="router.push(tagView.route)">
{{ tagView.title }}</div>
</div>
<div class="content">
<template v-for="tagView in tagViews" :key="tagView.key">
<Component :is="tagView.component" v-show="tagView.key === currentTagView.key" />
</template>
</div>
</template>
<script setup>
import { inject, ref, shallowRef, toValue, watch } from 'vue'
import { useRoute, useRouter, viewDepthKey } from 'vue-router'
const route = useRoute()
const router = useRouter()
const tagViews = ref([])
const currentTagView = ref(null)
// 參考了vue官方的RouterView, 是RouterView嵌套的深度
const routerViewDepth = inject(viewDepthKey, 0)
const routeKey = (route) => {
return route.fullPath
}
const routeTitle = (route) => {
// 還沒有設(shè)計(jì)title,先用fullPath替代
return route.fullPath
}
const toTagView = (route) => {
const depth = toValue(routerViewDepth)
return {
title: routeTitle(route),
key: routeKey(route),
route: { ...route },
component: shallowRef(route.matched[depth]?.components['default'])
}
}
watch(route, () => {
// 判斷是否已存在,存在則不添加
const key = routeKey(route)
let tagView = tagViews.value.find(tagView => tagView.key === key)
if (!tagView) {
tagView = toTagView(route)
tagViews.value.push(tagView)
}
currentTagView.value = tagView
})
</script>
<style scoped>
.tags {
gap: 8px;
padding: 4px;
display: flex;
border: 1px solid #ccc;
}
.tag {
padding: 4px 12px;
border: 1px solid #ccc;
}
.tag.active {
color: #fff;
background-color: #409EFF;
}
</style>然后在App.vue中使用
<template>
<div class="left-menu">
<RouterLink to="/counter?id=1">Counter 1</RouterLink>
<RouterLink to="/counter?id=2">Counter 2</RouterLink>
</div>
<div class="right-content">
<TagView />
</div>
</template>
<script setup>
import { watch } from 'vue'
import TagView from './components/TagView.vue'
import { RouterLink, useRoute } from 'vue-router'
const route = useRoute()
watch(route, () => {
console.log("頁面切換", route.fullPath)
})
</script>
<style scoped>
.left-menu {
display: flex;
padding: 8px;
width: 220px;
border: 1px solid #ccc;
flex-direction: column;
}
.right-content {
flex: 1;
padding: 8px;
}
</style>樣式隨便寫的,明白意思就好。
可以自由切換標(biāo)簽頁,并且填寫的內(nèi)容依然保留。

優(yōu)點(diǎn):編寫起來很簡單
缺點(diǎn):之前的組件一直保留,打開的頁面多了可能會(huì)卡
總結(jié):也算一種可行的方案,但要注意頁面不能太多

之前的組件只是display: none了
可能是優(yōu)化
上面其實(shí)解決了最大的問題,但是還可以優(yōu)化一下,可以利用KeepAlive卸載dom并緩存。
基于上面的方案,在Component外面再套一層KeepAlive,然后將v-show改成v-if。
<template>
<div class="tags">
<div class="tag" v-for="tagView in tagViews" :class="{ active: tagView.key === currentTagView?.key }"
@click="router.push(tagView.route)">
{{ tagView.title }}</div>
</div>
<div class="content">
<template v-for="tagView in tagViews" :key="tagView.key">
<KeepAlive>
<Component :is="tagView.component" v-if="tagView.key === currentTagView.key" />
</KeepAlive>
</template>
</div>
</template>

這樣就解決了打開頁面太多可能會(huì)導(dǎo)致的性能問題,但是在DevTool中就會(huì)看到很多個(gè)KeepAlive了,這也是一種取舍吧。
總結(jié)
上面的解決方案并不完美,要么容易影響性能,要么可能會(huì)影響開發(fā)(多個(gè)KeepAlive在DevTool里),要完美的話估計(jì)只能自己實(shí)現(xiàn)一個(gè)KeepAlive了。
到此這篇關(guān)于vue 不完美的多標(biāo)簽頁存在問題及解決方案的文章就介紹到這了,更多相關(guān)vue 多標(biāo)簽頁內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Vue $router.push打開新窗口的實(shí)現(xiàn)方法
在Vue中,$router.push方法默認(rèn)不支持在新窗口中打開頁面,但通過結(jié)合window.open方法和$router.resolve方法,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2024-09-09
一文掌握Pinia使用及數(shù)據(jù)持久化存儲(chǔ)超詳細(xì)教程
這篇文章主要介紹了Pinia安裝使用及數(shù)據(jù)持久化存儲(chǔ)的超詳細(xì)教程,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2023-07-07
vue3+electron12+dll開發(fā)客戶端配置詳解
本文將結(jié)合實(shí)例代碼,介紹vue3+electron12+dll客戶端配置,對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-06-06
vue結(jié)合vue-electron創(chuàng)建應(yīng)用程序小結(jié)
這篇文章主要介紹了vue結(jié)合vue-electron創(chuàng)建應(yīng)用程序,本文給大家介紹了安裝electron有兩種方式,兩種方式創(chuàng)建的項(xiàng)目結(jié)構(gòu)大不相同,需要的朋友可以參考下2024-03-03
vue給input file綁定函數(shù)獲取當(dāng)前上傳的對(duì)象完美實(shí)現(xiàn)方法
這篇文章主要介紹了vue給input file綁定函數(shù)獲取當(dāng)前上傳的對(duì)象完美實(shí)現(xiàn)方法,需要的朋友可以參考下2017-12-12
vue-router beforeEach跳轉(zhuǎn)路由驗(yàn)證用戶登錄狀態(tài)
這篇文章主要介紹了vue-router beforeEach跳轉(zhuǎn)路由驗(yàn)證用戶登錄狀態(tài),小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2018-12-12
vue-manage-system升級(jí)到vue3的開發(fā)總結(jié)分析
這篇文章主要為大家介紹了vue-manage-system升級(jí)到vue3的開發(fā)總結(jié)分析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-09-09
Vuex模塊化實(shí)現(xiàn)待辦事項(xiàng)的狀態(tài)管理
本文主要介紹了Vuex模塊化實(shí)現(xiàn)待辦事項(xiàng)的狀態(tài)管理的相關(guān)知識(shí),具有很好的參考價(jià)值,下面跟著小編一起來看下吧2017-03-03

