在 H5 日常开发中,会经常遇到列表点击进入详情页面然后返回列表的情况,对于电商类平台尤为常见,像我们平常用的淘宝、京东等电商平台都是做了缓存,而且不只是列表,很多地方都用到了缓存。但刚才说的都是 App,在原生 App 中,页面是一层层的 View,盖在 LastPage 上,天然就能够保存上一个页面的状态。
而 H5 不同,从详情返回到列表后,状态会被清除掉,重新走一遍生命周期,会重新发起请求,会有新的状态写入,对于分页接口,列表很长,当用户翻了好几页后,点击详情看看商品详情后再返回列表,此时页面回到第一页,这样用户体验很差,如果在进入详情的时候将列表数据缓存起来,返回列表的时候用缓存数据,而不是重新请求数据,停留在离开列表页时的浏览位置;或者是能够像 App 那样,将页面一层层堆叠在 LastPage 上,返回的时候展示对应的页面,这样用户体验会好很多,本文简单介绍一下在自己在做列表缓存的时候考虑的几点,后附简单实现。
function RouterConfig({ history, app }) { const routerData = getRouterData(app); return ( <ConnectedRouter history={history}> <Route path="/" render={(props) => <Layouts routerData={routerData} {...props} />} redirectPath="/exception/403" /> </ConnectedRouter> ); } // 堆代码 duidaima.com // 路由配置说明(你不用加载整个配置, // 只需加载一个你想要的根路由, // 也可以延迟加载这个配置)。 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)如何解决
<!-- 失活的组件将会被缓存!--> <keep-alive> <component v-bind:is="currentTabComponent"></component> </keep-alive>但是,React 中并没有 keep-alive 这种类似的标签或功能,官方认为这个功能容易造成内存泄漏,暂不考虑支持。
1.持久化缓存。 如果是数据持久化可存到 URL 或 localStorage 中,放到 URL 上有一个很好点在于确定性,易于传播。但 URL 可以先 pass 掉,因为在复杂列表的情况下,需要存的数据比较多,全部放到 URL 是不现实的,即使可以,也会让 URL 显得极其冗长,显然不妥。 localStorage 是一种方式,提供的 getItem、setItem 等 api 也足够支持存取操作,最大支持 5M,容量也够,通过序列化 Serialize 整合也可以满足需求,另外 IndexDB 也不失为一种好的方式,WebSQL 已废弃,就不考虑了。
2.内存。 对于不需要做持久化的列表或数据来说,放内存可能是一个更好的方式,如果进行频繁的读写操作,放内存中操作 I/O 速度快,方便。因此,可以放到 redux 或 rematch 等状态管理工具中,封装一些通用的存取方法,很方便,对于一般的单页应用来说,还可以放到全局的 window 中。
3.看具体的业务场景,来判断取的时机。
在哪取: window
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) } // 堆代码 duidaima.com // 设置 CACHE_NAME CACHE_NAME = `customerList${this.props.index}`; scrollDom = null state = { orderBy: '2', loading: false, num: 1, dataSource: [], keyWord: undefined } componentDidMount () { // 设置滚动容器list this.scrollElRefs = [this.scrollDom] // 请求数据,更新 state } render () { const { history } = this.props const { dataSource, orderBy, loading } = this.state return ( <div className={gcmc('wrapper')}> <MeScroll className={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 => ( <Card key={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> <img src={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) // 堆代码 duidaima.com // 初始化 if (!window[storeName][this.CACHE_NAME]) { window[storeName][this.CACHE_NAME] = {} } const { history: { action } = {} } = props // 取 state if (action === 'POP') { const { state = {} } = window[storeName][this.CACHE_NAME] this.state = { ...state, } } } async componentDidMount () { if (super.componentDidMount) { await super.componentDidMount() } const { history: { action } = {} } = this.props if (action !== 'POP') return const { scrollTops = [] } = window[storeName][this.CACHE_NAME] const { scrollElRefs = [] } = this // 取 scrollTop scrollElRefs.forEach((el, index) => { if (el && el.scrollTop !== undefined) { el.scrollTop = scrollTops[index] } }) } componentWillUnmount () { const { history: { action } = {} } = this.props if (super.componentWillUnmount) { super.componentWillUnmount() } if (action === 'PUSH') { const scrollTops = [] const { scrollElRefs = [] } = this scrollElRefs.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] = {} } } } }