-
Notifications
You must be signed in to change notification settings - Fork 53
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
为 react-router 写一个可以缓存的 Route #23
Comments
支持一个,mark |
支持,之前在项目中也意识到这个问题,当时没有找到楼主的方案,自己也实现了一套 思路比较简单,Route 部分没有重造,利用原有 Route Switch 部分在业务上有要求,所以有必要支持 Switch 中的缓存路由功能,方法是继承原有 Switch 并覆写 render 部分实现支持 目前存在的最大问题,难以突破的瓶颈是,如果是嵌套路由中,上层路由是原有 Route,下层路由的缓存行为便无法自控,上层 Route 如果被卸载,内部的缓存路由也将全部卸载... 由此看来,最好还是 React 本身能给出官方的 keep-alive 方案,实现真实 dom 节点的缓存和复用... 最后贴上自己的方案地址,与楼主有幸作为同一问题的探讨者 |
@CJY0208 |
见过两个例子,一个是 react-keeper,一个是 taro 编译出来的 h5 页面,他们都是通过隐藏页面来实现的。
|
@jamieYou 使用 react-router-cache-route,这 3 个问题都可以解决
楼主的方案相同,理论上也可以解决 |
@CJY0208 主要是解决起来很多功夫,为了做缓存反而多了其他坑出来维护,感觉不划算。 |
在 Fiber 架构下,实现 keep-alive 是有可能的,保持对应那部分的 Fiber tree 不被卸载即可。 |
@jamieYou 木有希望,官方目前明确表示不会支持,官方认为 keep-alive 带有缓存性质,是一个不纯行为,除了我们自己魔改拓展,目前别无他法 |
@CJY0208 好吧,在函数式面前凉凉 |
@fi3ework 可以理解的啦 官方对 作为基础、具有函数式特征、兼容 ssr,从这几点来看的话,官方确实没必要为了某些便捷性而给 但也并没有完全阻止我们的魔改之路~~哈哈,不然也不会有今天的讨论了 @jamieYou 另外还找到了一个不一样的实现方案 react-keep-alive 原理大致是利用 但测试中似乎无法保留滚动位置,可能和 |
mark一下。 |
前言
在 上一篇文章 中介绍了前端路由的实现及 react-router-v4(以下简称 rr4) 的源码分析,目前阶段 rr4 已经基本垄断了 react 生态圈的路由,虽然 v4 版本成功完成了一切皆组件的蜕变,但其实它本身还有诸多问题,比如 keep-alive。
keep-alive 的叫法取自 vue-keep-alive,在 vue 中,可以将某组件暂存于内存,然后跳转到其他页面再从内存中将这个组件拿出来。换算到路由中,我们可以想象这样一个情景 —— 有一个商品列表页,每个商品点进去都跳转到对应的商品详情页面,用户每次浏览完一个商品详情之后回退,列表页会重新渲染,那么如果用户已经往下划了几屏之后回退,那么每次返回后都要先滑到上次浏览的位置,这种体验可以说是灾难性的。
现在的浏览器非常贴心的实现了 Scroll Restoration(后退时恢复滚动位置),这在非 SPA 页面有非常好的体验效果,但是在 SPA 中,会有以下问题:
popstate
事件并触发 scroll restoration,通过点击链接无法触发滚动恢复。其实 iOS 和 Android 端的路由转换是十分理想的 —— 支持转场动画,手势返回,keep-alive。
本文中我们试图解决为 rr4 实现一个可以缓存的 Route 来解决上面例子中的问题,并借此探索一下 rr4 目前阶段的不足之处及可以加强的地方。说句题外话,rr4 的核心开发者又新搞了一个 reach-router 路由库,针对 rr4 的缺点进行了针对性的改进,已经钦点了是下一代的路由旗舰管理库。
轮子
先放上我造的轮子的仓库地址 react-live-route 感受一下本文的最终目的,react-live-route 可以使路由在路径不匹配时隐藏而不被卸载,在匹配路径时完全恢复离开页面时的样子。欢迎 star 和提 issue。
PC 端可以预览 demo
移动端扫码试玩
(点一下玩一年)思路
我们先重新将要解决的问题整理一下:
其中我们要恢复的状态:
并且要做到无痛兼容 rr4,侵入性越小越好。我们的目标是为 react-router 设计一个增强型的 Route 组件,可以像 iOS 和 Android 端的路由切换一样“隐藏”上一个导航的页面,在这里有两种解决问题的思路:
思路1
unmount 时储存状态,re-mount 时取回状态
在列表页将要 unmount 的时候,将需要保留的数据状态存在 context(或者 window.sessionStorage 等等)
**优点:**可以在 unmount 和 re-mount 时利用生命周期。
缺点:
思路2
不 unmount,只是根据路由隐藏/显示对应页面
在切换到详情页的路径时,不将列表页 unmount,而是
display: none
掉它,在从详情页返回列表页的时候,再display: block
将列表页显示回来。优点: 简单粗暴,因为没有卸载组件,所以可以不用管页面的数据状态的保存情况。只需要管理好恢复显示、隐藏与正常 re-render,再恢复滚动位置即可。
缺点: 配合转场动画可能会有问题。
由于思路 1 的实现有很大的局限性,所以按照思路 2 来进行实现。
实现
增强的 Route 组件称为 LiveRoute,我们首先要确定,这个增强组件在什么情况下起作用,以及它有哪几种状态,react-router 有一篇关于 Scroll Restoration 的文章 ,是关于 react-router 去除了滚动恢复的功能的原因,其中有提到原因:
就是因为实际的应用情况太多变,他们无法合适的判断什么时候需要进行滚动恢复的管理。
在一开始我是打算使用成对的路由来实现,其中一个 LiveRoute 的存活状态去控制另一个需要保留存活的 LiveRoute:
但是路由间需要在 router 上创建 context 来辅助通信,如下是 react-router 正常更新一次的流程,路由间的通信会再一次触发被通知的路由的 setState,这是无法避免的,但是 Route 作为整个应用中非常靠上的组件,副作用要尽可能的小。
换个思路,其实缓存页面的匹配规则就是控制页面的隐藏/恢复显示与正常卸载,而 rr4 正常的路由匹配规则就是控制渲染/卸载,通过
path
这个 props 来完成。那么我们直接给 LiveRoute 一个额外的来控制隐藏/恢复显示的livePath
的路径即可,其规则就可以直接套用path
,当路由livePath
匹配时,则处于隐藏状态,其他路径则按照 rr4 的规则正常渲染/卸载。调用方法:如此一来,LiveRoute 显示状态的依赖变为
context.router
,这样做的好处是依赖变的简单,所有的路由都会“同时”获得依赖的更新,并且相互之间没有耦合。LiveRoute 状态
LiveRoute 内部有一个状态机,有三种渲染组件的状态:
HIDE_RENDER
:livePath 匹配则需要将 LiveRoute 渲染的组件隐藏掉。进入此状态时需要备份页面的滚动位置,然后通过ReactDOM.findDOMNode
来获取路由渲染的组件的 DOM,将dom.style.display = 'none'
,并备份修改之前的 display 的属性。NORMAL_RENDER_MATCH
:路由正常渲染并且匹配上了。调用原版 Route 的渲染方法即可但是在每次正常匹配渲染的时候都要保存当前的
context.router
,作为之后隐藏渲染时需要保持渲染所需的 router,在 componnetDidUpdate 后查看有没有备份的滚动位置,如果有就恢复滚动位置并清除备份的滚动位置。NORMAL_RENDER_UNMATCH
:正常渲染但是不匹配,即要卸载当前路由的组件。要做的就比较简单了,清空 LiveRoute 中保存的 DOM 的引用,清除掉保存的滚动位置,然后调用原版的的 Route 的渲染方法(卸载)即可。实现细节
如何保护路由渲染的组件存活
当
router
与livePath 匹配
的时候需要将 LiveRoute 置为隐藏状态。但是新的 router 传入必然会计算出一个新的 match 去 setState,而新的 setState 与当前的 path 并不匹配,所以 LiveRoute 每次隐藏渲染时需要在 componentWillReceiveProps 中计算上次的 prevMatch。
在 render 的部分,需要当前的 router 在计算传递给组件的 props,所以需要在最后一次正常渲染的时候保存当前的 router。
最后,将 prevMatch 作为 setState 的 match,再拿出之前保存的 _prevRoute 完成渲染,一句话说就是将最后一次正常渲染的参数给保留了下来并在需要隐藏的时候拿出来伪装成最后一次正常渲染,再将 DOM 隐藏就完成了核心功能。
保存滚动位置
由于 LiveRoute 拦截了路由的卸载,所以滚动位置不需要再存储在全局的 sessionStorage 中,LiveRoute 会一直存活,滚动位置直接可以保存为 LiveRoute 的属性。并且,相比 sessionStorage 必须先
JSON.stringify()
保存对象的操作,有了更高的可拓展性。Switch
有一个问题就是与 Switch 的不兼容性,这个是采用
display:none
这种方法无法避免的,我也在 文档 中写到了。因为 Switch 的目的就是仅渲染第一个匹配的子元素,而 LiveRoute 的目的是强行渲染不匹配的子元素,所以不能在 Switch 中直接嵌套一个 LiveRoute 来使用。解决方法也简单,就是将 LiveRoute 从 Switch 中拿到外面来,不要让 LiveRoute 和 Switch 相互干扰,但是要注意此时 LiveRoute 的渲染与否也失去了 Switch 的跳过功能了。滚动位置的不变性
在一些情况下 LiveRoute 的 DOM 将会被直接修改,所以在切换路由时滚动位置将不会改变而界面已经发生改变。这并不是 react-live-route 带来的问题,你可以手动将页面滚动到顶部,这篇 react-router 提供的 教学文章 中可以提供一些帮助。另外,如果 LiveRoute 将要恢复滚动位置,由于 React 的渲染顺序,它将发生在 LiveRoute 渲染的组件的滚动操作之后发生(滚动操作发生在 componentDidMount 或 componentDidUpdate 中)。
总结
react-live-route 实现了路由的缓存及复原,但是还有一些其他的问题需要解决,比如与转场动画的兼容性及
给 LivePath 传入一个数组来实现多规则匹配的问题。(因为使用的是 react-router 的 computePath 方法解析 path,所以默认支持传入数组,具体详见 path-to-regexp 的 文档)最后再放上 react-live-route 的仓库地址 react-live-route,欢迎 star 和提出 issue。
参考
The text was updated successfully, but these errors were encountered: