vue单页缓存方案分析及实现


Posted in Javascript onSeptember 25, 2018

实现全站的页面缓存,前进刷新,返回走缓存,并且能记住上一页的滚动位置,参考了很多技术实现,github上的导航组件实现的原理要么使用的keep-alive,要么参考了keep-alive的源码,但是只用keep-alive没法实现相同path,不同参数展示不同view,这就有点坑了,所以需要结合自己要实现的功能,适当改造keep-alive,为了实现每次前进都能刷新,返回走缓存还能自动定位的功能,文章陆续从以下几个方面展开讲:两套技术方案可选,最后定的技术方案的原因,实现的功能和原理,踩过的坑

方案一:vue的keep-alive组件

 具体使用如下: 

<keep-alive max="10">
  <router-view/>
 </keep-alive>

为什么这么使用?

如vue官网(https://cn.vuejs.org/v2/api/#keep-alive)介绍:

<keep-alive> 包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们。和 <transition> 相似,<keep-alive> 是一个抽象组件:它自身不会渲染一个 DOM 元素,也不会出现在父组件链中。

当组件在 <keep-alive> 内被切换,它的 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 节点。所以对于首次渲染而言,除了在 <keep-alive> 中建立缓存,和普通组件渲染没什么区别。从进入到返回的大致执行流程如下

vue单页缓存方案分析及实现

能实现的功能

能够把要缓存的组件渲染的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上就可以实现前进刷新,返回走缓存,并且可以配置最大缓存数,后续开源到github

最后剩下返回上一页记住上一页的位置,之所以没有用开源的这个组件的记位置,是因为直接套用需要改整体布局,height:100%;样式造成$(windows).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,所以使用单页需要注意全局变量或是事件的谨慎使用,具体的踩坑记录在https://3water.com/article/147957.htm

3.通过push进入下一页时,head里边会累加前面页面的静态资源,访问的页面越多,最后的页面挂的静态的资源越多,返回的时候并不会减少已经加载的静态资源,单页缓存是典型的空间换时间的方案,内存的开销比较大,能否对资源动态增减以及内存占用的优化一直在探索中,暂时没有找到很好的解决方法。。。。。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
JavaScript 异步调用框架 (Part 2 - 用例设计)
Aug 03 Javascript
jquery中post方法用法实例
Oct 21 Javascript
JavaScript中使用Math.PI圆周率属性的方法
Jun 14 Javascript
jQuery.trim() 函数及trim()用法详解
Oct 26 Javascript
AngularJS中处理多个promise的方式
Feb 02 Javascript
浅谈AngularJs指令之scope属性详解
Oct 24 Javascript
AngularJS constant和value区别详解
Feb 28 Javascript
vue解决一个方法同时发送多个请求的问题
Sep 25 Javascript
vue权限问题的完美解决方案
May 08 Javascript
Vue之beforeEach非登录不能访问的实现(代码亲测)
Jul 18 Javascript
Vue中fragment.js使用方法小结
Feb 17 Javascript
JS数组转字符串实现方法解析
Sep 04 Javascript
Vue中对拿到的数据进行A-Z排序的实例
Sep 25 #Javascript
vue单页缓存存在的问题及解决方案(小结)
Sep 25 #Javascript
深入理解与使用keep-alive(配合router-view缓存整个路由页面)
Sep 25 #Javascript
使用vue2.0创建的项目的步骤方法
Sep 25 #Javascript
vue解决一个方法同时发送多个请求的问题
Sep 25 #Javascript
Vue2.0学习系列之项目上线的方法步骤(图文)
Sep 25 #Javascript
在vue中多次调用同一个定义全局变量的实例
Sep 25 #Javascript
You might like
PHP简单检测网址是否能够正常打开的方法
2016/09/04 PHP
基于PHPexecl类生成复杂的报表表头示例
2016/10/14 PHP
Alliance vs AM BO3 第一场2.13
2021/03/10 DOTA
js类 from qq
2006/11/13 Javascript
Ext javascript建立超链接,进行事件处理的实现方法
2009/03/22 Javascript
浅谈Unicode与JavaScript的发展史
2015/01/19 Javascript
jQuery中toggle()函数的使用实例
2015/04/17 Javascript
AngularJS指令中的绑定策略实例分析
2016/12/14 Javascript
JSON与JS对象的区别与对比
2017/03/01 Javascript
分分钟玩转Vue.js组件(二)
2017/03/01 Javascript
简单实现js菜单栏切换效果
2017/03/04 Javascript
小程序实现发表评论功能
2018/07/06 Javascript
jQuery实现的简单歌词滚动功能示例
2019/01/07 jQuery
生产制造追溯系统之再说条码打印
2019/06/03 Javascript
Vue 3.0双向绑定原理的实现方法
2019/10/23 Javascript
jQuery实现弹出层效果
2019/12/10 jQuery
node.js中process进程的概念和child_process子进程模块的使用方法示例
2020/02/11 Javascript
JavaScript arguments.callee作用及替换方案详解
2020/09/02 Javascript
[12:51]71泪洒现场!是DOTA2让经典重现
2014/03/24 DOTA
Python 学习笔记
2008/12/27 Python
python 获取文件列表(或是目录例表)
2009/03/25 Python
Python实现的数据结构与算法之双端队列详解
2015/04/22 Python
浅谈Python的Django框架中的缓存控制
2015/07/24 Python
python进阶_浅谈面向对象进阶
2017/08/17 Python
利用Python-iGraph如何绘制贴吧/微博的好友关系图详解
2017/11/02 Python
python实现发送邮件功能代码
2017/12/14 Python
python矩阵的转置和逆转实例
2018/12/12 Python
python实现证件照换底功能
2019/08/20 Python
LA MER海蓝之谜美国官网:传奇面霜
2016/08/27 全球购物
八项规定整改方案
2014/02/21 职场文书
《浅水洼里的小鱼》听课反思
2014/02/28 职场文书
爱护公共设施的标语
2014/06/24 职场文书
离婚协议书怎么写2014
2014/09/30 职场文书
《攀登者》:“海拔8000米以上,你不能指望任何人”
2019/11/25 职场文书
详解Javascript实践中的命令模式
2021/05/05 Javascript
MySQL中一条update语句是如何执行的
2022/03/16 MySQL