场景
有一个 列表页,里面有很多的商品。商品列表数据是懒加载的。
用户在拉到某条商品时,对它感兴趣,点进去看该商品的详情页。
提一个需求:当用户从 商品详情页 返回 列表页 时,要求之前加载的列表数据,依然存在;同时滚动条的位置,停留在刚才离开的地方。
这个需求,在 react-router 的情况下怎么实现呢?
Demo && github
Demo: yuanzhizhu.github.io/react-cache…
github: react-cache-router
基本原理
我们通常怎么写 Route ?
<Route path="/goods-list" component={GoodsList} />
除了这种写法,还有什么呢?
<Route path="/goods-list" children={({ match }) => <GoodsList />)} />
那么 children 和 component 有什么区别呢?
这边就不卖关子了,直接说结论:children 无论在 path 是否匹配的情况下,是都能渲染的。
都能渲染 换而言之就是组件 不被卸载。这就意味着,原先在页面上的所有东西(无论是状态还是数据,无论是受控还是不受控),都会始终存在。这样就能解决本文一开始提到的,当从 列表页 跳到 详情页 后,列表页 被卸载,导致数据被清除的问题。
当使用 Route.children 后,可传入一个回调函数。该函数格式如下:
<Route path="/some-path" children={
({ match, ...restProps }) => {
const visible = match ? true : false;
return (
<div style={{ display: visible ? 'block' : 'none' }}>
<SomeRouteComponent {...restProps} />
</div>
)
}
} />
解释一下,当路由匹配 /some-path 时,match为一个 Object,否则为 null。
故可通过 match 判断 visbile。同时给原来的 SomeRouteComponent 外部包裹一层 div,然后通过 visible 控制该 div 的显隐性。最终实现整个组件的显隐性。
如何处理滚动条位置
上面提到了如何解决路由跳转后,前一个页面被卸载,从而导致数据被清掉的问题。
接下来讨论如何处理滚动条位置。
现在无非是两种动作:
1、从 列表页 跳到 详情页,保存滚动条位置
2、从 详情页 跳回 列表页,恢复滚动条位置
那么怎么判断我当前是哪种动作呢?
其实很简单,保存上一个状态的 visible,同时和本次的 visible 做比较。如果是从 true -> false,则保存滚动条位置;如果是从 false -> true,则恢复滚动条位置。
当然,在实现细节上面,要考虑下兼容性问题。比如 获取当前滚动条位置,在不同浏览器是有差异的。
一些生命周期将失效
如果按照之前不缓存路由的方式,一切都很简单:
每次进入 列表页,都会触发 componentDidMount。
每次离开 列表页,都会触发 componentWillUnmount。
但是现在做了缓存后,一切都不一样了:
只会在第一次进入的时候触发 componentDidMount。
至于 componentWillUnmount,可能永远都不会执行了。
那如果遇到以下的场景:
页面A中有一个定时器。页面A在 componentDidMount 设置 setInterval,在 componentWillUnmount 的时候,取消 setInterval。
后来对页面A做了缓存,导致 componentDidMount 只触发一次,componentWillUnmount 失效。
所以需要两个新的生命周期,来同样实现作用:componentDidShow 和 componentWillHide。
那么如何实现这两个新的生命周期呢?其实也简单!
使用 ref 取到 页面组件 的实例。当 visible 从 false -> true 的时候,判断该实例上,是否有使用 componentDidShow 方法,如果有,则执行。
同理,当 visible 从 true -> false 的时候,判断该实例上,是否有使用 componentWillHide 方法,如果有,则执行。
如果页面组件有高阶函数包裹怎么办?
上面提到,因需要给页面挂在额外的两个生命周期,所以必须要取到 ref。但是,当页面被高阶函数包裹后,就很难再取到 ref(大多数的高阶函数都不使用 React.forwardRef())
遇到这种情况怎么办呢?我们提供了一个 $CacheRouteInjectPageElement 的方法,通过 props 传递给页面组件。
可以在页面组件的 constructor 中使用,把 this 给抛出去。这样就再也不用担心高阶函数中 ref 的问题了。
constructor(props) {
super(props);
props.$CacheRouteInjectPageElement(this);
}
display实现的缺陷
目前已经发现的,一个比较大的缺陷就是:
页面A使用了缓存。但是,在页面A中,使用了一个 BottomBar组件。最关键的是,这个 <BottomBar /> 是通过 ReactDom.createPortal 创建的,可能是挂载到了 document.body 下面。
这样的话,通过简单的 display 可以实现页面A的部分显隐,但是那个 <BottomBar /> 是控制不住的……
当然,对于这个缺陷,目前方案是:提供 getPageVisible 这样一个方法,通过 props 传递给页面组件。页面组件能够通过调用这个方法,获取当前页面的显隐性。再去页面组件中,手动控制 <BottomBar /> 的显隐。
未完待续……