React中实现返回列表时保持滚动位置scrollTop不变(一)

1. 介绍

工作中碰到一个需求,在后台管理系统中,从列表页点击查看详情,然后再点击浏览器的返回列表,希望保持列表页的scrollTop

目前的表现是,回到列表页总是回到顶部

我在网上查找的大部分方法都是在列表页监听scrollTop,在页面卸载的时候,保存当时的列表数据和scrollTop到状态管理(或者storage中),返回的时候读取状态管理(或者storage)中的列表数据和scrollTop回显出来

但是我觉得这种操作很麻烦,而且我印象浏览器自带的特性就是可以保存scrollTop的

2. 浏览器自带的特性

于是我先进行了一波印证,不用任何现代前端框架,看看浏览器自带的特性是啥样的

用原生html写了两个最简单的页面

  • index.html 是一个itemList的列表,点击任何一个item都会跳转到detail
  • detail.html 是一个超长的详情页,拥有2个按钮,一个是返回,一个是跳转

index.html

html 复制代码
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <style>
      .list {
        overflow: hidden;
      }
      .item {
        width: 100px;
        height: 300px;
        border: 1px solid #000;
        margin: 10px;
        padding: 5px;
      }
    </style>
    <script>
      function goDetail() {
        window.location.href = './detail.html'
      }

      window.onload = function () {
        // 给list增加 20 个item
        var list = document.querySelector('.list')
        for (var i = 0; i < 20; i++) {
          var item = document.createElement('div')
          item.className = 'item'
          item.innerText = `item ${i} \n 点击跳转到详情页`

          // 点击方法
          item.onclick = function () {
            goDetail()
          }

          list.appendChild(item)
        }
      }
    </script>
  </head>
  <body>
    <div class="list"></div>
  </body>
</html>

detail.html

html 复制代码
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <style>
      .box {
        width: 90px;
        height: 1999px;
        background: rosybrown;
      }
    </style>
  </head>
  <body>
    <div style="position: fixed; top: 20px; right: 10px; width: 100px">
      <button onclick="history.back(-1)">返回列表页</button>
      <button onclick="window.location.href = './index.html'">跳转列表页</button>
    </div>
    <div class="box" onclick="history.back(-1)">这是deal页</div>
  </body>
</html>

2.1 浏览器自带的特性-结论

2.1.1 结论1

  1. 浏览器自带的history.back(-1)本身就是可以保持scrollTop的
  2. 如果是用 window.location.href = './index.html' 则会重置scrollTop

如下图:

2.1.2 结论2

我又进行了一个额外的测试:

在列表页(index.html)onload中加上setTimeout 去创建items,滚动条是无法保存的

index.html

js 复制代码
  window.onload = function () {
    console.log(`🚀 ~ onload`)

    setTimeout(() => {
      // 给list增加 20 个item
      var list = document.querySelector('.list')
      for (var i = 0; i < 20; i++) {
        var item = document.createElement('div')
        item.className = 'item'
        item.innerText = `item ${i} \n 点击跳转到详情页`

        // 点击方法
        item.onclick = function () {
          goDetail()
        }

        list.appendChild(item)
      }
    }, 12)
  }

如下图:

2.1.3 结论3(重要!)

注意:以上2个测试,页面右侧滚动条都是body的滚动条,事实上对于一个普通html,如果我们不加任何样式干预,浏览器页面的滚动条其实都是body的

如果我把body overflow: hidden;

让产生滚动的容器是 list,会有什么表现呢

index.html 增加以下样式:

html 复制代码
<style>
  html,
  body {
    height: 100%;
    overflow: hidden;
  }
  .list {
    height: 100%;
    overflow: auto; // 让list作为滚动容器
  }
  .item {
    width: 100px;
    height: 300px;
    border: 1px solid #000;
    margin: 10px;
    padding: 5px;
  }
</style>
  • 结论:只有当滚动容器是body的时候,浏览器自带的特性才会记住它的scrollTop

如下图,此时滚动容器是list:

2.2 浏览器自带的特性-总结

如果想要页面可以通过浏览器自带的特性进行保存scrollTop,要满足

  1. 通过history.back触发,或者通过浏览器自带的返回键触发
  2. 返回页面时,要马上恢复数据
  3. 滚动的容器一定是body

