H5 页面列表缓存方案

大家好,我是若川(点这里加我微信 ruochuan12,长期交流学习)。今天给大家介绍一下关于h5页面的列表缓存方案。感谢屏幕前的你一直关注着我。

点击下方卡片关注我、加个星标,或者查看源码等系列文章。学习源码整体架构系列、年度总结、JS基础系列


前言

在 H5 日常开发中,会经常遇到列表点击进入详情页面然后返回列表的情况,对于电商类平台尤为常见,像我们平常用的淘宝、京东等电商平台都是做了缓存,而且不只是列表,很多地方都用到了缓存。但刚才说的都是 App,在原生 App 中,页面是一层层的 View,盖在 LastPage 上,天然就能够保存上一个页面的状态,而 H5 不同,从详情返回到列表后,状态会被清除掉,重新走一遍生命周期,会重新发起请求,会有新的状态写入,对于分页接口,列表很长,当用户翻了好几页后,点击详情看看商品详情后再返回列表,此时页面回到第一页,这样用户体验很差,如果在进入详情的时候将列表数据缓存起来,返回列表的时候用缓存数据,而不是重新请求数据,停留在离开列表页时的浏览位置;或者是能够像 App 那样,将页面一层层堆叠在 LastPage 上,返回的时候展示对应的页面,这样用户体验会好很多,本文简单介绍一下在自己在做列表缓存的时候考虑的几点,后附简单实现。

思考

状态丢失的原因

