vue spa应用中的路由缓存问题与解决方案


Posted in Javascript onMay 31, 2019

单页面应用中的路由缓存问题

通常我们在进行页面前后退时,浏览器通常会帮我们记录下之前滚动的位置,这使得我们不会在每次后退的时候都丢失之前的浏览器记录定位。但是在现在愈发流行的SPA(single page application 单页面应用)中,当我们从父级页面打开子级页面,或者从列表页面进入详情页面,此时如果回退页面,会发现之前我们浏览的滚动记录没有了,页面被置顶到了最顶部,仿佛是第一次进入这个页面一样。这是因为在spa页面中的url与路由容器页面所对应,当页面路径与其发生不匹配时,该页面组件就会被卸载,再次进入页面时,整个组件的生命周期就会完全重新走一遍,包括一些数据的请求与渲染,所以之前的滚动位置和渲染的数据内容也都完全被重置了。

vue中的解决方式

vue.js最贴心的一点就是提供了非常多便捷的API,为开发者考虑到很多的应用场景。在vue中,如果想缓存路由,我们可以直接使用内置的keep-alive组件,当keep-alive包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们。

内置组件keep alive

keep-alive是Vue.js的一个内置组件。它主要用于保留组件状态或避免重新渲染。

使用方法如下:

<keep-alive :include="['a', 'b']">
 <component :is="view"></component>
</keep-alive>

keep-alive组件会去匹配name名称为 'a', 'b' 的子组件,在匹配到以后会帮助组件缓存优化该项组件,以达到组件不会被销毁的目的。

实现原理

先简要看下keep-alive组件内部实现代码,具体代码可以见Vue GitHub

created () {
 this.cache = Object.create(null)
 this.keys = []
}

在created生命周期中会用Object.create方法创建一个cache对象,用来作为缓存容器,保存vnode节点。Tip: Object.create(null)创建的对象没有原型链更加纯净

render () {
 const slot = this.$slots.default
 const vnode: VNode = getFirstComponentChild(slot)
 const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions
 if (componentOptions) {
  // check pattern 检查匹配是否为缓存组件,主要根据include传入的name来对应
  const name: ?string = getComponentName(componentOptions)
  const { include, exclude } = this
  if (
   // not included  该判断中判断不被匹配,则直接返回当前的vnode(虚拟dom)
  (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]) {
   //查看cache对象中已经缓存了该组件,则vnode直接使用缓存中的组件实例
   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])
}

上述代码主要是在render函数中对是否是缓存渲染进行判断

vue keep-alive内部实现的基本流程就是:

  1. 首先通过getFirstComponentChild获取到内部的子组件
  2. 然后拿到该组件的name与keep-alive组件上定义的include与exclude属性进行匹配,
  3. 如果不匹配就表示不缓存组件,就直接返回该组件的vnode(vnode就是一个虚拟的dom树结构,由于原生dom上的属性非常多,消耗巨大,使用这种模拟方式会减少很多dom操作的开销)
  4. 如果匹配到,则在cache对象中查看是否已经缓存过该实例,如果有就直接将缓存的vnode的componentInstance(组件实例)覆盖到目前的vnode上面,否则将vnode存储在cache中。

React中的解决方案

在react中没有提供类似于vue的keep-alive的解决方案,这意味这我们可能需要自己编写一些代码或者通过一些第三方的模块来解决。

在React项目GitHub的该issue中进行了相关讨论,开发维护人员给出了两种方式来解决:

  • 将数据与组件分开缓存。例如,你可以将state提升到一个不会被卸载的父级组件,或者像redux一样将其放在一个侧面缓存中。我们也正在为此开发一类的API支持(context)。
  • 不要去卸载你要“保持活动”的视图,只需使用style={{display:'none'}}属性去隐藏它们。

vue spa应用中的路由缓存问题与解决方案

1. 集中的状态管理恢复快照方式

在React中通过redux或mobx集中的状态管理来缓存页面数据以及滚动条等信息,以达到缓存页面的效果。

