vue有个keep-alive组件可以支持路由缓存。
<keep-alive :include="cachedRoute">
<router-view :key="$route.fullPath" />
</keep-alive>
keep-alive 有两个props :include 和 exclude,可以更细粒度的控制要缓存的组件
// 处理include 和 exclude部分的源码
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
}
这种细粒度的控制只支持组件名的缓存。想根据不同地址缓存使用原生的组件不太好实现; 只要将原生keep-alive稍加改造就可以实现根据url地址缓存;只需要修改下面的代码即可:
// 改前
const name: ?string = getComponentName(componentOptions)
// 改后
const name: ?string = vnode.key
这样我们就可以通过 router-view 上挂载的key = $route.fullPath 实现路径级别的路由缓存; 在vuex 里管理cachedRoute ,根据业务需求对 cachedRoute 进行控制,就可以为所欲为的控制要缓存的路径。特别适用使用了tab 模式切换路由的,类似这种风格的(某大佬开源的vue-element-admin系统):panjiachen.gitee.io/vue-element…
具体改造方法: 重写keep-alive组件;主要修改的部分在render函数里,我们可以通过mixins混入原生keep-alive组件,重写render函数即可;
export default Vue.extend({
name: "keep-alive",
mixins: [Vue.component("KeepAlive")],
props: {
useKey: {
type: Boolean,
default: false,
},
},
abstract: true,
render() {
//
}
}
全部代码(ts版)
import { VNode, VNodeComponentOptions } from "vue";
import Vue from "vue";
const _toString = Object.prototype.toString;
type VNodeCache = { [key: string]: VNode };
function getComponentName(opts: VNodeComponentOptions): string {
return opts && ((opts.Ctor as any).options.name || opts.tag);
}
export function remove(arr: any[], item): any[] | void {
if (arr.length) {
const index = arr.indexOf(item);
if (index > -1) {
return arr.splice(index, 1);
}
}
}
export function isRegExp(v): boolean {
return _toString.call(v) === "[object RegExp]";
}
function matches(pattern: string | RegExp | Array<string>, name: string): boolean {
if (Array.isArray(pattern)) {
return pattern.indexOf(name) > -1;
} else if (typeof pattern === "string") {
return pattern.split(",").indexOf(name) > -1;
} else if (isRegExp(pattern)) {
return pattern.test(name);
}
/* istanbul ignore next */
return false;
}
function pruneCacheEntry(cache: VNodeCache, key: string, keys: Array<string>, current?: VNode) {
const cached = cache[key];
if (cached && (!current || cached.tag !== current.tag)) {
cached.componentInstance.$destroy();
}
cache[key] = null;
remove(keys, key);
}
function pruneCache(keepAliveInstance: any, filter: Function, useKey) {
const { cache, keys, _vnode } = keepAliveInstance;
for (const key in cache) {
const cachedNode: VNode = cache[key];
if (cachedNode) {
const name = useKey ? cachedNode.key : getComponentName(cachedNode.componentOptions);
if (name && !filter(name)) {
pruneCacheEntry(cache, key, keys, _vnode);
}
}
}
}
export function isAsyncPlaceholder(node: VNode): boolean {
return node.isComment && (node as any).asyncFactory;
}
export function isDef(v): boolean {
return v !== undefined && v !== null;
}
export function getFirstComponentChild(children: Array<VNode>): VNode {
if (Array.isArray(children)) {
for (let i = 0; i < children.length; i++) {
const c = children[i];
if (isDef(c) && (isDef(c.componentOptions) || isAsyncPlaceholder(c))) {
return c;
}
}
}
}
const cmpt = Vue.component("KeepAlive");
delete (cmpt as any).mounted;
export default Vue.extend({
name: "keep-alive",
mixins: [cmpt],
props: {
useKey: {
type: Boolean,
default: false,
},
},
abstract: true,
mounted() {
this.$watch("include", (val) => {
pruneCache(this, (name) => matches(val, name), this.useKey);
});
this.$watch("exclude", (val) => {
pruneCache(this, (name) => !matches(val, name), this.useKey);
});
},
render() {
const slot = this.$slots.default;
const vnode: VNode = getFirstComponentChild(slot);
const componentOptions: VNodeComponentOptions = vnode && vnode.componentOptions;
if (componentOptions) {
// check pattern
const name: string = this.useKey ? (vnode.key as string) : getComponentName(componentOptions); // getComponentName(componentOptions);
const { include, exclude } = this;
console.error(name);
if ((include && (!name || !matches(include, name))) || (exclude && name && matches(exclude, name))) {
console.error(name);
return vnode;
}
console.error(name);
const { cache, keys } = this;
// same constructor may get registered as different local components
// so cid alone is not enough (#3269)
const key =
vnode.key == null
? (componentOptions.Ctor as any).cid + (componentOptions.tag ? `::${componentOptions.tag}` : "")
: vnode.key;
if (cache[key]) {
vnode.componentInstance = cache[key].componentInstance;
// make current key freshest
remove(keys, key);
keys.push(key);
} else {
cache[key] = vnode;
keys.push(key);
// prune oldest entry
if (this.max && keys.length > parseInt(this.max)) {
pruneCacheEntry(cache, keys[0], keys, this._vnode);
}
}
vnode.data.keepAlive = true;
}
return vnode || (slot && slot[0]);
},
});
说明:
1源码中使用了很多的工具函数,这些函数都没有暴露出api给用户用,在修改render的过程中需要使用到部分工具函数,可以使用源码导入(见下);
import { isRegExp, remove } from 'vue/src/shared/util'
import { getFirstComponentChild } from 'vue/src/core/vdom/helpers/index'
2 导入源码可能要重新配置webpack打包,源码使用了@flow类型注解,需要添加相关loader。我选择把需要的工具函数copy到自己源码中,改造完100行左右的代码体积不是很大;
3 mixins: [Vue.component("KeepAlive")]可以避免我们复制整个组件进行改造,缩减依赖和体积;
4 增加useKey 扩展修改,不干扰原有功能;
5 删除原mounted 重新监听include 和 exclude,处理失效的缓存
6 最后将组件组册覆盖原生组件即可;