什么是单页面应用以及如何用javascript构建它?【教程】

单页面应用的核心特征是不触发整页刷新,所有视图切换、数据加载、路由跳转均由 JavaScript 在当前页面内完成,关键判断标准为 location.href 变化而 document.body 未被整体替换且 DOMContentLoaded 不重复触发。

单页面应用的核心特征是什么

单页面应用(SPA)不是指“只有一个 HTML 文件”,而是指用户在访问过程中,不触发整页刷新,所有视图切换、数据加载、路由跳转都由 JavaScript 在当前页面内完成。关键判断标准是:location.href 变了,但 document.body 没被整个替换,DOMContentLoaded 不会再次触发。

用原生 JavaScript 实现最小可行路由

不需要框架也能做 SPA,核心是监听 URL 变化并动态更新内容。重点不在“怎么写得漂亮”,而在“怎么避

免常见断裂点”:

  • 必须监听 popstate 事件,而不是只靠 hashchange —— 否则前进/后退按钮失效
  • history.pushState() 第一个参数(state 对象)不能为 null,Chrome 120+ 会静默失败,建议传空对象 {}
  • 首次加载时,需手动检查 location.pathnamelocation.hash 并渲染对应视图,否则刷新页面直接白屏

示例片段:

function navigate(path) {
  history.pushState({}, '', path);
  renderView(path);
}

window.addEventListener('popstate', () => {
  renderView(location.pathname);
});

// 首次加载
renderView(location.pathname);

如何避免 DOM 状态残留导致的视觉错乱

这是手写 SPA 最容易被忽略的坑:组件卸载时没清理副作用,导致旧事件监听器、定时器、动画帧持续运行,新内容渲染后行为异常。

  • 每次 renderView() 前,先调用上一个视图的 unmount()(如果存在),清掉 addEventListenerclearTimeoutcancelAnimationFrame
  • 不要直接用 innerHTML = htmlString 替换整个区域 —— 已挂载的 会丢失焦点、已绑定的 oninput 会断开;改用 replaceChildren() 或细粒度 diff
  • 表单提交、链接点击等交互,必须 e.preventDefault(),否则默认行为会跳转到真实 URL 导致整页刷新

什么时候该放弃手写、转向框架

当开始反复处理以下问题时,说明手写成本已超过收益:

  • 需要支持嵌套路由(如 /user/123/profile)且带参数解析
  • 多个异步数据依赖需协调加载状态(比如侧边栏菜单 + 主体内容同时请求)
  • 要兼容 IE11 或低版本 Safari,history.pushState 的 polyfill 维护成本陡增
  • 团队中有人修改了某个 renderView() 分支,却忘了同步更新 unmount() 逻辑,导致内存泄漏

这时候引入 react-routerpage.js 不是“过度设计”,而是止损。手写 SPA 的价值在于理解机制,而非长期维护。