大厂技术  坚持周更  精选好文

在我们的业务中,我们常常会有列表页跳转详情页,详情页可能还会继续跳转下一级页面,下一级页面还会跳转下一级页面,当我们返回上一级页面时,我想保持前一次的所有查询条件以及页面的当前状态。一想到页面缓存,在​​vue​​​中我们就想到​​keep-alive​​​这个​​vue​​​的内置组件,在​​keep-alive​​​这个内置组件提供了一个​​include​​​的接口,只要路由​​name​​匹配上就会缓存当前组件。你或多或少看到不少很多处理这种业务代码,本文是一篇笔者关于缓存多页面的解决实践方案,希望看完在业务中有所思考和帮助。

正文开始...

业务目标

首先我们需要确定需求,假设​​A​​​是列表页,​​A-1​​​是详情页,​​A-1-1​​​,​​A-1-2​​​是详情页的子级页面,​​B​​是其他路由页面

我们用一个图来梳理一下需求


keep-alive多级路由缓存最佳实践_vue

大概就是这样的,一图胜千言

然后我们开始,主页面大概就是下面这样

​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、点击编辑弹框修改数据也是要更新

当我从列表去详情页,我从详情页返回时,此时要缓存当前页的所有数据以及页面状态,那要该怎么做呢?

我们先看下主页面


keep-alive多级路由缓存最佳实践_java_02

大概需求已经明白,其实就是需要缓存条件以及分页状态,还有我展开子树也需要缓存

我的大概思路就是,首先在路由文件的里放入一个标识​​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]

keep-alive多级路由缓存最佳实践_vue_03

参考资料

[1]从官方文档知道:​ https://v2.cn.vuejs.org/v2/api/#keep-alive​

[2]code example:​ https://github.com/maicFir/lessonNote/tree/master/vue/05-keep-alive​

如果想学习更多H5游戏webpacknodegulpcss3javascriptnodeJScanvas数据可视化等前端知识和实战,欢迎在《趣谈前端》加入我们的技术群一起学习讨论,共同探索前端的边界。