通常在页面开发中,我们是通过路由去管理不同的页面,常用的路由库也有很多,譬如:React-Router (https://react-guide.github.io/react-router-cn/),Dva-router (https://dvajs.com/api/#dva-router)... 当我们切换路由时,没有被匹配到的 Component 也会被整体替换掉,原有的状态也丢失了。因此,当用户从详情页退回到列表页时,会重新加载列表页面组件,重新走一遍生命周期,获取的就是第一页的数据,从而回到了列表顶部,下面是常用的路由匹配代码段。

function RouterConfig({ history, app }) {const routerData = getRouterData(app);return (<ConnectedRouter history={history}><Routepath="/"render={(props) => <Layouts routerData={routerData} {...props} />}redirectPath="/exception/403"/></ConnectedRouter>);
}
// 路由配置说明(你不用加载整个配置,
// 只需加载一个你想要的根路由,
// 也可以延迟加载这个配置)。
React.render((<Router><Route path="/" component={App}><Route path="about" component={About}/><Route path="users" component={Users}><Route path="/user/:userId" component={User}/></Route><Route path="*" component={NoMatch}/></Route></Router>
), document.body)

如何解决

原因找到了,那么我们怎么去缓存页面或者数据呢?一般有两种解决方式:1. 路由切换时自动保存状态。2. 手动保存状态。在 Vue 中,可以直接使用 keep-alive 来实现组件缓存,只要使用了 keep-alive 标签包裹的组件,在页面切换的时候会自动缓存 失活 的组件,使用起来非常方便,简单例子如下。

<!-- 失活的组件将会被缓存!-->
<keep-alive><component v-bind:is="currentTabComponent"></component>
</keep-alive>

但是,React 中并没有 keep-alive 这种类似的标签或功能,官方认为这个功能容易造成内存泄漏,暂不考虑支持 (https://github.com/facebook/react/issues/12039)。

所以只能是在路由层做手脚,在路由切换时做对应的缓存操作,之前有开发者提出了一种方案:通过样式来控制组件的显示/隐藏 (https://github.com/facebook/react/issues/12039),但是这可能会有问题,例如切换组件的时候无法使用动画,或者使用 ReduxMobx 这样的数据流管理工具,还有开发者通过 React.createPortal API 实现了 React 版本的 React Keep Alive (https://github.com/Sam618/react-keep-alive),并且使用起来也比较方便。第二种解决方案就是手动保存状态,即在页面卸载时手动将页面的状态收集存储起来,在页面挂载的时候进行数据恢复,个人采用的就是简单粗暴的后者,实现上比较简单。缓存缓存,无外乎就是两件事,存和取,那么在存、取的过程中需要注意哪些问题呢?

个人认为需要注意的有以下几点:

存什么?何时存?存在哪?何时取?在哪取?

存什么

首先我们需要关心的是:存什么?既然要缓存,那么我们要存的是什么?是缓存整个 Component、列表数据还是滚动容器的 scrollTop。举个例子,微信公众号里的文章就做了缓存,任意点击一篇文章浏览,浏览到一半后关闭退出,再一次打开该文章时会停留在之前的位置,而且大家可以自行测试一下,再次打开的时候文章数据是重新获取的,在这种场景下,是缓存了文章详情滚动容器的滚动高度,在离开页面的时候存起来,再次进入的时候拿到数据后跳转到之前的高度,除此之外,还有很多别的缓存的方式,可以缓存整个页面,缓存 state 的数据等等,这些都可以达到我们想要的效果,具体用哪一种要看具体的业务场景。

何时存

其次,我们需要考虑的是什么时候存,页面跳转时会有多种 action 导航操作,比如:POPPUSHREPLACE 等,当我们结合一些比较通用的路由库时,action 会区分的更加细致,对于不同的 action 在不同的业务场景下处理的方式也不尽相同。还是拿微信公众号举例,文章详情页面就是无脑存,无论是 PUSHPOP 都会存高度数据,所以我们无论跳转多少次页面,再次打开总能跳转到之前离开时的位置,对于商品列表的场景时,就不能无脑存了,因为从 List -> Detail -> List 需要缓存没问题,但是用户从 List 返回到其他页面后再次进入 List 时,是进入一个新的页面,从逻辑上来说就不应该在用之前缓存的数据,而是重新获取数据。正确的方式应该是进行 PUSH 操作的时候存,POP 的时候取。

存在哪

  1. 持久化缓存。如果是数据持久化可存到 URL 或 localStorage 中,放到 URL 上有一个很好点在于确定性,易于传播。但 URL 可以先 pass 掉,因为在复杂列表的情况下,需要存的数据比较多,全部放到 URL 是不现实的,即使可以,也会让 URL 显得极其冗长,显然不妥。localStorage 是一种方式,提供的 getItemsetItem 等 api 也足够支持存取操作,最大支持 5M,容量也够,通过序列化 Serialize 整合也可以满足需求,另外 IndexDB 也不失为一种好的方式,WebSQL 已废弃,就不考虑了,详细可点击张鑫旭的这篇文章《HTML5 indexedDB前端本地存储数据库实例教程》(https://www.zhangxinxu.com/wordpress/2017/07/html5-indexeddb-js-example/)查看对比。

  2. 内存。对于不需要做持久化的列表或数据来说,放内存可能是一个更好的方式,如果进行频繁的读写操作,放内存中操作 I/O 速度快,方便。因此,可以放到 Redux 或 Rematch 等状态管理工具中,封装一些通用的存取方法,很方便,对于一般的单页应用来说,还可以放到全局的 window 中。

何时取

在进入缓存页面的时候取,取的时候又有几种情况

  1. 当导航操作为 POP 时取,因为每当 PUSH 时,都算是进入一个新的页面,这种情况是不应该用缓存数据。

  2. 无论哪种导航操作都进行取数据,这种情况需要和何时存一起看待。

  3. 看具体的业务场景,来判断取的时机。

在哪取

这个问题很简单,存在哪就从哪里取。

CacheHoc 的方案

  • 存什么:列表数据 + 滚动容器的滚动高度

  • 何时存:页面离开且导航操作为 PUSH

  • 存在哪:window

  • 何时取:页面初始化阶段且导航操作为 POP 的时候

  • 在哪取:window

CacheHoc 是一个高阶组件,缓存数据统一存到 window 内,通过 CACHE_STORAGE 收敛,外部仅需要传入 CACHE_NAMEscrollElRefs 即可,CACHE_NAME 相当于缓存数据的 key,而 scrollElRefs 则是一个包含滚动容器的数组,为啥用数组呢,是考虑到页面多个滚动容器的情况,在 componentWillUnmount 生命周期函数中记录对应滚动容器的 scrollTopstate,在 constructor 内初始化 state,在 componentDidMount 中更新 scrollTop

简单使用

import React from 'react'
import { connect } from 'react-redux'
import cacheHoc from 'utils/cache_hoc'@connect(mapStateToProps, mapDispatch)
@cacheHoc
export default class extends React.Component {constructor (...props) {super(...props)this.props.withRef(this)}// 设置 CACHE_NAMECACHE_NAME = `customerList${this.props.index}`;scrollDom = nullstate = {orderBy: '2',loading: false,num: 1,dataSource: [],keyWord: undefined}componentDidMount () {// 设置滚动容器listthis.scrollElRefs = [this.scrollDom]// 请求数据,更新 state}render () {const { history } = this.propsconst { dataSource, orderBy, loading } = this.statereturn (<div className={gcmc('wrapper')}><MeScrollclassName={gcmc('wrapper')}getMs={ref => (this.scrollDom = ref)}loadMore={this.fetchData}refresh={this.refresh}up={{page: {num: 1, // 当前页码,默认0,回调之前会加1,即callback(page)会从1开始size: 15 // 每页数据的数量// time: null // 加载第一页数据服务器返回的时间; 防止用户翻页时,后台新增了数据从而导致下一页数据重复;}}}down={{ auto: false }}>{loading ? (<div className={gcmc('loading-wrapper')}><Loading /></div>) : (dataSource.map(item => (<Cardkey={item.clienteleId}data={item}{...this.props}onClick={() =>history.push('/detail/id')}/>)))}</MeScroll><div className={styles['sort']}><div className={styles['sort-wrapper']} onClick={this._toSort}><span style={{ marginRight: 3 }}>最近下单时间</span><imgsrc={orderBy === '2' ? SORT_UP : SORT_DOWN}alt='sort'style={{ width: 10, height: 16 }}/></div></div></div>)}
}

效果如下:

缓存的数据:

代码

const storeName = 'CACHE_STORAGE'
window[storeName] = {}export default Comp => {return class CacheWrapper extends Comp {constructor (props) {super(props)// 初始化if (!window[storeName][this.CACHE_NAME]) {window[storeName][this.CACHE_NAME] = {}}const { history: { action } = {} } = props// 取 stateif (action === 'POP') {const { state = {} } = window[storeName][this.CACHE_NAME]this.state = {...state,}}}async componentDidMount () {if (super.componentDidMount) {await super.componentDidMount()}const { history: { action } = {} } = this.propsif (action !== 'POP') returnconst { scrollTops = [] } = window[storeName][this.CACHE_NAME]const { scrollElRefs = [] } = this// 取 scrollTopscrollElRefs.forEach((el, index) => {if (el && el.scrollTop !== undefined) {el.scrollTop = scrollTops[index]}})}componentWillUnmount () {const { history: { action } = {} } = this.propsif (super.componentWillUnmount) {super.componentWillUnmount()}if (action === 'PUSH') {const scrollTops = []const { scrollElRefs = [] } = thisscrollElRefs.forEach(ref => {if (ref && ref.scrollTop !== undefined) {scrollTops.push(ref.scrollTop)}})window[storeName][this.CACHE_NAME] = {state: {...this.state},scrollTops}}if (action === 'POP') {window[storeName][this.CACHE_NAME] = {}}}}
}

总结

以上的 CacheHoc 只是最简单的一种实现,还有很多可以改进的地方,譬如:直接存在 window 中有点粗暴,多页应用下存到 window 会丢失数据,可以考虑存到 IndexDB 或者 localStorage 中,另外这种方案若不配合上 mescroll 需要在 componentDidMount 判断 state 内的数据,若有值就不初始化数据,这算是一个 bug

缓存方案纵有多种,但需要考虑的问题就以上几点。另外在讲述需要注意的五个点的时候,着重介绍了存什么和存在哪,其实存在哪不太重要,也不需要太关心,找个合适的地方存着就行,比较重要的是存什么、何时存,需要结合实际的应用场景,来选择合适的方式,可能不同的页面采用的方式都不同,没有固定的方案,重要的是分析存取的时机和位置。


最近组建了一个江西人的前端交流群,如果你也是江西人可以加我微信 ruochuan12 拉你进群。


················· 若川出品 ·················

今日话题

还有最后一天上班就放五一小长假啦(努力让内心喜悦不被发现),虽然扣除2天补班, 2天周末,实际天只有1天假期~哈哈,但是能连着休息5天,也还是很不错哦。趁着小长假可以好好放休息休息,整理一下之前没及时整理的东西,大家五一都有什么计划呢?欢迎在下方留言~  欢迎分享、收藏、点赞、在看我的公众号文章~

一个愿景是帮助5年内前端人走向前列的公众号

可加我个人微信 ruochuan12,长期交流学习

推荐阅读

我在阿里招前端,我该怎么帮你?(现在还能加我进模拟面试群)

若川知乎问答:2年前端经验,做的项目没什么技术含量,怎么办?

点击方卡片关注我、加个星标,或者查看源码等系列文章。
学习源码整体架构系列、年度总结、JS基础系列

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/275943.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

不只是coding_不只是外表

不只是coding“We just need it to look more professional…”“我们只需要看起来更专业...” “We don’t have the graphic expertise you do…”“我们没有您所需要的图形专业知识……” “I just don’t know how to make it look good…”“我只是不知道如何使它看起来…

读取 wps_软件前世今生篇之WPS(求伯君1988年先于OFFICE研发出WPS)

软件前世今生篇之WPS今天给大家普及一下WPS这款办公软件&#xff0c;相信你会问wps有什么可普及的&#xff1f;我们都知道啊&#xff0c;不就是一款办公软件&#xff0c;而且还是抄袭office的&#xff0c;安装还挺简单的&#xff0c;而且还有一大堆广告&#xff0c;不过使用免费…

吴恩达机器学习笔记11-梯度下降法实践2-学习率

梯度下降算法收敛所需要的迭代次数根据模型的不同而不同&#xff0c;我们不能提前预知&#xff0c;我们可以绘制迭代次数和代价函数的图表来观测算法在何时趋于收敛。 也有一些自动测试是否收敛的方法&#xff0c;例如将代价函数的变化值与某个阀值&#xff08;例如0.001&#…

制作五彩纸屑转场动效_何时以及如何将五彩纸屑添加到产品UI

制作五彩纸屑转场动效As I am sure all designers have picked up on, confetti has become a popular method of (positive) feedback inside mobile and desktop apps. I will discuss the viable scenarios where you can implement confetti and will even provide some co…

【无套路送书】架构师是怎样炼成的?

大家好&#xff0c;我是若川。不知道这是今年第几次送书了&#xff0c;前三次分别是&#xff1a;第一次&#xff0c;第二次&#xff0c;第三次。本次《架构师的自我修炼》&#xff0c;非常珍贵&#xff0c;我争取到了2本送给大家&#xff0c;送书规则见文末。可以参与下&#x…

WinForm中使用Excel控件

&#xfeff;最近项目中要在WinForm中使用Excel控件&#xff0c;经过几天的研究&#xff0c;现在总结一下成果。 在WinForm中使用Excel控件主要有三种方法&#xff1a;WebBrowser、DSOFramer、OWC。下面分别描述一下如何使用。 一、WebBrowser /// -1、如何使用 WebBrowser 控件…

NASA公布“门户计划”,在月球轨道建立空间站进一步探索月球

门户是NASA研发一种小型的宇宙飞船的名字&#xff0c;该宇宙飞船将围绕月球轨道运行 成为宇航员临时住所和办公室。 日前&#xff0c;美国宇航局&#xff08;以下简称“NASA”&#xff09;公布了“门户计划”&#xff0c;该计划具体是指在月球轨道上建立空间站&#xff0c;以帮…

浅析Page.LoadTemplate(模板)方法动态获取绑定模板后,通过FindControl获取服务端控件的方法。...

平常使用DataList数据控件绑定数据时&#xff0c;都是在ItemTemplate项里面放入 <asp:DataList ID"list2"runat"server"><ItemTemplate><asp:HyperLink ID"hl"runat"server"></asp:HyperLink></ItemTempl…

苹果5s变砖_苹果砖的故事以及可以改进的地方

苹果5s变砖Even since I can remember I’ve always been curious about trying out all kinds of software, checking out different operating systems, and improving my own user experience through customizing them. Over the years I’ve had the opportunity to test …

学习 launch-editor 源码整体架构,探究 vue-devtools「在编辑器中打开组件」功能实现原理...

1. 前言你好&#xff0c;我是若川[1]&#xff0c;微信搜索「若川视野」关注我&#xff0c;专注前端技术分享&#xff0c;一个愿景是帮助5年内前端开阔视野走向前列的公众号。欢迎加我微信ruochuan12&#xff0c;长期交流学习。这是学习源码整体架构系列 之 launch-editor 源码&…

:传递给 left 或 substring 函数的长度参数无效。_Java函数式编码结构-好程序员

好程序员Java培训分享Java函数式编码结构&#xff0c;本文将探讨三种下一代JVM语言&#xff1a;Groovy、Scala和Clojure&#xff0c;比较并对比新的功能和范例&#xff0c;让Java开发人员对自己近期的未来发展有大体的认识&#xff0c;下面我们一起来看一下吧。当垃圾回收成为主…

跨库一致性_设计跨平台的一致性

跨库一致性I offended an Apple employee the other day when I was checking out the new iPad Pro and I told him that I was an Android phone user. Eyes rolled, jokes were made, and we agreed to disagree.前几天&#xff0c;我在检阅新iPad Pro时冒犯了一名苹果员工&…

漫画 | 一个NB互联网项目的上线过程…

大家好&#xff0c;我是若川&#xff08;点这里加我微信 ruochuan12&#xff0c;长期交流学习&#xff09;。今天虽然是周六&#xff0c;但还是要上班&#xff0c;所以就推荐一篇比较轻松的漫画。点击下方卡片关注我、加个星标&#xff0c;或者查看源码等系列文章。学习源码整体…

胖子脸:库珀·布莱克100年

In 16th century Europe, roman typefaces were the first to surpass blackletter as the preferred choice for expressing emphasis in print. True bold weight roman letters didn’t appear until the 19th century, which critics quickly coined “Fat Faces” due to …

C语言中的布尔值

C语言的布尔类型在C语言标准(C89)没有定义布尔类型&#xff0c;所以C语言判断真假时以0为假&#xff0c;非0为真。所以我们通常使用逻辑变量的做法&#xff1a; //定义一个int类型变量&#xff0c;当变量值为0时表示false&#xff0c;值为1时表示trueint flag;flag 0;//......…

c++ explicit关键字_聊一聊 C++的特性 explicit 匿名空间

聊一聊 C的特性 explicit && 匿名空间explicit关键字首先看一下explicit的作用&#xff1a;explicit 是避免构造函数的参数自动转换为类对象的标识符&#xff0c;平时代码中并不是经常用到&#xff0c;但是&#xff0c;有时候就是因为这个&#xff0c;会造成一定的BUG出…

谷歌浏览器那些有趣的隐藏功能

大家好&#xff0c;我是若川&#xff08;点这里加我微信 ruochuan12&#xff0c;长期交流学习&#xff09;。今天推荐一篇实用文章。文末有抽奖。点击下方卡片关注我、加个星标&#xff0c;或者查看源码等系列文章。学习源码整体架构系列、年度总结、JS基础系列很多小伙伴说还是…

yii mysql_Yii2框架操作数据库的方法分析【以mysql为例】

本文实例讲述了Yii2框架操作数据库的方法。分享给大家供大家参考&#xff0c;具体如下&#xff1a;准备数据库DROP TABLE IF EXISTS pre_user;CREATE TABLE pre_user(id int(11) AUTO_INCREMENT PRIMARY KEY,username varchar(255) NOT NULL,password varchar(32) NOT NULL DEF…

C++接口注意

1. 用Record接口&#xff0c;要注意 Packed的区别 2. cdecl和stdcall的区别 3. C导出的函数建议用C格式stdcall导出&#xff0c;使用Def文件定义名称 4. 用VS写的API dll要注意是否引用了MFC的DLL&#xff0c;否则会使LoadLibrary失败&#xff0c;并GetLastError后返回14001 Ap…

Vue 3.1.0 的 beta 版发布

大家好&#xff0c;我是若川&#xff08;点这里加我微信 ruochuan12&#xff0c;长期交流学习&#xff09;。昨晚尤大视频号直播说到vue 3.1.0 beta版发布了&#xff0c;今天分享这篇文章。也有小伙伴可能注意到了昨晚我一直在送礼物。点击下方卡片关注我、加个星标&#xff0c…