componentDidMount() {
 const {app: {dataSoruce = [], scrollTop}, loadData} = this.props;
 if (dataSoruce.length) { //判断redux中是否已经有数据源
  // 有数据则不再加载收据,只恢复滚动状态
  window.scrollTo(0, scrollTop);
 } else { //没有数据就去请求数据源
  this.props.loadData(); // 在redux中定义的数据请求的action
 }
}

handleClik = () => {
 在点击进入下一级页面前先保存当前的滚动距离
 const scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
 const {saveScrollTop} = this.props;
 saveScrollTop(scrollTop);
}

首先我们可以在redux中为页面定义异步的action,将请求回来的数据放入集中的store中(redux的该相关具体用法不在细述)。在sotre里我们可以保存当前页面的数据源、滚动条高度以及其他一些可能要用到的分页数据等来帮助我们恢复状态。

在componentDidMount生命周期里,首先根据redux里store中的对应的字段,判断是否已经加载过数据源。如果已经缓存过数据则不再去请求数据源,只去恢复一下store里的存储过的一些滚动条位置信息等。如果还未请求过数据,就使用在redux中定义的异步action去请求数据,在将数据在reducer里将数据存到store中。 在render函数里,我们只需要读取redux里存储的数据即可。

为了保留要缓存页面的一些状态信息,如滚动条、分页、操作状态,我们可以在进行对应操作时候将这些信息存入redux的store中,这样当我们恢复页面时,就可以将这些对应状态一一读取并还原。

2. 使用display的属性来切换显示隐藏路由组件

想要display的属性来切换显示隐藏路由组件,首先要保证路由组件不会在url变化时候被卸载。在react-router中最使用的Route组件,它可以通过我们定义的path属性来与页面路径来进行匹配,并渲染对应的组件,从而达到保持UI与URL同步变化的效果。

首先简要看下Route组件的实现 GitHub Route.js

return (
 <RouterContext.Provider value={props}>
  {children && !isEmptyChildren(children)
   ? children
   : props.match // props.match 属性来确定是否要渲染组件
    ? component
     ? React.createElement(component, props)
     : render
      ? render(props)
      : null
    : null}
 </RouterContext.Provider>
);

上述代码出现在关键的render方法最后的return中

Route组件会根据props对象中的match属性来确定是否要渲染组件,如果match匹配到了就使用Route组件上传递的component或者render属性来渲染对应组件,否则就返回null。

然后溯源而上,我们找到了props对象中关于match的定义:

const location = this.props.location || context.location;
const match = this.props.computedMatch
 ? this.props.computedMatch // <Switch> already computed the match for us
 : this.props.path
  ? matchPath(location.pathname, this.props)
  : context.match;

const props = { ...context, location, match };

上述代码显示,match首先会从组件的this.props中的computedMatch属性来判断:如果this.props中存在computedMatch则直接使用定义好的computedMatch属性赋值给match,否则如果this.props.path存在,就会使用matchPath方法来根据当前的location.pathname来判断是否匹配。

然而在react router的Route组件API文档中我们似乎没有看到过有关于computedMatch的介绍,不过在源码中有一行这样的注释

// <Switch> already computed the match for us

该注释说在<Switch>组件中已经为我们计算了该匹配。

接下来我们再去了解一下Switch组件:

Switch组件只会渲染第一个被location匹配到的并且作为子元素的<Route>或者<Redirect>

我们翻开Switch组件的实现源码:

let element, match; // 定义最后返回的组件元素,和match匹配变量
 
 React.Children.forEach(this.props.children, child => {
  if (match == null && React.isValidElement(child)) { // 如果match没有内容则进入该判断
   element = child;
 
   const path = child.props.path || child.props.from;
 
   match = path // 该三元表达式只有在匹配到后会给match赋值一个对象,否则match一直为null
    ? matchPath(location.pathname, { ...child.props, path })
    : context.match;
  }
 });
 
 return match
  ? React.cloneElement(element, { location, computedMatch: match })
  : null;

首先我们找到computedMatch属性是在React.cloneElement方法中,cloneElement方法会将追加定义的属性合并到该clone组件元素上,并返回clone后的React组件,等于就是将新的props属性传入组件并返回新组件。

