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 文章截取部分无损html显示实现代码
May 04 Javascript
JS特权方法定义作用以及与公有方法的区别
Mar 18 Javascript
js弹出层包含flash 不能关闭隐藏的2种处理方法
Jun 17 Javascript
js与jquery获取父元素,删除子元素的两种不同方法
Jan 09 Javascript
深入理解Ajax的get和post请求
Jun 02 Javascript
Node.js中sequelize时区的配置方法
Dec 10 Javascript
Vue官网todoMVC示例代码
Jan 29 Javascript
vue+axios新手实践实现登陆的示例代码
Jun 06 Javascript
JavaScript实现异步图像上传功能
Jul 12 Javascript
一步快速解决微信小程序中textarea层级太高遮挡其他组件
Mar 04 Javascript
webgl实现物体描边效果的方法介绍
Nov 27 Javascript
Vue 请求传公共参数的操作
Jul 31 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
微信自定义菜单的处理开发示例
2015/04/16 PHP
PHP获取网页所有连接的方法(附demo源码下载)
2016/03/30 PHP
php文件上传类完整实例
2016/05/14 PHP
PHP实现json_decode不转义中文的方法
2017/05/20 PHP
php二维数组按某个键值排序的实例讲解
2019/02/15 PHP
JavaScript 的继承
2011/10/01 Javascript
javascript中的缓动效果实现程序
2012/12/29 Javascript
javascript中String对象的slice()方法分析
2014/12/20 Javascript
JavaScript中isPrototypeOf函数作用和使用实例
2015/06/01 Javascript
Bootstrap显示与隐藏简单实现代码
2017/03/06 Javascript
详解nodejs微信公众号开发——6.自定义菜单
2017/04/13 NodeJs
JS实现简单的选择题测评系统代码思路详解(demo)
2017/09/03 Javascript
angular-tree-component的使用详解
2018/07/30 Javascript
Angular服务Request异步请求的实例讲解
2018/08/13 Javascript
js canvas实现写字动画效果
2018/11/30 Javascript
JavaScript学习笔记之DOM基础操作实例小结
2019/01/09 Javascript
JavaScript实现模态对话框实例
2020/01/13 Javascript
JS实现滑动拼图验证功能完整示例
2020/03/29 Javascript
解决vue watch数据的方法被调用了两次的问题
2020/11/07 Javascript
Python实现PS滤镜特效Marble Filter玻璃条纹扭曲效果示例
2018/01/29 Python
windows下python虚拟环境virtualenv安装和使用详解
2019/07/16 Python
Python嵌套函数,作用域与偏函数用法实例分析
2019/12/26 Python
Python自动化办公Excel模块openpyxl原理及用法解析
2020/11/05 Python
python3 googletrans超时报错问题及翻译工具优化方案 附源码
2020/12/23 Python
如何用Python和JS实现的Web SSH工具
2021/02/23 Python
HTMl5的存储方式sessionStorage和localStorage详解
2014/03/18 HTML / CSS
受外贸欢迎的美国主机:BlueHost
2017/05/16 全球购物
美国最大的购物网站:Amazon.com(亚马逊美国)
2020/05/23 全球购物
高级电工工作职责
2013/11/21 职场文书
运动会广播稿200字
2014/01/15 职场文书
努力学习演讲稿
2014/05/10 职场文书
保安2014年终工作总结
2014/12/06 职场文书
廉政承诺书范文
2015/04/28 职场文书
护士年终工作总结不会写?各科护士模板总结
2020/01/02 职场文书
Python中如何处理常见报错
2022/01/18 Python
浅谈redis的过期时间设置和过期删除机制
2022/03/18 MySQL