vue单页缓存实现方案分析
实现前进刷新,返回不刷新的功能,并且返回时可以记住上一页的滚动位置,有两套方案可选
方案一:vue的keep-alive组件
具体使用如下:
<keep-alive max="10">
<router-view/>
</keep-alive>
为什么这么使用?
如vue官网(https://cn.vuejs.org/v2/api/#keep-alive)介绍:
包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们。和 相似, 是一个抽象组件:它自身不会渲染一个 DOM 元素,也不会出现在父组件链中。
当组件在 内被切换,它的 activated 和 deactivated 这两个生命周期钩子函数将会被对应执行。主要用于保留组件状态或避免重新渲染。
因为缓存的需要通常出现在切换页面时,所以就需要结合vue-router的router-view来实现
为什么keep-alive能实现缓存?
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
}
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[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])
}
如上keep-alive源码,其中render函数是这样实现的,要渲染的试图组件作为插槽内容被获取到,当渲染到路径匹配到的视图组件时会根据vnode存储的内容拿到对应的name,一次将这些组件实例放到变量cache中,这样根据路由就可以找到缓存的vnode,返回给createComponent方法去执行initComponent,vue组件渲染这块的代码如下
function initComponent (vnode, insertedVnodeQueue) {
if (isDef(vnode.data.pendingInsert)) {
insertedVnodeQueue.push.apply(insertedVnodeQueue, vnode.data.pendingInsert)
vnode.data.pendingInsert = null
}
vnode.elm = vnode.componentInstance.$el
if (isPatchable(vnode)) {
invokeCreateHooks(vnode, insertedVnodeQueue)
setScope(vnode)
} else {
// empty component root.
// skip all element-related modules except for ref (#3455) registerRef(vnode)
// make sure to invoke the insert hook insertedVnodeQueue.push(vnode)
}
}
这里会有 vnode.elm 缓存了 vnode 创建生成的 DOM 节点。所以对于首次渲染而言,除了在 中建立缓存,和普通组件渲染没什么区别。
能实现的功能
能够把要缓存的组件渲染的vnode记到cache里边,当返回的时候用缓存里边的dom直接渲染,还有keep-alive组件提供的include 和 exclude属性,可以有条件的缓存想缓存的组件,如果配置了 max 并且缓存的长度超过了这个max的值,还要从缓存中删除第一个
存在的问题
存在的问题是存储vnode节点的key是name,也就是定义路由时组件对应的name,这就会导致同样的path,不同参数的时候打开的是从cache里边拿到的vnode,很多业务场景都是根据参数来显示不同内容,而keep-alive底层并没有对此做扩展,可以看下keep-alive源码
const key: ?string = vnode.key == null
? componentOptions.Ctor.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.key就是路由里边定义的name,所以要用这套方案来实现的根据不同参数展示不同视图的功能就要对这里的key做改造,但是keep-alive是vue自带的,没法改底层,然后就诞生了我的第二套方案
方案二:navigation组件,scrollbehavior
github上找到类似功能的组件vue-navigation,这个vue组件可以实现返回走缓存,底层原理跟keep-alive一样,实际上是改写了keep-alive组件,前进刷新时新增了一个参数VNK,这样在路由发生变化的时候都会用给url带一个参数,并且cache的key取值依赖这个参数,借鉴这个组件的思路,做了一个类似keep-alive的组件,其中key的值是getKey方法获取的,改写以后的render方法如下
render () {
var vnode = this.$slots.default ? this.$slots.default[0] : null
if (vnode) {
vnode.key = vnode.key || (vnode.isComment ? 'comment' : vnode.tag)
const { cache, keys } = this
var key = getKey(this.$route, keyName)
if (vnode.key.indexOf(key) === -1) {
vnode.key = '__navigation-' + key + '-' + vnode.key
}
if (cache[key]) {
if (vnode.key === cache[key].key) {
vnode.componentInstance = cache[key].componentInstance
} else {
cache[key].componentInstance.$destroy()
cache[key] = vnode
}
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
}
getKey方法实现
//url上新增参数vnk的值
export function genKey() {
// const t = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'
const t = 'xxxxxxxx'
return t.replace(/[xy]/g, function (c) {
const r = Math.random() * 16 | 0
const v = c === 'x' ? r : (r & 0x3 | 0x8)
return v.toString(16)
})
}
//
export function getKey(route, keyName) {
return `${route.name || route.path}?${route.query[keyName]}`
}
通过新写一个install方法挂载这个导航组件到vue上就可以实现前进刷新,返回走缓存,并且可以配置最大缓存数,使用如下(后期开源,目前只在内部镜像):
//app.vue
<navigation max="10">
<router-view/>
</navigation>
//入口js
import Navigation from 'component/navigation'
最后剩下返回上一页记住上一页的位置,之所以没有用开源的这个组件的记位置,是因为它的scrollTop是绑定在子元素上,而我的项目之前不是SPA,scroll都是绑在window上的,导致我的项目window上的scrollTop失效,改动较大,还是使用了vue-router提供的scrollBehavior,在路由配置里引入
实现如下:
var scrollBehavior = async (to, from, savedPosition) => {
if (savedPosition) {
return savedPosition
} else {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({ x: 0, y: to.meta.savedPosition || 0 })
}, 300)
})
}
}
const router = new VueRouter({
mode: 'history',
scrollBehavior,
routes: [{
path: '',
redirect: '/mobile/home.html',
meta: {
needMtaReport: true,
parentsStyle: {
height: '100%',
minHeight: '100%'
}
}
},
{
name: 'scienceCompetition',
path: '/mobile/scienceCompetition.html',
component: scienceCompetition
}]
}
总结:
1.单页缓存下js加载解析编译执行的时间缩短了,返回的时候由于走缓存js脚本的占用时间完全可以忽略,从而整体上缩减了页面的加载渲染时间,
2. 因为项目以前不是单页,代码里边定义了很多全局变量或者全局事件绑定,改成单页后全局变量的值依然存在,就会导致业务逻辑出现bug,所以使用单页需要注意全局变量或是事件的谨慎使用
3.通过push进入下一页时,head里边会累加前面页面的静态资源,访问的页面越多,最后的页面挂的静态的资源越多,返回的时候并不会减少已经加载的静态资源,单页缓存是典型的空间换时间的方案,内存的开销比较大,能否对资源动态增减以及内存占用的优化一直在探索中,暂时没有找到很好的解决方法。