纯HTML无法实现SPA,必须用JavaScript拦截路由、更新DOM、管理历史状态与可访问性细节。需preventDefault()阻止a标签刷新,用pushState改URL并fetch加载内容,服务端需fallback至index.html,手动处理滚动、焦点、标题等默认行为。

纯 HTML 无法实现 SPA,必须靠 JavaScript 主动干预路由和 DOM 更新——这不是“加点属性就能跑”的事,而是要亲手拦截跳转、改地址栏、换内容、管前进后退。
为什么 <a href="/about"> 点一下就全页刷新?
浏览器对 <a> 标签的默认行为就是发起新 HTTP 请求、解析新 HTML、重绘整个页面。SPA 要打破这个链条,就得在点击瞬间用 e.preventDefault() 拦住它。不拦,一切后续操作都无效。
- 只写
href="#about"是哈希模式起点,但不监听hashchange事件,页面照样不动 - 用
history.pushState()改 URL,但没配套fetch()加载内容,地址栏变了,页面还是旧的 - 没监听
popstate,用户点浏览器后退按钮,URL 回退了,但视图卡在原地
history.pushState() 的第三个参数为什么必须是同源路径?
这是浏览器安全限制:你不能用它跳到 https://evil.com。更关键的是,这个路径会成为用户直接访问的入口 —— 比如你 push 到 /dashboard,那用户在地址栏敲完回车,请求就发给服务端了。如果 Nginx/Apache 没配 fallback,直接返回 404。
- 开发时用
pushState(null, '', '/about')可以,但上线前必须确保服务端对所有前端路由都返回index.html(不是 302 重定向) - 路径必须是相对路径或同源绝对路径;
pushState(..., '/about')和pushState(..., 'about')行为不同,后者是相对当前路径拼接 - 调用后
document.title不变,SEO 工具和历史记录里看到的仍是旧标题,得手动同步
视图更新该用 innerHTML 还是 replaceChildren()?
两者都能换内容,但语义和副作用差很多。innerHTML = htmlString 简单粗暴,适合原型或静态片段;replaceChildren() 更安全,不会意外执行内联脚本,也避免重复绑定事件。
立即学习“前端免费学习笔记(深入)”;
- 如果加载的是带
<script>的 HTML 片段,innerHTML会执行它,可能造成重复初始化或报错 - 用
fetch('/about.html').then(r => r.text()).then(html => main.replaceChildren(...))更可控 - 真实项目中,DOM 操作只是最后一步;前面往往还要解析 URL 参数、触发数据请求、处理 loading 状态
滚动位置和焦点状态为什么总“丢”?
浏览器原生跳转会自动恢复滚动位置、聚焦输入框;SPA 完全绕过了这套机制,所有状态都得自己记、自己设。
- 进新页面前存
window.scrollY到history.state里,popstate时读出来window.scrollTo(0, y) - 如果上一页有
input获得了焦点,跳走再回来,得手动el.focus(),否则键盘打不出字 - 无障碍场景下,
document.title不同步、aria-live不提示、焦点不在主内容区,都会让屏幕阅读器用户迷路
真正卡住人的从来不是 pushState 那几行代码,而是这些“浏览器默认帮你做了,现在你要自己补全”的细节 —— 它们分散在路由、渲染、历史、可访问性多个层面,且只有在线上真实用户操作时才集中爆发。



