3. React 的表现

3.1 React原始的表现

因为项目中,有太多的干扰项,我先建一个干净的react项目,只增加react-router-dom依赖包,想要看看react最原始的表现如何

我直接用vite官网的模板创建的,只安装了react-router-dom,相关版本请看package.json

package.json:

同样的,我创建了一个列表页index和一个详情页detail,和之前样式都是一样的

可以通过CodeSandBox访问:

codesandbox.io/p/github/ji...

3.2 React原始的表现-结论

3.2.1 结论1:body作为滚动容器

  1. react + react router本身也是可以保存scrollTop的

  2. 但是有些区别:

    2.1 在react中,从index跳转到detail时,detail的scrollTop不会回到顶部

    2.2 在detail中直接跳转(不调用navigate(-1),而是调用navigate(path)),回到index页时,不会重置scrollTop

可以认为:body作为滚动容器时,react本身的特性是在跳转中永远都会保持scrollTop

如下图:

3.2.2 结论2:body作为滚动容器,使用useEffect,useState初始值[]

代码进行一些修改:

结论:

如果使用了useState + useEffect ,并且useState初始值是 []的话,返回index页时,scrollTop总会回到顶部。

我猜测是因为useEffect的执行时机是在dom渲染结束之后,此时dom已经渲染过一次了(scrollTop已经归零),再setList scrollTop肯定不会回到原位了

效果如下图:

3.2.3 结论3:body作为滚动容器

使用useEffect,useState初始值defaultList

结论: 设置useState初始值,即在dom渲染之前设置好list,表现和结论1时是一样的:在跳转中永远都会保持scrollTop

使用useLayoutEffect也是同样的效果

codesandbox.io/p/github/ji...

3.2.4 结论4:使用非body元素作为滚动容器会是什么表现呢

我加了一个 div.page 设置为 height:100vh; overflow: auto; 同时 body设置为overflow: hidden;

结果和原生浏览器是一样的:只要是非body作为滚动容器,都不会保持scrollTop

codesandbox.io/p/github/ji...

4. 最终的结论

  1. 浏览器本身就有保持scrollTop的特性
  2. 前提是一定要body作为滚动容器
  3. React同样也有,区别是前进的时候不会重置scrollTop到顶部(任何跳转都会保持scrollTop)

我们在项目中大部分的场景都没保持scrollTop,大概率是因为:1.可能不是body作为滚动容器;2. 返回到列表页时,重新进行请求,并没有缓存之前的列表数据,列表数据经历了从空数组[] 到重新赋值的过程,在这个过程中scrollTop已经重置到顶部了。

得知了这些,我们要做的就是:

  1. 保证列表页的滚动容器是body
  2. 缓存列表页的数据到状态管理,返回列表页的时候,直接使用缓存的数据
  3. 处理React中前进(跳转)时,总会保持scrollTop的问题

接下来请看第二篇的内容:

React中实现返回列表时保持滚动位置scrollTop不变(二)

相关推荐
萌萌哒草头将军4 分钟前
🔥🔥🔥 NuxtLabs 宣布加入了 Vercel !
前端·javascript·vue.js
LuciferHuang7 小时前
震惊!三万star开源项目竟有致命Bug?
前端·javascript·debug
GISer_Jing7 小时前
前端实习总结——案例与大纲
前端·javascript
天天进步20158 小时前
前端工程化:Webpack从入门到精通
前端·webpack·node.js
姑苏洛言8 小时前
编写产品需求文档:黄历日历小程序
前端·javascript·后端
知识分享小能手9 小时前
Vue3 学习教程,从入门到精通,使用 VSCode 开发 Vue3 的详细指南(3)
前端·javascript·vue.js·学习·前端框架·vue·vue3
姑苏洛言9 小时前
搭建一款结合传统黄历功能的日历小程序
前端·javascript·后端
你的人类朋友10 小时前
🤔什么时候用BFF架构?
前端·javascript·后端
知识分享小能手11 小时前
Bootstrap 5学习教程,从入门到精通,Bootstrap 5 表单验证语法知识点及案例代码(34)
前端·javascript·学习·typescript·bootstrap·html·css3