单页应用中可通过 history.state 手动保存恢复滚动位置:跳转前用 pushState 存 window.scrollY;popstate 中用 requestAnimationFrame 延迟 scrollTo;初始加载需在路由就绪后读取 state 滚动,注意 iframe、移动端 Safari 兼容性。

在单页应用(SPA)中,浏览器原生的前进/后退按钮默认不会保存或恢复页面滚动位置,导致用户体验断层。利用 history.state 配合 pushState/replaceState 和 popstate 事件,可以手动记录并还原滚动位置,效果接近多页应用(MPA)。
在跳转前保存当前滚动位置
每次路由变化(如点击导航、调用 router.push)前,应将当前 window.scrollY(或 document.documentElement.scrollTop)存入 state 对象:
// 示例:模拟一次前端路由跳转
const goToPage = (url, title, state = {}) => {
const scrollY = window.scrollY;
const newState = { ...state, scrollY };
history.pushState(newState, title, url);
};
注意:不要依赖 scrollY 在 popstate 中直接恢复——因为此时页面可能尚未渲染完成,DOM 还未就绪。应把位置暂存,留待合适时机应用。
在 popstate 中延迟恢复滚动
监听 popstate 事件,在状态变更后等待 DOM 更新(例如使用 requestAnimationFrame 或 setTimeout(..., 0)),再执行滚动:
window.addEventListener('popstate', (event) => {
if (event.state?.scrollY != null) {
requestAnimationFrame(() => {
window.scrollTo(0, event.state.scrollY);
});
}
});
使用 requestAnimationFrame 是因为:它确保在下一次重绘前执行,此时新路由对应的内容通常已挂载完毕,滚动目标元素存在且尺寸可读。
处理初始加载和空 state 场景
页面首次加载时,history.state 可能为 null,且无 popstate 触发。需在初始化阶段主动检查是否需要滚动:
- 若 URL 匹配某个已缓存的滚动位置(如从服务端直出或 SSR 恢复),可提前设置
scrollY - 更稳妥做法:在路由就绪(如 Vue 的
onMounted、React 的useEffect)后,读取history.state?.scrollY并滚动 - 对首页或无历史记录的页面,可设默认滚动为
0,或忽略(保持自然加载位置)
补充细节与兼容性提醒
避免重复保存:不是每次 scroll 事件都存 —— 那会频繁触发 pushState,影响性能且污染历史栈。只在明确的导航动作前保存。
处理 iframe 或容器滚动:若滚动发生在某个容器内(如 overflow-y: auto 的 div),需记录该元素的 scrollTop,而非 window.scrollY,并在恢复时操作对应元素。
移动端 Safari 注意:部分旧版 iOS Safari 在 popstate 后会自动重置滚动,需在 setTimeout(..., 100) 中再次强制滚动以覆盖其行为。










