【前端页面缓存技术方案】

  • Post author:
  • Post category:其他




关于页面缓存数据的纯前端技术方案



背景

为了优化用户的体验,可能会遇到这样的需求:在列表页跳到详情页然后又返回列表的时候,需要保持状态和滚动位置;或是页面内切换组件(比如切换 Tab )的时候,需要保持状态。但目前由于react router只保留当前匹配第一个的路由状态,卸载掉其他路由。切换页面或Tab时会销毁上个组件,所以现状是每次返回时都是当前页面的初始状态。



项目存在的现有方案


思路:将列表页的数据全部保存起来,再次返回列表页的时候,进行读取数据

  • 本地存储

    通过localStorage或者sessionStorage缓存列表页的状态,当再次返回列表页时,通过获取本地缓存数据来实现保留当前页面的状态。


    弊端:

    • 如果用户手动清除了本地缓存后,再次返回到列表页,将获取不到保存的数据;
    • 如果当前分页为2,返回列表页后再增加条件搜索,搜索结果数据不足达到2页,可能会有报错问题
  • redux存储

    对于react实现的SPA页面,我们可以通过创建store存储列表数据保留在一级页面路径下,然后再次返回列表页面后,通过react-redux的connect方法将state中的数据绑定到页面的props中,方便访问。


    弊端:

    • 列表数据较多时,需要存储多个数据状态,不易维护;
    • 在页面路径较深或子路径多时,其他页面也会访问到store中的数据,导致数据较多,出现冗余
  • url携带参数存储

    将列表页的数据状态作为参数保存在页面的url中,页面参数通过列表页面数据变化而实时改变。当详情页再次返回到列表页,通过解析url参数来展示页面相应状态。


    弊端:

    • 随着切换筛选条件次数增多,浏览器会记录每一次的url变化,当用户点击浏览器回退或前进按钮时,浏览器会从历史记录中获取返回url,用户多次点击返回,可能导致页面展示上一次筛选状态,而并非跳转页面,影响用户体验
    • 如果列表页的数据状态较多,url上的公共业务参数也较多,由于url参数长度有限,可能会出现丢失参数的问题
  • react context

    和redux相似,将列表页面数据保存到 context 中,context provider 放置在列表页和详情页共同的父组容器上。弊端也是会存在不易维护的问题,还有在子组件中更新 context 容易引发死循环的问题。(项目没有使用context,因此放弃)



思考🤔

除了以上存储页面各个数据的方式以外,是否还有其他方式解决呢?

联想到了在手机屏幕上进行操作App的页面切换,我们可以理解是在当前屏幕上做一层层叠加,用户看到的一直都是最上层,当用户返回页面,即看到下一层页面,每一层的页面数据都有所保留,因此在切换页面的时候并没有销毁,而是叠加。

通过移动端的实现,联想PC端是不是也可以不让当前页面销毁?达到一个伪销毁的状态,将页面保存到另一个容器里,然后当访问到该路由时,直接把容器里的数据取出再赋给真实的DOM组件中。



其他技术调研



react-activation

git地址:

https://github.com/CJY0208/react-activation

import React, { Component, createContext } from 'react'

const { Provider, Consumer } = createContext()
const withScope = WrappedComponent => props => (
  <Consumer>{keep => <WrappedComponent {...props} keep={keep} />}</Consumer>
)

export class AliveScope extends Component {
  nodes = {}
  state = {}

  keep = (id, children) =>
    new Promise(resolve =>
      this.setState(
        {
          [id]: { id, children }
        },
        () => resolve(this.nodes[id])
      )
    )

  render() {
    return (
      <Provider value={this.keep}>
        {this.props.children}
        {Object.values(this.state).map(({ id, children }) => (
          <div
            key={id}
            ref={node => {
              this.nodes[id] = node
            }}
          >
            {children}
          </div>
        ))}
      </Provider>
    )
  }
}

@withScope
class KeepAlive extends Component {
  constructor(props) {
    super(props)
    this.init(props)
  }

  init = async ({ id, children, keep }) => {
    const realContent = await keep(id, children)
    this.placeholder.appendChild(realContent)
  }

  render() {
    return (
      <div
        ref={node => {
          this.placeholder = node
        }}
      />
    )
  }
}

export default KeepAlive

实现大致过程是将AliveScope组件通过上下文,把一个keep方法传递下去,将KeepAlive组件包裹需要缓存的组件,然后一个高阶组件获取到 keep 方法,并把 children 属性传入 KeepAlive 组件,在 KeepAlive 组件中调用 keep 方法,把 children 属性缓存到 AliveScope 的 state 中。在 state 更新后,把 ref (真实 DOM)返回给 KeepAlive 组件。KeepAlive 组件拿到真实 DOM 后,把它移动到自己组件内的某个占位中。

在这里插入图片描述
主要思路:把 children 包裹起来并且传递出去,在缓存组件内被渲染,当前组件正常地更新卸载。当前组件卸载的时候,children 也被卸载了,但是它的虚拟 DOM 已经被缓存在了缓存组件中。这个组件重新被加载的时候,把缓存直接渲染后移入当前组件,就恢复了组件卸载前状态。


示例

import React from 'react';
import ReactDOM from 'react-dom';
import { AliveScope, KeepAlive } from 'react-activation'
import Test from './views/Test';

ReactDOM.render(
  <AliveScope>
    <KeepAlive name="Test">
      <Test />
    </KeepAlive>
  </AliveScope>,
  document.getElementById('root'),
);


弊端:

  • 适用场景不灵活,例如当用户只在点击浏览器回退按钮时读取缓存,其他场景返回不缓存时,该方案实现起来较难,因此在特定场景下缓存页面,不推荐使用



react-router-cache-route

git地址:

https://github.com/CJY0208/react-router-cache-route


通过“重写“Switch、Route两个组件,控制页面组件的生命周期,使其在不匹配的时候,记录滚动位置并从dom上移除,在匹配的时候,重新挂载到dom上并恢复滚动位置,整个过程组件实例并未销毁,从而达到缓存实例数据状态的目的。


示例:

<AppContainer>
	<CustomHeader />
	<CacheSwitch>
		{
			routes.map((route: RouteType, index:number) => {
				return route.cache ? (
						<CacheRoute
							exact={true}
							path={`/${route.routerPath}`}
							key={index}
							component={route.component}
						/>
					) : (
						<Route 
							exact={true}
							path={`/${route.routerPath}`}
							key={index}
							component={route.component}
						/>
					)
			})
		}
		<Redirect to='/login' />
	</ CacheSwitch>
</AppContainer>


CacheRoute


给组件套上Route、CacheComponent等wrapper,配合computedMatchForCacheRoute欺骗Route组件,从而确保其Route内嵌套的CacheComponent会一直渲染。通过可传props参数项,可以在指定场景进行缓存,适用场景更加灵活 。

在这里插入图片描述


弊端:

  • 项目框架受限,当项目采用umi等一些对router组件进行封装的框架时,项目对router组件改装起来较难,因此不适合该方案



结论

  • 通过将页面数据进行保存数据,更容易让人理解,但是每次页面有此需求都需要再次开发需要存储的数据,开发成本比较大,不易维护;
  • 通过第三方插件将页面组件保存另一个容器里,开发成本低,但会因项目框架的设计,使用可能会受限制;



版权声明:本文为csdn__qiao原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。