雕虫小技-让你的vue项目支持url缓存

467 阅读2分钟

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 最后将组件组册覆盖原生组件即可;