SPA 中:首页加载帖子列表,点击某帖跳转详情页(URL 变化),点击浏览器后退返回首页时,如何让列表内容保持原状、无需重新请求也无需重新渲染?请从路由机制、组件缓存、滚动恢复三方面说明原理,并分别给出 Vue 与 React 的实现。
核心矛盾:SPA 路由切换本质是组件卸载/挂载。默认情况下返回列表页 = 重新 mount = 重新发请求、重新渲染、滚动回到顶部。要做到"原样保留",需要三件套:组件实例缓存 + 数据状态外提 + 滚动位置恢复。
一、底层机制:URL 变化如何驱动视图
- 浏览器后退/前进触发
popstate事件,URL 变化后路由库(vue-router / react-router)监听它,解析出新路由并切换对应组件。 - 路由库只负责"切组件",不负责保存旧页面的状态——状态丢失是"切组件"的天然副作用,不是路由的错。
- 因此解决方案都围绕:让"切走"的组件不被真正销毁,或让数据不依赖组件生命周期。
二、方案 A:组件缓存(Vue 推荐)
Vue 的 <keep-alive> 会缓存组件实例(连同 DOM 子树、data、滚动的 DOM 位置):
<router-view v-slot="{ Component }">
<keep-alive :include="['Home']" :max="10">
<component :is="Component" />
</keep-alive>
</router-view>- 切走时组件不销毁,而是
deactivated;返回时不重新 mount,onMounted不再执行 → 自然不发请求、不重新渲染。 onActivated里可做"增量刷新"(如静默拉最新数据但保留旧列表),配合include/exclude精确控制缓存哪些路由,max限制缓存上限。
三、方案 B:状态外提(React 常用)
React Router 默认没有 keep-alive,返回列表页必然重新 mount。常见做法:
- 把列表数据 + 筛选分页 + 滚动位置提升到全局 store(Redux / Zustand / Pinia),组件重新挂载时从 store 直接恢复,不发请求。
- "有缓存则不请求"约定:
useEffect里if (store.list.length) return; fetch()。 - 想彻底保 DOM 可用第三方库
react-activation(KeepAlive 包裹),或手动维护一个"离屏容器"缓存列表 DOM。
四、滚动位置恢复
- Vue Router:内置
scrollBehavior,返回savedPosition即恢复原位置:
scrollBehavior(to, from, savedPosition) {
return savedPosition || { top: 0 }
}- React Router v6:无内置,手动
window.history.scrollRestoration = 'manual'+ 在路由切换时保存/恢复scrollTop,或监听popstate恢复。 - 底层都依赖浏览器
history.scrollRestoration('auto' | 'manual')。
五、常见坑
- 后退但组件被
keep-alive排除 → 仍会重新请求。 - 缓存无上限 → 内存泄漏,务必
max。 - 依赖
onMounted发请求的组件,缓存后onMounted只执行一次,第二次进入要写进onActivated。 keep-alive不能包"每次都要全新的页面"(如编辑页),用include白名单更安全。