前端实现真正的SPA需用History API接管URL变化,初始化渲染+监听popstate+服务端fallback,手写Router核心是路由存储、路径匹配与触发渲染,且须妥善管理state状态与滚动行为。

前端路由不是靠 location.href 跳转就能叫“SPA”的——真要实现无刷新切换、浏览器前进后退正常、URL 可收藏,得自己接管 URL 变化并匹配视图。
用 History API 替代 hashchange 实现干净 URL
现代 SPA 都走 pushState 路由,避免 #。但直接调用 history.pushState() 不会触发页面重载,你得手动监听变化并响应:
-
history.pushState()和history.replaceState()只改 URL 和状态对象,不触发popstate事件;只有用户点后退/前进,或代码调用history.back()才触发 - 必须在初始化时读取
location.pathname渲染首屏,不能只等popstate - 服务端需配置 fallback:所有非静态资源请求都返回
index.html,否则刷新 404
手写一个最小可用的 Router 类
不用框架也能跑通核心逻辑。关键就三点:存储路由表、匹配路径、触发渲染。下面这个版本去掉错误处理和嵌套路由,但能 work:
class Router {
constructor() {
this.routes = new Map();
this.currentPath = location.pathname;
}
add(path, callback) {
this.routes.set(path, callback);
}
navigate(path) {
history.pushState({ path }, '', path);
this.currentPath = path;
this.handleRoute();
}
handleRoute() {
const callback = this.routes.get(this.currentPath) || this.routes.get('*');
if (callback) callback();
}
init() {
// 初始化渲染
this.handleRoute();
// 监听浏览器导航
window.addEventListener('popstate', (e) => {
this.currentPath = e.state?.path || location.pathname;
this.handleRoute();
});
}
}
用法示例:
立即学习“Java免费学习笔记(深入)”;
const router = new Router();
router.add('/', () => {
document.getElementById('app').innerHTML = 'Home
';
});
router.add('/about', () => {
document.getElementById('app').innerHTML = 'About
';
});
router.add('*', () => {
document.getElementById('app').innerHTML = '404
';
});
router.init();
pushState 的 state 参数容易被忽略,但它决定后退时能否还原页面状态
很多人传个空对象甚至 null,结果后退时页面丢了滚动位置、表单输入、筛选条件。正确做法是把当前可序列化的 UI 状态塞进去:
- state 对象会被序列化进浏览器历史栈,大小限制约 640KB(Chrome),别塞 DOM 节点或函数
- 比如搜索页跳转到详情页,应在
pushState({ query: 'js', scrollY: 240 }, ...)中保存关键字段 - 在
popstate回调里取e.state.query重新发起请求或恢复 UI,而不是重新 load 整个页面
真实项目中绕不开的两个坑
一是 scrollRestoration 默认为 auto,导致后退时自动滚回顶部,掩盖了你本该手动控制的行为;二是 beforeunload 和路由离开守卫不是一回事——beforeunload 是全局离开页面,而路由守卫得你自己在 navigate() 前加判断逻辑,比如表单未保存时弹确认框。
这些细节不处理,用户一刷新、一点后退,体验就断了。路由不是“写完就能跑”,而是“写完才刚开始调”。











