商城微信小程序生成_vue spa应用中的路由缓存问题

阅读  ·  发布日期 2021-01-07 13:22  ·  admin
vue spa应用中的路由缓存问题与解决方案       这篇文章主要介绍了vue spa应用中的路由缓存问题与解决方案,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

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

vue中的解决方式

vue.js最贴心的一点就是提供了非常多便捷的API,为开发者考虑到很多的应用场景。在vue中,如果想缓存路由,我们可以直接使用内置的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组件内部实现代码,具体代码可以见

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)
 ponentOptions: VNodeComponentOptions = ponentOptions
 if (componentOptions) {
 // check pattern 检查匹配是否为缓存组件,主要根据include传入的name来对应
 const name: string = ponentOptions)
 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 ponents
 // so cid alone is not enough (#3269)
 componentOptions.Ctor.cid + (componentOptions.tag `::${componentOptions.tag}` : '')
 : vnode.key
 if (cache[key]) {
 //查看cache对象中已经缓存了该组件,则vnode直接使用缓存中的组件实例
 ponentInstance = 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内部实现的基本流程就是:

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

React中的解决方案

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

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

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

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

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

componentDidMount() {
 const {app: {dataSoruce = [], scrollTop}, loadData} = this.pro凡科抠图;
 if (dataSoruce.length) { //判断redux中是否已经有数据源
 // 有数据则不再加载收据,只恢复滚动状态
 window.scrollTo(0, scrollTop);
 } else { //没有数据就去请求数据源
 this.pro凡科抠图.loadData(); // 在redux中定义的数据请求的action
handleClik = () = {
 在点击进入下一级页面前先保存当前的滚动距离
 const scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
 const {saveScrollTop} = this.pro凡科抠图;
 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组件的实现

return (
 RouterContext.Provider value={pro凡科抠图} 
 {children !isEmptyChildren(children)
 children
 : pro凡科抠图.match // pro凡科抠图.match 属性来确定是否要渲染组件
 component
 ponent, pro凡科抠图)
 : render
 render(pro凡科抠图)
 : null
 : null}
 /RouterContext.Provider 

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

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

然后溯源而上,我们找到了pro凡科抠图对象中关于match的定义:

const location = this.pro凡科抠图.location || context.location;
const match = putedMatch
 putedMatch // puted the match for us
 : this.pro凡科抠图.path
 matchPath(location.pathname, this.pro凡科抠图)
 : context.match;
const pro凡科抠图 = { ...context, location, match };

上述代码显示,putedMatch属性来判断:putedMatch属性赋值给match,否则如果this.pro凡科抠图.path存在,就会使用方法来根据当前的location.pathname来判断是否匹配。

putedMatch的介绍,不过在源码中有一行这样的注释

// puted the match for us

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

接下来我们再去了解一下:

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

我们翻开Switch组件的:

let element, match; // 定义最后返回的组件元素,和match匹配变量
 React.Children.forEach(this.pro凡科抠图.children, child = {
 if (match == null React.isValidElement(child)) { // 如果match没有内容则进入该判断
 element = child;
 const path = child.pro凡科抠图.path || child.pro凡科抠图.from;
 match = path // 该三元表达式只有在匹配到后会给match赋值一个对象,否则match一直为null
 matchPath(location.pathname, { ...child.pro凡科抠图, path })
 : context.match;
 return match
 React.cloneElement(element, { location, computedMatch: match })
 : 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.pro凡科抠图;
 return React.Children.map(children, child = {
 if (React.isValidElement(child)) { // 验证是否为是react element
 const {path} = child.pro凡科抠图;
 const match = matchPath(location.pathname, {...child.pro凡科抠图, path});
 if (match (include === true || include.includes(path))) {
 //putedMatch属性加入cache对象里
 //当include为true时,缓存全部组件,当include为数组时缓存对应组件
 this.cache[path] = {computedMatch: match};
 //可以在computedMatch里追加入一个display属性,可以在路由组件的pro凡科抠图.match拿到
 const clonePro凡科抠图 = this.cache[path] Object.assign(this.cache[path].computedMatch, {display: match 'block' : 'none'});
 return div 'block' : 'none'}} {React.cloneElement(child, {computedMatch: clonePro凡科抠图})} /div 
 return null;
// 使用
 RouteCache include={['/login', '/home']} 
 Route path="/login" component={Login} / 
 Route path="/home" component={App} / 
 /RouteCache 

在阅读了源码后,putedMatch来判断是否要渲染该组件。

putedMatch属性写入该缓存对象中。这样即使当url不再匹配时,也能通过读取cache对象中该路径的值,并使用React .putedMatch属性赋值给组件的pro凡科抠图。这样已缓存过的路由组件就会被一直渲染出来,组件就不会被卸载掉。

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

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

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

使用效果

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

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

ponentDidMount生命周期也只执行一次,在pro凡科抠图.match中我们可以获取到当前的display值。

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



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