一开始的思路是在列表页面, 利用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