前端页面缓存技术方案
- 关于页面缓存数据的纯前端技术方案
- 背景
- 项目存在的现有方案
- 思考🤔
- 其他技术调研
- react-activation
- react-router-cache-route
- 结论
关于页面缓存数据的纯前端技术方案
背景
为了优化用户的体验,可能会遇到这样的需求:在列表页跳到详情页然后又返回列表的时候,需要保持状态和滚动位置;或是页面内切换组件(比如切换 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 }) => (<divkey={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 (<divref={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 ? (<CacheRouteexact={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组件改装起来较难,因此不适合该方案
结论
- 通过将页面数据进行保存数据,更容易让人理解,但是每次页面有此需求都需要再次开发需要存储的数据,开发成本比较大,不易维护;
- 通过第三方插件将页面组件保存另一个容器里,开发成本低,但会因项目框架的设计,使用可能会受限制;