在上文中找到computedMatch的值match也是根据matchPath来判断是否匹配的,matchPath是react router中的一个API,该方法会根据你传入的第一个参数pathname与第二个要匹配的props属性参数来判断是否匹配。如果匹配就返一个对象类型并包含相关的属性,否则返回null。

在React.Children.forEach循环子元素的方法中,matchPath方法判断当前pathname是否匹配,如果匹配就给定义的match变量进行赋值,所以当match被赋值以后,后续的循环就也不会再进行匹配赋值,因为Switch组件只会渲染第一次与之匹配的组件。

3. 实现一个路由缓存组件

我们知道Switch组件只会渲染第一项匹配的子组件,如果可以将匹配到的组件都渲染出来,然后只用display的block和none来切换是否显示,这也就实现了第二种解决方案。

参照Switch组件来封装一个RouteCache组件:

import React from 'react';
import PropTypes from 'prop-types';
import {matchPath} from 'react-router';
import {Route} from 'react-router-dom';

class RouteCache extends React.Component {

 static propTypes = {
  include: PropTypes.oneOfType([
   PropTypes.bool,
   PropTypes.array
  ])
 };

 cache = {}; //缓存已加载过的组件

 render() {
  const {children, include = []} = this.props;

  return React.Children.map(children, child => {
   if (React.isValidElement(child)) { // 验证是否为是react element
    const {path} = child.props;
    const match = matchPath(location.pathname, {...child.props, path});

    if (match && (include === true || include.includes(path))) {
     //如果匹配,则将对应path的computedMatch属性加入cache对象里
     //当include为true时,缓存全部组件,当include为数组时缓存对应组件
     this.cache[path] = {computedMatch: match};
    }

    //可以在computedMatch里追加入一个display属性,可以在路由组件的props.match拿到
    const cloneProps = this.cache[path] && Object.assign(this.cache[path].computedMatch, {display: match ? 'block' : 'none'});

    return <div style={{display: match ? 'block' : 'none'}}>{React.cloneElement(child, {computedMatch: cloneProps})}</div>;
   }

   return null;
  });
 }
}

// 使用
<RouteCache include={['/login', '/home']}>
 <Route path="/login" component={Login} />
 <Route path="/home" component={App} />
</RouteCache>

在阅读了源码后,我们知道Route组件会根据它的this.props.computedMatch来判断是否要渲染该组件。

我们在组件内部创建一个cache对象,将已经匹配到的组件的computedMatch属性写入该缓存对象中。这样即使当url不再匹配时,也能通过读取cache对象中该路径的值,并使用React .cloneElement方法将computedMatch属性赋值给组件的props。这样已缓存过的路由组件就会被一直渲染出来,组件就不会被卸载掉。

因为组件内部可能会包裹多个路由组件,所以使用React.Children.map方法将内部包含的子组件都循环返回。

为了UI与路由对应显示正确,我们通过当前的计算得出的match属性,来隐藏掉不匹配的组件,只为我们展示匹配的组件即可。如果你不想在组件外再套一层div,也可以在组件内部通过this.props.match中的display属性来切换显示组件。

仿照vue keep alive的形式,设置一个 include 参数API。当参数为true时缓存内部的所有子组件,当参数为数组时则缓存对应的path路径组件。

使用效果

vue spa应用中的路由缓存问题与解决方案

在最初时,从未被url匹配过的组件不会被渲染,里面的dom结构是空的。

vue spa应用中的路由缓存问题与解决方案

当切换到对应组件时,当前的组件被渲染,而之前已匹配的组件不会被卸载,只是被隐藏

vue spa应用中的路由缓存问题与解决方案

在输出日志中可以看到,当我们不停的来回切换时,componentDidMount生命周期也只执行一次,在props.match中我们可以获取到当前的display值。

4. 另外的也可以采用一些第三方组件模块来实习缓存机制:

react-keeper
react-router-cache-route
react-live-route

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

