解决路由切换的滚动条异常问题

755 阅读6分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第4天,点击查看活动详情

前言

对于路由切换导致的滚动条异常问题,在之前的篇章中我们也测试了,正常情况下,push页面会保留滚动条位置,而前进后退的滚动条记录由浏览器维护着,当回退的页面初始页面存在滚动高度,则滚动条可以复原到原来的滚动位置,当回退的页面初始时不存在滚动高度,则直接导致滚动条置顶了,这也导致了有时候回退页面滚动条位置和之前的位置不一样的现象。

工程介绍

基于V5路由的react工程,大致目录结构如下:

//App入口
import React from "react";

import {
  BrowserRouter as Router,
  Switch,
  Route,
  Link,
} from "react-router-dom";

import Home from "./components/Home"
import About from "./components/About"
import Users from "./components/Users"

export default function App() {
  return (
    <Router>
        <div>
          <nav>
            <ul>
              <li>
                <Link to="/">Home</Link>
              </li>
              <li>
                <Link to="/about">About</Link>
              </li>
              <li>
                <Link to="/users">Users</Link>
              </li>
            </ul>
          </nav>


          <Switch>
            <Route path="/" exact >
              <Home />
            </Route>
            <Route path="/about" exact >
              <About />
            </Route>
            <Route path="/users" exact >
              <Users />
            </Route>
          </Switch>
          
        </div>
    </Router>
  );
}

//home页面
import { useEffect, useState } from "react"
import { NavLink } from "react-router-dom"
const Home = () => {
    const [display, setDisplay] = useState(false)

    useEffect(() => {
        setTimeout(() => {
            setDisplay(true)
        }, 500)


    }, [])
    return <>
        {
            display ? (<>
                <div style={{
                    width: '500px',
                    background: 'red',
                    height: "400vh",
                    display: display ? 'block' : 'none'
                }}>

                </div>

                <NavLink
                    to="/about"
                >
                    about
                </NavLink>
                <NavLink
                    to="/users"
                >
                    user
                </NavLink>

                <div style={{

                    width: '500px',
                    background: 'red',
                    height: "400vh",
                    display: display ? 'block' : 'none'
                }}>

                </div>
            </>) : null
        }
    </>
}

export default Home
//about页面
import {  useState,useEffect } from "react"
import { NavLink } from "react-router-dom"
const About = (props) => {
 
    const [state,setState] = useState([])
    useEffect(() => {
        console.log(window.scrollY)
        setTimeout(() => {
            setState([1, 2, 3, 4, 5, 6, 7, 8, 9,10,11,12,13,14])
            
            //此处延迟是为了得到渲染完毕后的页面的scrollY
            setTimeout(()=>{
                console.log(window.scrollY)
            },1000)
        },500)
    }, [])

 
    return <>
        <div style={{
            width: '500px',
            background: 'green',
            height: "150vh"
        }}>

            <NavLink
                to="/"
            >
                home
            </NavLink>
        </div>

        <ul id="ul">
            {state.map((item,index)=>
              <li key={index} style={{
                  width:'500px',
                  height:'500px',
                  background:"green"
              }}>{item}</li>
            )}
        </ul>
    </>
}

export default About

我们从主页下滑,滚动到/about页面的入口,此时我们控制台打印一下滚动位置:

image.png

点击进入/about页面:

image.png

我们看到了当我们在地址栏push一个新地址时,由于新页面的初始高度不够,为了复原上一个页面的滚动条,只能将滚动条推至底部,而当数据请求到来时,页面更改,所以我们的页面高度足够了,滚动条会滚动至上一个页面的滚动条高度,这一行为这是浏览器保留的。

在之前的测试我们也知道,如果push新地址的页面初始高度不存在滚动条,即使后面数据请求到了页面高度足够了,那也只能是置顶的结果。

解决方案

设置回退页面初始高度

我们知道,回退之所以滚动条不能复原是因为初始化的高度不够,假设能够保留历史记录页面渲染完毕时的页面高度,那是否就可以使得滚动条复原呢?

