React SPA中锚点链接在Safari浏览器无法正常滚动的问题
Safari v15.6.1中React MUI SPA锚点滚动失效问题解决
问题概述
开发基于React MUI的单页应用,设置了锚点链接用于跳转至指定板块,在Chrome、Firefox中表现正常,但在Safari v15.6.1中锚点无法正确滚动定位。仅在满足以下三个条件时触发问题:
- MUI TextField组件保留
multiline属性 - 使用本地图片资源
- 元素样式使用
maxWidth而非width
临时通过setTimeout延迟5秒执行scrollIntoView可正常工作,但这并非可靠解决方案。
问题根源
Safari在页面资源(尤其是图片)未完全加载、组件(如MUI多行文本框)未完成布局渲染时,执行scrollIntoView会因为锚点元素的位置计算错误,导致滚动定位失效。maxWidth属性让元素宽度依赖视口或父容器,图片加载会动态改变页面高度,再加上MUI多行文本框可能存在异步布局调整,进一步加剧了初始定位的偏差。
解决方案
方案1:等待页面所有资源加载完成后执行滚动
通过监听window.load事件,确保图片、样式等所有资源加载完毕后再执行滚动:
import {Grid, TextField, Typography} from "@mui/material" import React, {useEffect} from "react"; import image from "./Box.jpg" const TallBox = (props: {id: string, color: string}) => ( <> <Typography variant="h4" id={props.id} gutterBottom>{props.id}</Typography> <Typography style={{maxWidth: "400px", border: `1px solid ${props.color}`}} gutterBottom> Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec ac tellus nisi. Sed elementum ornare justo, vel ullamcorper justo aliquam ac. Vestibulum euismod faucibus quam ut pulvinar. Proin auctor, elit ut tempor pretium, nunc lacus ornare urna, eget egestas diam velit id enim. Curabitur gravida sed elit at sollicitudin. Integer ultricies diam blandit felis euismod interdum. Ut malesuada erat id malesuada laoreet. Aenean tortor lacus, blandit non consectetur quis, eleifend at urna. Ut elementum sem et tincidunt aliquam. Donec fermentum dictum interdum. Aliquam eget ullamcorper dolor, a tincidunt mauris. Quisque pulvinar luctus erat, ut dignissim purus ultricies id. Pellentesque molestie mi nulla, quis convallis magna varius a. Curabitur ultrices lobortis massa, a consequat justo tincidunt a. Praesent sollicitudin ligula ut lectus condimentum tincidunt. Cras euismod molestie ullamcorper. Aliquam in vestibulum ex, placerat interdum nisl. Vestibulum quis mattis est. Nam dui diam, sagittis non neque eget, laoreet pharetra augue. Pellentesque in ipsum in ligula volutpat pellentesque ut vel sapien. Morbi ac posuere urna. Quisque et pellentesque mi. Sed auctor massa et neque commodo rhoncus nec quis est. Aenean et ligula dolor. Duis fringilla a justo nec vehicula. Pellentesque blandit sed nibh porttitor faucibus. Phasellus nisi erat, eleifend eu sem vel, convallis pharetra purus. Cras sagittis est at suscipit hendrerit. Mauris volutpat lectus eu mi facilisis, in gravida odio aliquet. Proin ut viverra tortor. Ut commodo felis ut ullamcorper fermentum. Donec ultricies dictum euismod. Suspendisse sit amet purus dui. Nulla vestibulum enim tristique commodo interdum. Aenean sollicitudin lacinia placerat. Morbi magna purus, sollicitudin et purus non, scelerisque auctor lectus. Fusce hendrerit est at dui consectetur iaculis. Maecenas placerat luctus blandit. Aenean ac risus a tellus dictum mattis. Nullam sapien metus, imperdiet eget neque quis, convallis vulputate odio. Ut magna massa, posuere eu efficitur eu, semper ut enim. Morbi tincidunt consectetur quam, eget porta mi tincidunt et. Duis lacinia lectus et lobortis malesuada. Duis bibendum tristique nulla. Maecenas tincidunt faucibus commodo. Pellentesque in tincidunt libero. Vestibulum ac fermentum erat, a ultricies mi. Nam vitae tortor quis lorem imperdiet gravida non at sapien. Fusce tincidunt maximus sapien quis porta. Donec id sapien vel enim imperdiet rutrum non eget arcu. Cras egestas vel mi eget viverra. Ut mollis neque eros, at fringilla ligula semper at. Nulla volutpat maximus mollis. </Typography> </> ) export function App() { useEffect(() => { const scrollToAnchor = () => { if (window.location.hash) { document.querySelector(window.location.hash)?.scrollIntoView({ behavior: 'auto' // 可选,如需平滑滚动改为'smooth' }); } }; // 首次尝试执行,再监听load事件确保资源加载完成 scrollToAnchor(); window.addEventListener('load', scrollToAnchor); return () => window.removeEventListener('load', scrollToAnchor); }, []); return ( <Grid container flexDirection="column" alignItems="center" textAlign="center" spacing={2}> <Grid item> <Typography sx={{maxWidth: "600px"}}> <img src={image} alt="Box" style={{width: "100%"}}/> </Typography> </Grid> <Grid item> <TallBox id="section1" color="red"/> </Grid> <Grid item> <TallBox id="section2" color="blue"/> </Grid> <Grid item> <TextField name="text" label="text" multiline/> </Grid> </Grid> ) }
方案2:使用useLayoutEffect结合requestAnimationFrame
useLayoutEffect会在DOM布局更新后同步执行,搭配requestAnimationFrame确保在浏览器重绘后计算元素位置:
import {Grid, TextField, Typography} from "@mui/material" import React, {useLayoutEffect} from "react"; import image from "./Box.jpg" const TallBox = (props: {id: string, color: string}) => ( <> <Typography variant="h4" id={props.id} gutterBottom>{props.id}</Typography> <Typography style={{maxWidth: "400px", border: `1px solid ${props.color}`}} gutterBottom> Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec ac tellus nisi. Sed elementum ornare justo, vel ullamcorper justo aliquam ac. Vestibulum euismod faucibus quam ut pulvinar. Proin auctor, elit ut tempor pretium, nunc lacus ornare urna, eget egestas diam velit id enim. Curabitur gravida sed elit at sollicitudin. Integer ultricies diam blandit felis euismod interdum. Ut malesuada erat id malesuada laoreet. Aenean tortor lacus, blandit non consectetur quis, eleifend at urna. Ut elementum sem et tincidunt aliquam. Donec fermentum dictum interdum. Aliquam eget ullamcorper dolor, a tincidunt mauris. Quisque pulvinar luctus erat, ut dignissim purus ultricies id. Pellentesque molestie mi nulla, quis convallis magna varius a. Curabitur ultrices lobortis massa, a consequat justo tincidunt a. Praesent sollicitudin ligula ut lectus condimentum tincidunt. Cras euismod molestie ullamcorper. Aliquam in vestibulum ex, placerat interdum nisl. Vestibulum quis mattis est. Nam dui diam, sagittis non neque eget, laoreet pharetra augue. Pellentesque in ipsum in ligula volutpat pellentesque ut vel sapien. Morbi ac posuere urna. Quisque et pellentesque mi. Sed auctor massa et neque commodo rhoncus nec quis est. Aenean et ligula dolor. Duis fringilla a justo nec vehicula. Pellentesque blandit sed nibh porttitor faucibus. Phasellus nisi erat, eleifend eu sem vel, convallis pharetra purus. Cras sagittis est at suscipit hendrerit. Mauris volutpat lectus eu mi facilisis, in gravida odio aliquet. Proin ut viverra tortor. Ut commodo felis ut ullamcorper fermentum. Donec ultricies dictum euismod. Suspendisse sit amet purus dui. Nulla vestibulum enim tristique commodo interdum. Aenean sollicitudin lacinia placerat. Morbi magna purus, sollicitudin et purus non, scelerisque auctor lectus. Fusce hendrerit est at dui consectetur iaculis. Maecenas placerat luctus blandit. Aenean ac risus a tellus dictum mattis. Nullam sapien metus, imperdiet eget neque quis, convallis vulputate odio. Ut magna massa, posuere eu efficitur eu, semper ut enim. Morbi tincidunt consectetur quam, eget porta mi tincidunt et. Duis lacinia lectus et lobortis malesuada. Duis bibendum tristique nulla. Maecenas tincidunt faucibus commodo. Pellentesque in tincidunt libero. Vestibulum ac fermentum erat, a ultricies mi. Nam vitae tortor quis lorem imperdiet gravida non at sapien. Fusce tincidunt maximus sapien quis porta. Donec id sapien vel enim imperdiet rutrum non eget arcu. Cras egestas vel mi eget viverra. Ut mollis neque eros, at fringilla ligula semper at. Nulla volutpat maximus mollis. </Typography> </> ) export function App() { useLayoutEffect(() => { const scrollToAnchor = () => { if (window.location.hash) { const target = document.querySelector(window.location.hash); if (target) { requestAnimationFrame(() => { target.scrollIntoView(); }); } } }; scrollToAnchor(); }, []); return ( <Grid container flexDirection="column" alignItems="center" textAlign="center" spacing={2}> <Grid item> <Typography sx={{maxWidth: "600px"}}> <img src={image} alt="Box" style={{width: "100%"}}/> </Typography> </Grid> <Grid item> <TallBox id="section1" color="red"/> </Grid> <Grid item> <TallBox id="section2" color="blue"/> </Grid> <Grid item> <TextField name="text" label="text" multiline/> </Grid> </Grid> ) }
方案3:监听目标图片加载状态
针对本地图片资源,监听其加载完成事件,确认图片布局稳定后再执行滚动:
import {Grid, TextField, Typography} from "@mui/material" import React, {useState, useEffect} from "react"; import image from "./Box.jpg" const TallBox = (props: {id: string, color: string}) => ( <> <Typography variant="h4" id={props.id} gutterBottom>{props.id}</Typography> <Typography style={{maxWidth: "400px", border: `1px solid ${props.color}`}} gutterBottom> Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec ac tellus nisi. Sed elementum ornare justo, vel ullamcorper justo aliquam ac. Vestibulum euismod faucibus quam ut pulvinar. Proin auctor, elit ut tempor pretium, nunc lacus ornare urna, eget egestas diam velit id enim. Curabitur gravida sed elit at sollicitudin. Integer ultricies diam blandit felis euismod interdum. Ut malesuada erat id malesuada laoreet. Aenean tortor lacus, blandit non consectetur quis, eleifend at urna. Ut elementum sem et tincidunt aliquam. Donec fermentum dictum interdum. Aliquam eget ullamcorper dolor, a tincidunt mauris. Quisque pulvinar luctus erat, ut dignissim purus ultricies id. Pellentesque molestie mi nulla, quis convallis magna varius a. Curabitur ultrices lobortis massa, a consequat justo tincidunt a. Praesent sollicitudin ligula ut lectus condimentum tincidunt. Cras euismod molestie ullamcorper. Aliquam in vestibulum ex, placerat interdum nisl. Vestibulum quis mattis est. Nam dui diam, sagittis non neque eget, laoreet pharetra augue. Pellentesque in ipsum in ligula volutpat pellentesque ut vel sapien. Morbi ac posuere urna. Quisque et pellentesque mi. Sed auctor massa et neque commodo rhoncus nec quis est. Aenean et ligula dolor. Duis fringilla a justo nec vehicula. Pellentesque blandit sed nibh porttitor faucibus. Phasellus nisi erat, eleifend eu sem vel, convallis pharetra purus. Cras sagittis est at suscipit hendrerit. Mauris volutpat lectus eu mi facilisis, in gravida odio aliquet. Proin ut viverra tortor. Ut commodo felis ut ullamcorper fermentum. Donec ultricies dictum euismod. Suspendisse sit amet purus dui. Nulla vestibulum enim tristique commodo interdum. Aenean sollicitudin lacinia placerat. Morbi magna purus, sollicitudin et purus non, scelerisque auctor lectus. Fusce hendrerit est at dui consectetur iaculis. Maecenas placerat luctus blandit. Aenean ac risus a tellus dictum mattis. Nullam sapien metus, imperdiet eget neque quis, convallis vulputate odio. Ut magna massa, posuere eu efficitur eu, semper ut enim. Morbi tincidunt consectetur quam, eget porta mi tincidunt et. Duis lacinia lectus et lobortis malesuada. Duis bibendum tristique nulla. Maecenas tincidunt faucibus commodo. Pellentesque in tincidunt libero. Vestibulum ac fermentum erat, a ultricies mi. Nam vitae tortor quis lorem imperdiet gravida non at sapien. Fusce tincidunt maximus sapien quis porta. Donec id sapien vel enim imperdiet rutrum non eget arcu. Cras egestas vel mi eget viverra. Ut mollis neque eros, at fringilla ligula semper at. Nulla volutpat maximus mollis. </Typography> </> ) export function App() { const [imageLoaded, setImageLoaded] = useState(false); useEffect(() => { if (imageLoaded && window.location.hash) { document.querySelector(window.location.hash)?.scrollIntoView(); } }, [imageLoaded]); return ( <Grid container flexDirection="column" alignItems="center" textAlign="center" spacing={2}> <Grid item> <Typography sx={{maxWidth: "600px"}}> <img src={image} alt="Box" style={{width: "100%"}} onLoad={() => setImageLoaded(true)} /> </Typography> </Grid> <Grid item> <TallBox id="section1" color="red"/> </Grid> <Grid item> <TallBox id="section2" color="blue"/> </Grid> <Grid item> <TextField name="text" label="text" multiline/> </Grid> </Grid> ) }
内容的提问来源于stack exchange,提问作者Pavel Lobodinský
相关产品推荐
相关产品推荐

