大廠技術 堅持周更 精選好文
在我們的業務中,我們常常會有清單頁跳轉詳情頁,詳情頁可能還會繼續跳轉下一級頁面,下一級頁面還會跳轉下一級頁面,當我們傳回上一級頁面時,我想保持前一次的所有查詢條件以及頁面的目前狀态。一想到頁面緩存,在
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 class="list-app">
<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"
style="width: 100%"
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 id="app">
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