针对这样的猜想,我们可以让进入的时候页面高度超过100vh,然后当页面渲染完毕之后再把高度还原到真实节点撑开的高度。而这个猜想的难点在于,我们无法判断这个真实节点什么时候渲染完毕,假如这个页面存在请求驱使dom渲染,而请求的时长可能是1~3s甚至更长。而实际上浏览器在渲染时判断这个初始高度也是这样思考的,因为并不知道异步渲染的页面即使等待异步渲染完毕是否也是存在滚动条。

不过,从用户的感知上看,我们把这个初始超过100vh到高度还原的过程设置成1s,在用户体验上勉强是可以接受的。

所以我们可以使用延时计时器设置初始高度,1s之后如果请求还没到,那就当这个页面的高度就是此时的高度。

针对延时器的设置,我们可以通过监听history来进行,为了方便每次页面渲染进行,这里通过路由拦截写了一层,代码如下:

import React from "react";
import { useHistory } from "react-router-dom"

import {
  BrowserRouter as Router,
  Switch,
  Route,
  Link,
} from "react-router-dom";

import Home from "./components/Home"
import About from "./components/About"
import Users from "./components/Users"


const MyRoute = (props) => {
  const history = useHistory();

  history.listen((e, type) => {
    if (type === 'PUSH') {
      setTimeout(() => {
        window.scrollTo(0, 0)
      }, 0)
    } else {
      document.body.style.height = '999vh'
      setTimeout(() => {
        document.body.style.height = 'auto'
      }, 1000)
    }
  })


  return props.children
}


export default function App() {
  return (
    <Router>
      <MyRoute>
        <div>
          <nav>
            <ul>
              <li>
                <Link to="/">Home</Link>
              </li>
              <li>
                <Link to="/about">About</Link>
              </li>
              <li>
                <Link to="/users">Users</Link>
              </li>
            </ul>
          </nav>


          <Switch>
            <Route path="/" exact >
              <Home />
            </Route>
            <Route path="/about" exact >
              <About />
            </Route>
            <Route path="/users" exact >
              <Users />
            </Route>

          </Switch>
        </div>
      </MyRoute>

    </Router>
  );
}



我们重复之前的操作:

image.png

image.png

image.png

上面的代码基本实现了后退复原,新push页面复原滚动条的逻辑。

不过,上面的页面存在的问题时,当请求的时长超过了计时器的设置的时长,那么滚动条停留的位置只能是计时器执行时读取到的页面高度,如果高度不够,那么只能是滚动至此时的页面高度,后续的页面增长并不会让滚动条继续回滚。

保存旧节点

针对高度不确定问题,实际我们可以针对路由行为采取保留dom节点的方法。

我们知道常见的前端框架渲染页面无非是采用window上的api去remove节点或者append节点。而我们要做的是把这种行为采取display的处理,在视觉上实现节点的增删。

为此我们可以采用路由拦截children的方法,通过标记缓存对应的节点,通过设置其display实现回退页面节点显示。

缓存节点的好处除了能让滚动条回滚(因为缓存的页面高度是已知的,如果当时存在滚动条,那么回滚的页面必然是有滚动条,浏览器自然能回滚到记录的位置),还能缓存当时的页面状态等。

此时我们要介绍的便是,社区提供提供的第三方库** react-router-cache-route**,我们先来了解一下该库的使用。

image.png

yarn add react-router-cache-route

image.png

image.png

image.png

除此之外,该库还提供了钩子函数,在缓存释放与被缓存都有对应的钩子。通过节点的复用能够更好的处理滚动条复原问题,针对新push页面的逻辑处理,该库也并不能决定其处理行为,所以可以通过配合history的监听去置顶滚动条或者让原生去复用滚动位置等。

注意的点:

每次页面切换,实际上是否请求应该配合其提供的钩子去决定,因为有可能新的状态页面复用了之前的状态,这个时候就需要去请求数据了。

小结

本篇我们介绍了如何解决在react路由下的状态缓存问题,通过缓存相应的dom节点,实现状态的缓存。

滚动条的状态回退得益于dom节点的缓存使得初始页面存在滚动条高度,从而避免了置顶的情况。