交流群:462197261站长百科站长论坛热门标签收藏本站北冥有鱼 互联网前沿资源第一站 助力全行业互联网+
点击这里给我发消息
  • 当前位置:
  • vue spa应用中的路由缓存问题与解决方案

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

    通常我们在进行页面前后退时,浏览器通常会帮我们记录下之前滚动的位置,这使得我们不会在每次后退的时候都丢失之前的浏览器记录定位。但是在现在愈发流行的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'}}属性去隐藏它们。

    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路径组件。

    使用效果

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

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

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

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

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

    以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持北冥有鱼。

    您可能感兴趣的文章:

    • vue动态添加路由addRoutes之不能将动态路由存入缓存的解决
    • Vue动态路由缓存不相互影响的解决办法

    广而告之:
    热门推荐:
    php抽奖小程序的实现代码

    这个抽奖小程序,在实际的测试环境中也可以用到,比方说测试数据的查询在in条件下,要查询随机的5个id,然后在用ab去压测 复制代码 代码如下:<?php  /**   * “抽奖”函数   *   * @param integer $first    起始编号   * @par···

    cookie的secure属性详解

    今天做项目的时候涉及到了cookie跨域传递的问题,也因此了解了cookie的一个属性——secure。 顾名思义,这个属性就是用来保证cookie的安全的。 当secure属性设置为true时,cookie只有在https协议下才能上传到服务器,而在http协议下是没法上传的,所以也不会被窃听。 简单实践一···

    简单实现jquery隔行变色

    本文实例为大家分享了jquery隔行变色展示的具体代码,供大家参考,具体内容如下 效果图 代码 <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title></title> </head> <script type="text/javascript" src="js/jquery···

    微信小程序 中wx.chooseAddress(OBJECT)实例详解

    1、wx.chooseAddress(OBJECT) 概述 调起用户编辑收货地址原生界面,并在编辑完成后返回用户选择的地址。 OBJECT参数说明 代码实现 if(wx.chooseAddress){ wx.chooseAddress({ success: function (res) { console.log(JSON.stringify(res)) }, fail: function(err){···

    sqlserver中delete、update中使用表别名和oracle的区别

    昨天发现程序中数据分析的结果不对,重新进行分析后,原数据仍在,有值的字段被累计。心说,不对啊,是重新生成记录后才分析的啊。难道忘了DELETE了?查代码,发现有删除语句。于是在查询分析器中执行,报错。反复试几次,明白了,Delete From不认表名别名!回头想下,当初程···

    在php7中MongoDB实现模糊查询的方法详解

    前言 在实际开发中, 有不少的场景需要使用到模糊查询, MongoDB shell 模糊查询很简单: db.collection.find({'_id': /^5101/}) 上面这句就是查询_id以'5101'开始的内容。 在老的MogoDB中模糊查询挺简单的,这里简单记录下模糊查询的操作方式: 命令行下: db.letv_logs.find···

    ASP.NET 中的正则表达式

    摘要:正则表达式是一种处理文本的有用工具。无论是验证用户输入、搜索字符串内的模式、还是以各种有效方式重新设置文本格式,正则表达式都非常有用。下载本文的源代码。引言Microsoft®.NET Framework 对正则表达式的支持是一流的,甚至在 Microsoft® ASP.NET 中···

    jQuery EasyUI API 中文文档 - TreeGrid 树表格使用介绍

    扩展自 $.fn.datagrid.defaults,用 $.fn.treegrid.defaults 重写了 defaults。 依赖 datagrid 用法 复制代码 代码如下: <table id="tt"></table> 复制代码 代码如下: $('#tt').treegrid({ url:'treegrid_data.json', treeField:'name', columns:[[ {title:···

    SQLserver中用convert函数转换日期格式的方法

    SQL Server中文版的默认的日期字段datetime格式是yyyy-mm-dd Thh:mm:ss.mmm 例如: select getdate() 整理了一下SQL Server里面可能经常会用到的日期格式转换方法: 举例如下: select CONVERT(varchar, getdate(), 120 ) 2004-09-12 11:06:08 select replace(replace(replace(···

    Vue单页及多页应用全局配置404页面实践记录

    前后端分离后,控制路由跳转的责任转移到了前端,后端只负责给前端返回一个html文档以及提供各种接口。下面我们用作例子的两个项目,均采用vue作为基础框架,一个是SPA应用,另一个是多页应用,均由前端进行路由控制及渲染的。 总体思路 无论单页还是多页,我的实现思路是总体···