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中Eval函数的使用说明
Oct 11 Javascript
编写Js代码要注意的几条规则
Sep 10 Javascript
javascript中万恶的function实例分析
May 25 Javascript
一个JS的日期格式化算法示例
Jul 31 Javascript
动态加载dtree.js树treeview(示例代码)
Dec 17 Javascript
使用JQ来编写最基本的淡入淡出效果附演示动画
Oct 31 Javascript
jQuery搜索同辈元素方法
Feb 10 Javascript
JavaScript让网页出现渐隐渐显背景颜色的方法
Apr 21 Javascript
浅谈Node.js 中间件模式
Jun 12 Javascript
Vue cli构建及项目打包以及出现的问题解决
Aug 27 Javascript
解决axios会发送两次请求,有个OPTIONS请求的问题
Oct 25 Javascript
Vue.js的模板语法详解
Feb 16 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中feof()函数实例测试
2014/08/23 PHP
thinkphp实现163、QQ邮箱收发邮件的方法
2015/12/18 PHP
php+ajax实现无刷新文件上传功能(ajaxuploadfile)
2018/02/11 PHP
Laravel框架路由和控制器的绑定操作方法
2018/06/12 PHP
PHP实现发送微博消息功能完整示例
2019/12/04 PHP
ExtJs使用IFrame的实现代码
2010/03/24 Javascript
JQuery里选择超链接的实现代码
2011/05/22 Javascript
深入理解JavaScript系列(11) 执行上下文(Execution Contexts)
2012/01/15 Javascript
javascript按位非运算符的使用方法
2013/11/14 Javascript
一款由jquery实现的整屏切换特效
2014/09/15 Javascript
JavaScript实现对下拉列表值进行排序的方法
2015/07/15 Javascript
js实现商城星星评分的效果
2015/12/29 Javascript
jQuery实现微信长按识别二维码功能
2016/08/26 Javascript
ReactJs实现树形结构的数据显示的组件的示例
2017/08/18 Javascript
Angular中管道操作符(|)的使用方法
2017/12/15 Javascript
vue router 用户登陆功能的实例代码
2019/04/24 Javascript
开源一个微信小程序仪表盘组件过程解析
2019/07/30 Javascript
axios如何利用promise无痛刷新token的实现方法
2019/08/27 Javascript
使用 Element UI Table 的 slot-scope方法
2019/10/10 Javascript
[07:49]2014DOTA2国际邀请赛 Newbee夺冠后采访xiao8坦言奖金会上交
2014/07/23 DOTA
Python 中开发pattern的string模板(template) 实例详解
2017/04/01 Python
Python中使用支持向量机(SVM)算法
2017/12/26 Python
flask中主动抛出异常及统一异常处理代码示例
2018/01/18 Python
Python实现绘制双柱状图并显示数值功能示例
2018/06/23 Python
Python将一个CSV文件里的数据追加到另一个CSV文件的方法
2018/07/04 Python
详解Python with/as使用说明
2018/12/13 Python
解决Python找不到ssl模块问题 No module named _ssl的方法
2019/04/29 Python
python实现静态web服务器
2019/09/03 Python
Django框架HttpRequest对象用法实例分析
2019/11/01 Python
PyTorch中的padding(边缘填充)操作方式
2020/01/03 Python
大学生职业规划范文:象牙塔生活的四年计划
2014/01/14 职场文书
清洁工岗位职责
2014/01/29 职场文书
新闻发布会活动策划方案
2014/09/15 职场文书
2014年新农村建设工作总结
2014/12/01 职场文书
入伍志愿书怎么写?
2019/07/19 职场文书
JS一分钟在github+Jekyll的博客中添加访问量功能的实现
2021/04/03 Javascript