浏览器 History API 是前端路由的基础,通过 pushState/replaceState 修改 URL 不刷新页面,并需监听 popstate 事件响应导航;需服务端配置避免 404,手动实现路由器需处理路径匹配、视图渲染、副作用管理及清理。

浏览器 History API 是前端路由的真正基础
单页应用(SPA)不刷新页面却能切换视图,靠的不是框架魔法,而是 history.pushState() 和 history.replaceState()。这些方法修改 URL 且不触发页面重载,同时把状态对象存入历史栈。关键点在于:它们不会自动触发任何回调,你必须自己监听 popstate 事件来响应后退/前进。
-
pushState()添加新历史记录,用户点击返回时会触发popstate -
replaceState()替换当前历史记录,适合初始化或修正 URL 而不增加栈深度 - 直接访问带 hash 的 URL(如
#/user)是降级方案,hashchange事件兼容性好但语义弱、SEO 不友好 - 使用
History API必须配合服务端配置:所有路径都返回同一份 HTML,否则刷新会 404
手动实现一个最小可用的前端路由器
不用框架也能跑通核心流程:监听路由变化 → 匹配路径 → 渲染对应视图。重点不在封装多漂亮,而在理解数据流向和副作用控制。
let routes = new Map();
function addRoute(path, render) {
routes.set(path, render);
}
function navigate(path) {
history.pushState({ path }, '', path);
renderView(path);
}
function renderView(path) {
const renderFn = routes.get(path) || routes.get('/404');
if (renderFn) document.getElementById('app').innerHTML = renderFn();
}
window.addEventListener('popstate', e => {
renderView(e.state?.path || location.pathname);
});
// 使用示例
addRoute('/', () => '首页
');
addRoute('/about', () => '关于页
');
addRoute('/404', () => '页面未找到
');
// 初始化渲染
renderView(location.pathname);
- 路径匹配是精确字符串比对,实际项目中需支持参数解析(如
/user/:id),可用正则或URLPattern(注意兼容性) -
renderView()直接写入innerHTML简单但有 XSS 风险;真实场景应结合模板函数或虚拟 DOM 差分更新 - 每次渲染前没清空旧状态?组件内定时器、事件监听器可能泄漏 —— 渲染函数最好返回一个
unmount()清理钩子
React Router / Vue Router 的本质只是封装了上述逻辑
它们没发明新机制,只是把 History API 操作、路径匹配、嵌套路由、懒加载、导航守卫等能力组织成声明式接口。比如 最终仍靠正则或 URLPattern 提取 id 参数传给组件。
-
useNavigate()(React Router v6+)内部调用的就是history.pushState() -
router-view(Vue Router)本质是一个动态占位符,根据当前location.pathname渲染匹配的组件 - 嵌套路由(如
/admin/users)靠的是父子组件的Outlet或递归渲染,不是服务端多层路径转发 - 服务端渲染(SSR)场景下,路由匹配必须在 Node.js 端同步执行,否则首屏仍是白屏或错误内容
视图渲染时最容易被忽略的副作用管理
路由切换不只是换 HTML 字符串。组件挂载常伴随数据请求、滚动复位、文档标题更新、分析上报等动作。这些若不统一管理,就会出现:后退时数据没刷新、页面卡在底部、title 还是上一页的。
立即学习“Java免费学习笔记(深入)”;
- 不要在组件
useEffect或mounted中无条件发请求 —— 应结合路由参数变化依赖项,或用路由守卫拦截 - 滚动行为需显式控制:
scrollRestoration: 'manual'+window.scrollTo(0, 0),否则浏览器可能保留上次位置 - 文档标题更新要用
document.title = ...,不能只靠组件内 state —— 否则分享链接时标题丢失 - 如果组件内用了
setTimeout或addEventListener,必须在卸载时清除,否则跨路由内存泄漏