Javascript 相关文章推荐
JavaScript基本编码模式小结
May 23 Javascript
JavaScript中的包装对象介绍
Jan 27 Javascript
js实现类似新浪微博首页内容渐显效果的方法
Apr 10 Javascript
JS实现表单中checkbox对勾选中增加边框显示效果
Aug 21 Javascript
关于javascript获取内联样式与嵌入式样式的实例
Jun 01 Javascript
JS检测是否可以访问公网服务器功能代码
Jun 19 Javascript
深入理解vue2.0路由如何配置问题
Jul 18 Javascript
Vue.js 通过jQuery ajax获取数据实现更新后重新渲染页面的方法
Aug 09 jQuery
JQuery样式操作、click事件以及索引值-选项卡应用示例
May 14 jQuery
javascript 对象 与 prototype 原型用法实例分析
Nov 11 Javascript
JS实现页面鼠标点击出现图片特效
Aug 19 Javascript
javascript对象3个属性特征
Nov 17 Javascript
JS实现处理时间,年月日,星期的公共方法示例
May 31 #Javascript
一文了解vue-router之hash模式和history模式
May 31 #Javascript
vue App.vue中的公共组件改变值触发其他组件或.vue页面监听
May 31 #Javascript
微信小程序环境下将文件上传到OSS的方法步骤
May 31 #Javascript
Vue Router history模式的配置方法及其原理
May 30 #Javascript
vue-cli3+ts+webpack实现多入口多出口功能
May 30 #Javascript
详解Vue项目引入CreateJS的方法(亲测可用)
May 30 #Javascript
You might like
一个分页的论坛
2006/10/09 PHP
php下删除字符串中HTML标签的函数
2008/08/27 PHP
php之Memcache学习笔记
2013/06/17 PHP
php计算数组不为空元素个数的方法
2014/01/27 PHP
PHP中使用Memache作为进程锁的操作类分享
2015/03/30 PHP
Thinkphp5框架ajax接口实现方法分析
2019/08/28 PHP
javascript继承之为什么要继承
2012/11/10 Javascript
jQuery.buildFragment使用方法及思路分析
2013/01/07 Javascript
jquery遍历筛选数组的几种方法和遍历解析json对象
2013/12/13 Javascript
js实现的GridView即表头固定表体有滚动条且可滚动
2014/02/19 Javascript
js设置function参数默认值(适合没有传参情况)
2014/02/24 Javascript
JQ技术实现注册页面带有校验密码强度
2015/07/27 Javascript
使用html+js+css 实现页面轮播图效果(实例讲解)
2017/09/21 Javascript
解决Js先触发失去焦点事件再执行点击事件的问题
2018/08/30 Javascript
bootstrap-paginator服务器端分页使用方法详解
2020/02/13 Javascript
15个简单的JS编码标准让你的代码更整洁(小结)
2020/07/16 Javascript
小程序实现左滑删除的效果的实例代码
2020/10/19 Javascript
python 字典(dict)遍历的四种方法性能测试报告
2014/06/25 Python
分享Python开发中要注意的十个小贴士
2016/08/30 Python
利用Python-iGraph如何绘制贴吧/微博的好友关系图详解
2017/11/02 Python
浅谈python配置与使用OpenCV踩的一些坑
2018/04/02 Python
Django-Rest-Framework 权限管理源码浅析(小结)
2018/11/12 Python
对python GUI实现完美进度条的示例详解
2018/12/13 Python
PyQt5 对图片进行缩放的实例
2019/06/18 Python
10个Python面试常问的问题(小结)
2019/11/20 Python
美国畅销的跑步机品牌:ProForm
2017/02/06 全球购物
英国奢华护肤、美容和Spa品牌:Temple Spa
2019/11/02 全球购物
一封普通求职者的求职信
2013/11/20 职场文书
音乐学个人的自荐书范文
2013/11/26 职场文书
幼儿园托班开学寄语
2014/01/18 职场文书
冰淇淋开店创业计划书
2014/02/01 职场文书
我的中国梦演讲稿500字
2014/08/19 职场文书
课程设计的心得体会
2014/09/03 职场文书
2014年仓库管理员工作总结
2014/11/18 职场文书
网络营销计划
2015/01/17 职场文书
2016年6.5世界环境日宣传活动总结
2016/04/01 职场文书