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,只改了 statehistory.pushState() + 手动更新 UI,不能依赖 默认行为Router 要能匹配路径、提取参数、支持嵌套,但别自己写正则引擎手写路径匹配容易漏掉边界情况:带可选斜杠、重复斜杠、编码字符、通配符优先级。用成熟方案更稳,比如 path-to-regexp(React Router v5 / Express 底层)或 regexparam。它们把 "/user/:id(\\d+)" 编译成高效正则,并返回命名捕获组。
关键设计点:
"/user/:id" 必须在 "/user/new" 之前,否则 /user/new 会被误认为 id 是 "new"children 字段 + 状态透传实现,不是靠子 Router 实例。父路由匹配后,把剩余路径交给子路由匹配,避免多层 history 操作import() + then(),但注意 loading 状态要包裹整个路由出口,不能只包组件内部useState + useEffect 组合解决局部问题90% 的 SPA 页面状态生命周期和路由强绑定:进入 /user/123 时拉数据,离开时清副作用,参数变时重新请求。这时候硬上全局 store(如 Redux)反而增加同步成本和调试难度。
推荐分层策略:
URLSearchParams 解析,不进 JS 变量useState + useEffect 在组件内维护。例如:useEffect(() => { fetch(`/api/user/${id}`) }, [id])
zustand)。避免把所有状态都塞进一个 store.js,导致每次路由跳转都触发无关 re-render如果后续要支持 SSR,现在就得约束代码:所有路由匹配逻辑、数据获取、状态初始化必须能在 Node.js 环境执行。常见翻车点:
window.location 或 document.cookie —— SSR 时这些不存在,会报 ReferenceError
setTimeout 或 requestAnimationFrame 延迟渲染,SSR 不执行这些回调,导致首屏缺失内容history.state,但 SSR 渲染时 history 是空的,客户端 hydration 时发现 state 不一致,React 报 Hydration failed
真正难的不是写几个 pushState,而是让路由变更、数据加载、UI 更新、服务端兼容这四件事始终对齐。多数项目卡在这一步,不是因为技术不会,而是没在早期约定好状态归属和生命周期边界。