单页面应用(SPA)的最小实现是通过history.pushState修改URL、popstate监听路由变化、配合路由表匹配路径并更新DOM。需手动首次调用路由函数,拦截a标签点击,服务端须兜底返回主HTML。

什么是单页面应用(SPA)的最小实现?
单页面应用的核心不是框架,而是「不刷新页面却能改变 URL 并响应视图变化」。关键在于拦截浏览器默认跳转行为,用 JavaScript 手动更新内容 + 修改地址栏,同时监听地址变化做对应渲染。
最简可行方案只需三步:history.pushState 改 URL、window.addEventListener('popstate') 监听后退/前进、配合一个路由表匹配路径并切换 DOM。
- 不用
location.hash也能做,但需服务端配合(所有路径返回同一 HTML) -
pushState不触发页面刷新,但会留下历史记录,用户可正常点击后退 - 首次加载时,需从
location.pathname读取当前路径,主动匹配一次路由
如何手写一个基础前端路由?
下面是一个无依赖、支持 /、/about、/user/123 的简易路由:
const routes = {
'/': () => document.getElementById('app').innerHTML = '首页
',
'/about': () => document.getElementById('app').innerHTML = '关于
',
'/user/:id': (params) => document.getElementById('app').innerHTML = `用户 ${params.id}
`
};
function route() {
const path = location.pathname;
for (const [pattern, handler] of Object.entries(routes)) {
const regex = new RegExp('^' + pattern.replace(/:(\w+)/g, '([^/]+)') + '$');
const match = path.match(regex);
if (match) {
return handler(match.slice(1));
}
}
document.getElementById('app').innerHTML = '404
';
}
// 首次加载
route();
// 监听浏览器前进/后退
window.addEventListener('popstate', route);
// 拦截 a 标签点击(假设所有路由链接都加了 data-navigate 属性)
document.addEventListener('click', e => {
if (e.target.matches('a[data-navigate]')) {
e.preventDefault();
history.pushState({}, '', e.target.href);
route();
}
});
注意:pushState 第三个参数必须是同源 URL,否则抛出 SecurityError;popstate 在页面首次加载时不会触发,所以必须手动调用一次 route()。
立即学习“Java免费学习笔记(深入)”;
为什么不能只靠 location.hash?
location.hash 确实能避免刷新,但它有硬伤:URL 中的 # 后内容不发给服务端,导致分享链接、SEO、服务端直出全部失效。更重要的是,现代 SPA 都要求真实路径语义(如 /blog/2023/04),而 # 路由只能模拟成 /?#/blog/2023/04 这类丑陋形式。
-
hashchange事件兼容性好,但已属历史方案,新项目应优先用History API - 使用
hash时,pushState和replaceState无效,必须用location.hash = '...' - 服务端无需配置,但不利于 PWA 缓存策略和 HTTP 缓存头控制
服务端需要做什么?
如果用了 history.pushState,用户直接访问 /user/123 时,浏览器会向服务端发起请求。此时服务端不能返回 404,而要始终返回你的主 HTML 文件(比如 index.html),让前端路由接管。
- Nginx 配置示例:
try_files $uri $uri/ /index.html; - Vercel/Netlify 自动处理,无需额外配置
- 本地开发用
serve -s build(-s 表示 single-page app 模式) - 若服务端返回 404,用户刷新页面就会看到空白或错误页,这是最常被忽略的部署陷阱
路由逻辑本身完全在前端,但它的健壮性高度依赖服务端兜底策略——这点比写多少嵌套路由都关键。











