title: keep-alive:元件級快取 tags:
- keep-alive
- Vue
- vue-router
頁面快取
在Vue構建的單頁面應用(SPA)中,路由模組一般使用vue-router。vue-router不儲存被切換元件的狀態,它進行push或者replace時,舊元件會被銷燬,而新元件會被新建,走一遍完整的生命週期。
但有時候,我們有一些需求,比如跳轉到詳情頁面時,需要保持列表頁的滾動條的深度,等返回的時候依然在這個位置,這樣可以提高使用者體驗。在Vue中,對於這種“頁面快取”的需求,我們可以使用keep-alive元件來解決這個需求。
使用方式
keep-alive是個抽象元件(或稱為功能型元件),實際上不會被渲染在DOM樹中。它的作用是在記憶體中快取元件(不讓元件銷燬),等到下次再渲染的時候,還會保持其中的所有狀態,並且會觸發activated鉤子函式。因為快取的需要通常出現在頁面切換時,所以常與router-view一起出現:
<keep-alive>
<router-view />
</keep-alive>
複製程式碼
如此一來,每一個在router-view中渲染的元件,都會被快取起來。
如果只想渲染某一些頁面/元件,可以使用keep-alive元件的include/exclude屬性。include屬性表示要快取的元件名(即元件定義時的name屬性),接收的型別為string、RegExp或string陣列;exclude屬性有著相反的作用,匹配到的元件不會被快取。假如可能出現在同一router-view的N個頁面中,我只想快取列表頁和詳情頁,那麼可以這樣寫:
<keep-alive :include="['ListView', 'DetailView']">
<router-view />
</keep-alive>
複製程式碼
實現條件快取:全域性的include陣列
上面include的寫法不是常用的,因為它固定了哪幾個頁面快取或不快取,假如有下面這個場景:
- 現有頁面:首頁(A)、列表頁(B)、詳情頁(C),一般可以從:A->B->C;
- B到C再返回B時,B要保持列表滾動的距離;
- B返回A再進入B時,B不需要保持狀態,是全新的。
很明顯,這個例子中,B是“條件快取”的,C->B時保持快取,A->B時放棄快取。其實解決方案也不難,只需要將B動態地從include陣列中增加/刪除就行了。具體步驟是:
- 在Vuex中定義一個全域性的快取陣列,待傳給include:
// global.js
export default {
namespaced: true,
state: {
keepAliveComponents: [] // 快取陣列
},
mutations: {
keepAlive (state, component) {
// 注:防止重複新增(當然也可以使用Set)
!state.keepAliveComponents.includes(component) &&
state.keepAliveComponents.push(component)
},
noKeepAlive (state, component) {
const index = state.keepAliveComponents.indexOf(component)
index !== -1 &&
state.keepAliveComponents.splice(index, 1)
}
}
}
複製程式碼
- 在父頁面中定義keep-alive,並傳入全域性的快取陣列:
// App.vue
<div class="app">
<!--傳入include陣列-->
<keep-alive :include="keepAliveComponents">
<router-view></router-view>
</keep-alive>
</div>
export default {
computed: {
...mapState({
keepAliveComponents: state => state.global.keepAliveComponents
})
}
}
複製程式碼
- 快取:在路由配置頁中,約定使用meta屬性keepAlive,值為true表示元件需要快取。在全域性路由鉤子beforeEach中對該屬性進行處理,這樣一來,每次進入該元件,都進行快取:
const router = new Router({
routes: [
{
path: '/A/B',
name: 'B',
component: B,
meta: {
title: 'B頁面',
keepAlive: true // 這裡指定B元件的快取性
}
}
]
})
router.beforeEach((to, from, next) => {
// 在路由全域性鉤子beforeEach中,根據keepAlive屬性,統一設定頁面的快取性
// 作用是每次進入該元件,就將它快取
if (to.meta.keepAlive) {
store.commit('global/keepAlive', to.name)
}
})
複製程式碼
- 取消快取的時機:對快取元件使用路由的元件層鉤子beforeRouteLeave。因為B->A->B時不需要快取B,所以可以認為:當B的下一個頁面不是C時取消B的快取,那麼下次進入B元件時B就是全新的:
export default {
name: 'B',
created () {
// ...設定滾動條在最頂部
},
beforeRouteLeave (to, from, next) {
// 如果下一個頁面不是詳情頁(C),則取消列表頁(B)的快取
if (to.name !== 'C') {
this.$store.commit('global/noKeepAlive', from.name)
}
next()
}
}
複製程式碼
因為B的條件快取,是B自己的職責,所以最好把該業務邏輯寫在B的內部,而不是A中,這樣不至於讓元件之間的跳轉關係變得混亂。
- 一個需要注意的細節:因為keep-alive元件的include陣列操作的物件是元件名、而不是路由名,因此我們定義每一個元件時,都要顯式宣告name屬性,否則快取不起作用。而且,一個顯式的name對Vue devtools有提示作用。
另一種方式?
網上看到實現條件快取的另一種方式很類似,但它是在父元件中,使用兩個router-view並進行條件渲染:
// App.vue
<div class="app">
<keep-alive>
<router-view v-if="$route.meta.keepAlive"></router-view>
</keep-alive>
<router-view v-if="!$route.meta.keepAlive"></router-view>
</div>
複製程式碼
在GitHub的issue中(見參考連結)有一些爭議,本人也還沒有比較全面的驗證過,所以也暫不知道效果如何。
Q&A
Q:元件名已經加入全域性include陣列了,為什麼頁面還是沒有快取到?
A:如果按照上面的思路來做就沒有問題,注意第5點,即很有可能你忘記給元件宣告一個name屬性了。
Q:元件能夠快取,但是滾動條並沒有快取、比如還是會回到頂部?
A:滾動條這個問題跟元件的HTML結構有關。淺顯而言,keep-alive功能快取的是父元素相對於元件的scrollTop,所以如果你的元件/頁面設定了height:100%、滾動條在元件內部的,看到的滾動條就是沒有快取的。當然,關於這個,還有待進入原始碼深究,給自己留個坑吧。
參考
- 文件
- 應用
- 討論
https://github.com/vuejs/vue-router/issues/811