SPA 应用应使用 history.pushState/replaceState 和 popstate 事件管理路由,避免 location.href 刷新;路由匹配推荐 path-to-regexp 等成熟库,注意路径顺序与嵌套设计;状态按生命周期分层管理,优先组件内 useState,跨路由状态才提升;SSR 需规避客户端专属 API 并确保服务端可执行。

用 URL 控制视图切换,别碰 location.href 刷新
单页应用(SPA)的核心是不刷新页面的前提下响应 URL 变化。浏览器原生支持 history.pushState() 和 history.replaceState(),配合 popstate 事件就能监听前进/后退。直接改 location.href 或 location.assign() 会触发整页 reload,完全违背 SPA 原则。
实操建议:
- 初始化时用
history.replaceState()抹掉初始 URL 的潜在 hash 或冗余参数,避免重复触发 - 监听
popstate时,从event.state读取路由参数,而不是重新解析location.pathname—— 因为pushState可能没改 path,只改了 state - 所有导航入口(如按钮点击、表单提交)必须调用
history.pushState()+ 手动更新 UI,不能依赖默认行为
Router 要能匹配路径、提取参数、支持嵌套,但别自己写正则引擎
手写路径匹配容易漏掉边界情况:带可选斜杠、重复斜杠、编码字符、通配符优先级。用成熟方案更稳,比如 path-to-regexp(React Router v5 / Express 底层)或 regexparam。它们把 "/user/:id(\\d+)" 编译成高效正则,并返回命名捕获组。
关键设计点:
立即学习“Java免费学习笔记(深入)”;
- 路由配置用数组而非对象,保持顺序——
"/user/:id"必须在"/user/new"之前,否则/user/new会被误认为 id 是 "new" - 嵌套路由靠
children字段 + 状态透传实现,不是靠子Router实例。父路由匹配后,把剩余路径交给子路由匹配,避免多层 history 操作 - 动态加载组件用
import()+then(),但注意loading状态要包裹整个路由出口,不能只包组件内部
状态管理别过早抽象,先用 useState + useEffect 组合解决局部问题
90% 的 SPA 页面状态生命周期和路由强绑定:进入 /user/123 时拉数据,离开时清副作用,参数变时重新请求。这时候硬上全局 store(如 Redux)反而增加同步成本和调试难度。
推荐分层策略:
- 路由参数、搜索参数、激活 tab 这类「界面控制态」,直接存在 URL 中,用
URLSearchParams解析,不进 JS 变量 - 接口响应数据、表单输入值、展开/收起状态这类「业务态」,优先用
useState+useEffect在组件内维护。例如:useEffect(() => { fetch(`/api/user/${id}`) }, [id]) - 跨多个路由共享的状态(如用户登录态、主题色),才提升到 context 或轻量 store(如
zustand)。避免把所有状态都塞进一个store.js,导致每次路由跳转都触发无关 re-render
服务端渲染(SSR)和客户端水合(hydration)失败的典型原因
如果后续要支持 SSR,现在就得约束代码:所有路由匹配逻辑、数据获取、状态初始化必须能在 Node.js 环境执行。常见翻车点:
- 组件里直接调用
window.location或document.cookie—— SSR 时这些不存在,会报ReferenceError - 用
setTimeout或requestAnimationFrame延迟渲染,SSR 不执行这些回调,导致首屏缺失内容 - 路由状态依赖
history.state,但 SSR 渲染时history是空的,客户端 hydration 时发现 state 不一致,React 报Hydration failed
真正难的不是写几个 pushState,而是让路由变更、数据加载、UI 更新、服务端兼容这四件事始终对齐。多数项目卡在这一步,不是因为技术不会,而是没在早期约定好状态归属和生命周期边界。











