keep-alive 多級路由緩存最佳實踐
在我們的業務中,我們常常會有列表頁跳轉詳情頁,詳情頁可能還會繼續跳轉下一級頁面,下一級頁面還會跳轉下一級頁面,當我們返回上一級頁面時,我想保持前一次的所有查詢條件以及頁面的當前狀態。一想到頁面緩存,在vue
中我們就想到keep-alive
這個vue
的內置組件,在keep-alive
這個內置組件提供了一個include
的接口,只要路由name
匹配上就會緩存當前組件。你或多或少看到不少很多處理這種業務代碼,本文是一篇筆者關於緩存多頁面的解決實踐方案,希望看完在業務中有所思考和幫助。
正文開始...
業務目標
首先我們需要確定需求,假設A
是列表頁,A-1
是詳情頁,A-1-1
,A-1-2
是詳情頁的子級頁面,B
是其他路由頁面
我們用一個圖來梳理一下需求
大概就是這樣的,一圖勝千言
然後我們開始,主頁面大概就是下面這樣
pages/list/index.vue
我們暫且把這個當成A
頁面模塊吧
<template>
<div>
<div><a href="javascript:void(0)" @click="handleToHello">to hello</a></div>
<el-form ref="form" :model="condition" label-width="80px" inline>
<el-form-item label="姓名">
<el-input
v-model="condition.name"
clearable
placeholder="請輸入搜索姓名"
></el-input>
</el-form-item>
<el-form-item label="地址">
<el-select v-model="condition.address" placeholder="請選擇地址">
<el-option
v-for="item in tableData"
:key="item.name"
:label="item.address"
:value="item.address"
>
</el-option>
</el-select>
</el-form-item>
<el-form-item>
<el-button @click="featchList">刷新</el-button>
</el-form-item>
</el-form>
<el-table
:data="tableData"
row-key="id"
border
lazy
:load="load"
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
>
<el-table-column prop="date" label="日期"> </el-table-column>
<el-table-column prop="name" label="姓名"> </el-table-column>
<el-table-column prop="address" label="地址"> </el-table-column>
<el-table-column prop="options" label="操作">
<template slot-scope="scope">
<a href="javascript:void(0);" @click="handleView">查看詳情</a>
<a href="javascript:void(0);" @click="handleEdit(scope.row)">編輯</a>
</template>
</el-table-column>
</el-table>
<!--分頁-->
<el-pagination
@current-change="handleChangePage"
background
layout="prev, pager, next"
:total="100"
>
</el-pagination>
<!--彈框-->
<list-modal
title="編輯"
width="50%"
v-model="formParams"
:visible.sync="dialogVisible"
@refresh="featchList"
></list-modal>
</div>
</template>
我們再看下對應頁面的業務js
<!--pages/list/index.vue-->
<script>
import { sourceDataMock } from '@/mock';
import ListModal from './ListModal';
export default {
name: 'list',
components: {
ListModal,
},
data() {
return {
tableData: [],
cacheData: [], // 緩存數據
condition: {
name: '',
address: '',
page: 1,
},
dialogVisible: false,
formParams: {
date: '',
name: '',
address: '',
},
};
},
watch: {
// eslint-disable-next-line func-names
'condition.name': function (val) {
if (val === '') {
this.tableData = this.cacheData;
} else {
this.tableData = this.cacheData.filter(v => v.name.indexOf(val) > -1);
}
},
},
created() {
this.featchList();
},
methods: {
handleToHello() {
this.$router.push('/hello-world');
},
handleChangePage(val) {
this.condition.page = val;
this.featchList();
},
handleSure() {
this.dialogVisible = false;
},
load(tree, treeNode, resolve) {
setTimeout(() => {
resolve(sourceDataMock().list);
}, 1000);
},
handleView() {
this.$router.push('/detail');
},
handleEdit(row) {
this.formParams = { ...row };
this.dialogVisible = true;
console.log(row);
},
featchList() {
console.log('----start load data----', this.condition);
const list = sourceDataMock().list;
// 深拷貝一份數據
this.cacheData = JSON.parse(JSON.stringify(list));
this.tableData = list;
},
},
};
</script>
以上業務代碼主要做了以下幾件事情
1、用mockjs
模擬了一份列表數據
2、根據條件篩選對應的數據,分頁操作
3、從當前頁面跳轉子頁面,或者跳轉其他頁面,還有打開編輯彈框
首先我們要確認幾個問題,當前頁面的幾個特殊條件:
1、當前頁面的條件變化,頁面要更新
2、分頁器切換,頁面就需要更新
3、點擊編輯彈框修改數據也是要更新
當我從列表去詳情頁,我從詳情頁返回時,此時要緩存當前頁的所有數據以及頁面狀態,那要該怎麼做呢?
我們先看下主頁面
大概需求已經明白,其實就是需要緩存條件以及分頁狀態,還有我展開子樹也需要緩存
我的大概思路就是,首先在路由文件的裏放入一個標識cache
,這個cache
裝載的就是當前的路由name
import Vue from 'vue';
import Router from 'vue-router';
import HelloWorld from '@/components/HelloWorld';
import List from '@/pages/list';
import Detail from '@/pages/detail';
Vue.use(Router);
export default new Router({
routes: [
{
path: '/hello-world',
name: 'HelloWorld',
component: HelloWorld,
},
{
path: '/',
name: 'list',
component: List,
meta: {
cache: ['list'],
},
},
{
path: '/detail',
name: 'detail',
component: Detail,
meta: {
cache: [],
},
},
],
});
然後我們在App.vue
中的router-view
中加入keep-alive
,並且include
指定對應路由頁面
<template>
<div>
cache Page:{{ cachePage }}
<keep-alive :include="cachePage">
<router-view />
</keep-alive>
</div>
</template>
我們看下cachePage
是從哪裏來的, 我們通常把這種公用的變量放在全局store
中管理
import store from '@/store';
export default {
name: 'App',
computed: {
cachePage() {
return store.state.global.cachePage;
},
},
};
當我們進入這個頁面時就要根據路由上設置的meta
去確認當前頁面是否有緩存的name
,所以本質上也就成了,我如何設置keep-alive
中的include
值
import store from '@/store';
export default {
...
methods: {
cacheCurrentRouter() {
const { meta } = this.$route;
if (meta) {
if (meta.cache) {
store.commit('global/setGlobalState', {
cachePage: [
...new Set(store.state.global.cachePage.concat(meta.cache)),
],
});
} else {
store.commit('global/setGlobalState', {
cachePage: [],
});
}
}
},
},
created() {
this.cacheCurrentRouter();
this.$watch('$route', () => {
this.cacheCurrentRouter();
});
},
};
我們注意到,我們是根據$route
的meta.cache
然後去修改store
中的cachePage
的
然後我們去store/index.js
看下
// store/index.js
import Vue from 'vue';
import Vuex from 'vuex';
import { gloablMoudle } from './modules';
Vue.use(Vuex);
const initState = {};
const store = new Vuex.Store({
state: initState,
modules: {
global: gloablMoudle,
},
});
export default store;
我們繼續找到最終設置cachePage
的modules/global/index.js
// modules/global/index.js
export const gloablMoudle = {
namespaced: true,
state: {
cachePage: [],
},
mutations: {
setGlobalState(state, payload) {
Object.keys(payload).forEach((key) => {
if (Reflect.has(state, key)) {
state[key] = payload[key];
}
});
},
},
};
所以我們可以看到mutations
有這樣的一段設置state
的操作setGlobalState
這塊代碼可以給大家分享下,爲什麼我要循環payload
獲取對應的key
, 然後再從state
中判斷是否有key
,最後再賦值?
在業務中我們看到不少這樣的代碼
export const gloablMoudle = {
namespaced: true,
state: {
a: [],
b: []
},
mutations: {
seta(state, payload) {
state.a = payload
},
setb(state, payload) {
state.b = payload
},
...
},
actions: {
actA({commit, state}, payload) {
commit('seta', payload)
},
actB({commit, state}, payload) {
commit('setb', payload)
}
...
}
...
};
在具體業務中大概就下面這樣
store.dispatch('actA', {})
store.dispatch('actB', {})
所以你會看到如此重複的代碼,寫多了,貌似會越來越多,有沒有可以一勞永逸呢?
因此上面一塊代碼,你可以優化成下面這樣
export const gloablMoudle = {
namespaced: true,
state: {
a: [],
b: []
},
mutations: {
setState(state, payload) {
Object.keys(payload).forEach(key => {
if (Reflect.has(state, key)) {
state[key] = payload[key]
}
})
},
},
actions: {
setActionState({commit, state}, payload) {
commit('setState', payload)
}
}
};
在業務代碼裏你就這樣做
store.dispatch('setActionState', {a: [1,2,3]})
store.dispatch('setActionState', {b: [1,2,3]})
或者是下面這樣
store.commit('setState', {a: [1,2,3]})
store.commit('setState', {b: [1,2,3]})
所以你會看到我這個文件會非常的小,同樣達到目的,而且維護成本會降低很多,達到了我們代碼設計的高內聚,低耦合,一勞永逸的抽象思想。
回到正題,我們已經設置的全局store
的cachePage
我們注意到在created
裏面我們除了有去更新cachePage
,還有去監聽路由的變化, 當我們切換路由去詳情頁面,我們是要根據路由標識更新cachePage
的。
import store from '@/store';
export default {
...
methods: {
cacheCurrentRouter() {
const { meta } = this.$route;
if (meta) {
if (meta.cache) {
store.commit('global/setGlobalState', {
cachePage: [
...new Set(store.state.global.cachePage.concat(meta.cache)),
],
});
} else {
store.commit('global/setGlobalState', {
cachePage: [],
});
}
}
},
},
created() {
this.cacheCurrentRouter();
// 監聽路由,根據路由判斷當前是否應該要緩存
this.$watch('$route', () => {
this.cacheCurrentRouter();
});
},
};
當我們從當前頁面切換到tohello
頁面時,再回來,當前頁面就會重新被激活,然後重新再次緩存
如果我需要detial/index.vue
也需要緩存,那麼我只需要在路由文件新增當前路由名稱即可
export default new Router({
routes: [
{
path: '/hello-world',
name: 'HelloWorld',
component: HelloWorld,
},
{
path: '/',
name: 'list',
component: List,
meta: {
cache: ['list'],
},
},
{
path: '/detail',
name: 'detail',
component: Detail,
meta: {
cache: ['detail'], // 這裏的名稱就是當前路由的名稱
},
},
],
});
所以無論多少級頁面,跳轉哪些頁面,都可以輕鬆做到緩存,而且核心代碼非常簡單
keep-alive 揭祕
最後我們看下vue
中這個內置組件keep-alive
有什麼特徵,以及他是如何實現緩存路由組件的
從官方文檔知道 [1],當一個組件被keep-alive
緩存時
1、該組件不會重新渲染
2、不會觸發created
,mounted
鉤子函數
3、提供了一個可觸發的鉤子函數activated
函數【當前組件緩存時會激活該鉤子】
4、deactivated
離開當前緩存組件時觸發
我們注意到keep-alive
提供了 3 個接口props
-
include, 被匹配到的路由組件名(注意必須時組件的
name
) -
exclude,排序不需要緩存的組件
-
max 提供最大緩存組件實例,設置這個可以限制緩存組件實例
不過我們注意,keep-alive
並不能緩在函數式組件裏使用,也就是是申明的純函數組件
不會有作用
我們看下keep-alive
這個內置組件是怎麼緩存組件的
在vue2.0
源碼目錄裏看到/core/components/keep-alive.js
首先我們看到,在created
鉤子裏綁定了兩個變量cache
,keys
created () {
this.cache = Object.create(null)
this.keys = []
},
然後我們會看到有在mounted
和updated
裏面有去調用cacheVNode
...
mounted () {
this.cacheVNode()
this.$watch('include', val => {
pruneCache(this, name => matches(val, name))
})
this.$watch('exclude', val => {
pruneCache(this, name => !matches(val, name))
})
},
我們可以看到首先在mounted
裏就是cacheVNode()
,然後就是監聽props
的變化
methods: {
cacheVNode() {
const { cache, keys, vnodeToCache, keyToCache } = this
if (vnodeToCache) {
const { tag, componentInstance, componentOptions } = vnodeToCache
cache[keyToCache] = {
name: getComponentName(componentOptions),
tag,
componentInstance,
}
keys.push(keyToCache)
// prune oldest entry
if (this.max && keys.length > parseInt(this.max)) {
pruneCacheEntry(cache, keys[0], keys, this._vnode)
}
this.vnodeToCache = null
}
}
},
上面一段代碼大的大意就是,如果有vnodeToCache
存在,那麼就會將組件添加到cache
對象中,並且如果有max
,則會對多餘的組件進行銷燬
在render
裏,我們看到會獲取默認的slot
, 然後會根據slot
獲取根組件
首先會判斷路由根組件上的是否有name
, 沒有就不緩存,直接返回vnode
render () {
const slot = this.$slots.default
const vnode: VNode = getFirstComponentChild(slot)
const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions
if (componentOptions) {
// check pattern
const name: ?string = getComponentName(componentOptions)
const { include, exclude } = this
if (
// not included
(include && (!name || !matches(include, name))) ||
// excluded
(exclude && name && matches(exclude, name))
) {
return vnode
}
...
}
當再次訪問時,就會從當前緩存對象裏去找, 直接執行
vnode.componentInstance = cache[key].componentInstance
, 組件實例會從cache
對象中尋找
render () {
const slot = this.$slots.default
const vnode: VNode = getFirstComponentChild(slot)
const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions
const { cache, keys } = this
const key: ?string = vnode.key == null
// same constructor may get registered as different local components
// so cid alone is not enough (#3269)
? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
: vnode.key
if (cache[key]) {
// vnode.componentInstance 從cache對象中尋找
vnode.componentInstance = cache[key].componentInstance
// make current key freshest
remove(keys, key)
// 在刪除的時候會有用到keys
keys.push(key)
} else {
// delay setting the cache until update
this.vnodeToCache = vnode
this.keyToCache = key
}
vnode.data.keepAlive = true
}
return vnode || (slot && slot[0])
}
總結
-
keep-alive
緩存多級路由,主要思路根據路由的meta
標識,然後在App.vue
組件中keep-alive
包裹router-view
路由標籤,我們通過全局store
變量去控制includes
判斷當前路由是否該被緩存,同時需要監聽路由判斷是否有需要緩存,通過設置全局cachePage
去控制路由的緩存 -
優化
store
數據流代碼,可以減少代碼,提高的代碼模塊的複用度 -
當一個組件被緩存時,加載該緩存組件時是會觸發
activated
鉤子,當從一個緩存組件離開時,會觸發deactivated
,在特殊場景可以在這兩個鉤子函數上做些事情 -
簡略剖析
keep-alive
實現原理, 從默認插槽中獲取組件實例,然後會根據是否有name
,include
以及exclude
,判斷是否每次返回vnode
, 如果include
有需要緩存的組件,則會從cache
對象中獲取實例對vnode.componentInstance
進行重新賦值優先從緩存對象中獲取 -
本文示例 code example[2]
參考資料
[1] 從官方文檔知道: https://v2.cn.vuejs.org/v2/api/#keep-alive
[2]code example: https://github.com/maicFir/lessonNote/tree/master/vue/05-keep-alive
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/bQTk2v_yqqjlxABBAonrrw