一开始的思路是在列表页面, 利用useEffect钩子监听location, 在清除函数里获取滚动条的高度, 可惜获取时机好像不对, 一直获取的高度都是0, 通过打印body的innterHTML发现, 这时候路由已经开始渲染详情页了
然后开始尝试使用addEventListener监听scroll, 代码如下:
export function useSaveScroll(key: string) {
const location = useLocation()
const pathname = location.pathname
useEffect(() => {
const handleScroll = () => {
ls.set(`scroll_path_${pathname}`, window.scrollY)
}
const scrollY = ls.get(`scroll_path_${pathname}`) || 0
window.scrollTo(0, scrollY)
ls.set(`scroll_path_${pathname}`, 0)
window.addEventListener('scroll', handleScroll) // 添加滚动事件监听
return () => {
window.removeEventListener('scroll', handleScroll) // 组件卸载时移除事件监听
}
}, [])
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
结果发现还是有问题, removeEventListener的时机和跳转到详情后渲染的时机还是有先后的问题, 就是说开始渲染详情页时, 还没有把列表的监听滚动事件移除, 导致开始渲染详情页时, 滚动条会因页面高度不够, 自动变成0, 这个变成0还是被列表页的监听滚动事件捕抓到了
最终考虑, 在保存滚动条位置时, 添加防抖, 这个防抖不管怎么样都是必须加的, 不然滚动速度快的, 会频繁保存滚动位置
export function useSaveScroll(key: string) {
const location = useLocation()
const pathname = location.pathname
useEffect(() => {
const handleScroll = () => {
if (window.$timeout[key])
clearTimeout(window.$timeout[key])
window.$timeout[key] = setTimeout(() => {
console.log(window.scrollY)
ls.set(`scroll_path_${pathname}`, window.scrollY)
}, 200)
}
const scrollY = ls.get(`scroll_path_${pathname}`) || 0
window.scrollTo(0, scrollY)
ls.set(`scroll_path_${pathname}`, 0)
window.addEventListener('scroll', handleScroll) // 添加滚动事件监听
return () => {
window.removeEventListener('scroll', handleScroll) // 组件卸载时移除事件监听
}
}, [])
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
当然, 只是加个防抖的话, 前面说的问题依然存在, 但是有这个延迟, 我们就可以在详情页做点什么了, 比如把最后一次的延迟清除了
if (window.$timeout.list)
clearTimeout(window.$timeout.list)
2
最终代码如下:
// src/composables/index.ts
import { useLocation} from 'react-router-dom'
import ls from 'store2'
export function useSaveScroll(key: string) {
const location = useLocation()
const pathname = location.pathname
useEffect(() => {
const handleScroll = () => {
if (window.$timeout[key])
clearTimeout(window.$timeout[key])
window.$timeout[key] = setTimeout(() => {
console.log(window.scrollY)
ls.set(`scroll_path_${pathname}`, window.scrollY)
}, 200)
}
const scrollY = ls.get(`scroll_path_${pathname}`) || 0
window.scrollTo(0, scrollY)
ls.set(`scroll_path_${pathname}`, 0)
window.addEventListener('scroll', handleScroll) // 添加滚动事件监听
return () => {
window.removeEventListener('scroll', handleScroll) // 组件卸载时移除事件监听
}
}, [])
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// src/pages/topics/index.tsx
import { observer } from 'mobx-react-lite'
import { useAutoScroll } from '~/composables'
const Main = observer(() => {
// ...其他代码
useSaveScroll('list')
// ...其他代码
})
export default Main
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// src/pages/article/index.tsx
import { observer } from 'mobx-react-lite'
const PageArticle = observer(() => {
if (window.$timeout.list)
clearTimeout(window.$timeout.list)
// ...其他代码
})
export default PageArticle
2
3
4
5
6
7
8
9
10
11
12
警告
以下方法在新版本react和react-router中已经失效
需求: 1、需要路由切换动画, 2、从详情页返回列表页时, 需要滚动到历史位置
咋一看, 这不挺简单的吗?
使用redux做数据的临时存储, 在componentWillUnmount钩子里, 读取滚动条位置, 然后保存到本地存储
当从详情页或者其他页面返回列表页时, 判断redux里是否有数据, 如果有数据直接读取, 不再发起请求, 在componentDidMount钩子里读取本地存储的滚动条位置, 然后滚动到相应位置不就行了吗
上面的思路在没有路由动画时, 是可行的, 但是有路由动画的话, 就不行了, 你会发现在componentWillUnmount钩子里取到的滚动条位置, 总是不对的
主要原因是, 为了用户体验, 在详情页需要componentDidMount钩子里将滚动条设置为0, 不然在进入详情页时, 滚动条位置会不在顶部
再加上, 添加了路由动画后, 从列表页进入详情页, 有一小段时间, 列表页和详情页这两个组件会同时存在, 而且两个页面钩子的执行顺序为: 详情页的componentDidMount => 列表页的componentWillUnmount, 所以问题看的出来了
说了半天废话, 还是说解决方案吧, 依靠 react 的钩子是不行了, 但是还可以靠路由的Prompt组件
import { Prompt } from 'react-router-dom'
render() {
return (
<div className="main wrap">
<Prompt
when
message={() => {
const path = this.props.location.pathname
const scrollTop = Math.max(window.pageYOffset, document.documentElement.scrollTop, document.body.scrollTop)
ls.set(path, scrollTop)
return true
}}
/>
<div>.........</div>
</div>
)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
滚动条的位置是记录下来, 接下来的问题就是这么还原到对应位置, 这里以redux为例:
// 空数据时
this.props.topics => {
data: [],
hasNext: 0,
page: 1,
pathname: ''
}
// 有数据时
this.props.topics => {
data: [{...}, {...}, {...}],
hasNext: 0,
page: 1,
pathname: '/'
}
2
3
4
5
6
7
8
9
10
11
12
13
14
1, redux 已存在列表数据
那么只要在componentDidMount读取滚动条位置, 并滚动即可
componentDidMount() {
console.log(`topics: componentDidMount`)
const pathname = this.props.location.pathname
if (this.props.topics.pathname !== '') {
const scrollTop = ls.get(pathname) || 0
ls.remove(pathname)
window.scrollTo(0, scrollTop)
}
}
2
3
4
5
6
7
8
9
2, redux 不存在列表数据, 需要异步请求时, 通过componentDidUpdate钩子判断列表是否渲染完成
componentDidUpdate(prevProps) {
const pathname = this.props.location.pathname
if (this.props.topics.pathname !== '' && this.props.topics.pathname !== prevProps.topics.pathname) {
const scrollTop = ls.get(pathname) || 0
ls.remove(pathname)
window.scrollTo(0, scrollTop)
}
}
2
3
4
5
6
7
8