如何用javascript实现路由功能_怎样构建单页面应用基础【教程】

SPA路由核心是用history.pushState更新URL而不刷新页面,需配合popstate监听、拦截a标签点击、健壮路径匹配及首次加载手动渲染。

直接用 history.pushState 操作 URL 而不刷新页面

单页应用(SPA)路由的核心,是不让浏览器真正跳转,而是手动更新地址栏并响应变化。关键在于绕过默认的页面重载行为:history.pushStatehistory.replaceState 是唯一直接、标准、无兼容性问题的方式(IE10+ 支

持)。location.hash 虽然兼容性更好,但会多一个 #,且不利于 SEO 和服务端直出。

实操建议:

  • 每次“跳转”都调用 history.pushState({ path: '/user' }, '', '/user') —— 第一个参数是状态对象(可存任意数据),第二个是 title(通常为空),第三个才是真实路径
  • 必须配合 popstate 事件监听返回/前进操作:window.addEventListener('popstate', e => render(e.state?.path || location.pathname))
  • 注意:pushState 不会触发 popstate,只对浏览器前进/后退生效;首次加载仍需手动调用一次 render

拦截 点击并阻止默认跳转

用户点击链接时,浏览器默认会发起新请求,这会彻底破坏 SPA 体验。必须主动捕获所有内部链接点击,改用 JS 控制路由逻辑。

常见错误现象:点击后页面白屏或整页刷新,说明某些 没被拦截。

实操建议:

  • 用事件委托绑定到 document,避免漏掉动态插入的链接:document.addEventListener('click', e => { if (e.target.matches('a[href^="/"]') && !e.target.hasAttribute('target')) { e.preventDefault(); router.navigate(e.target.getAttribute('href')); } })
  • 只拦截以 / 开头、且没 target 属性的链接(排除外链和新窗口)
  • 不要用 onclick 内联 handler,维护成本高且易遗漏

匹配路径时慎用字符串相等,优先考虑前缀或正则

简单用 location.pathname === '/user' 只能处理静态路径,一旦涉及参数(如 /user/123)、嵌套路由(/admin/users)或带查询参数的场景,就会失效。

实操建议:

  • 基础场景可用 pathname.startsWith('/admin') 判断布局层级
  • 带参数的路径推荐用正则:const match = pathname.match(/^\/user\/(\d+)$/),提取 match[1] 即 ID
  • 避免手写复杂正则,可封装一个轻量 matchPath(pathname, pattern) 函数,支持 "/user/:id" 这类写法(但不必引入完整 React Router 语法)
  • 注意结尾斜杠:/user 和 /user/ 是不同路径,统一处理(比如强制去除末尾 /

popstate 事件监听不到初始页面加载

这是最容易忽略的坑:用户直接访问 https://site.com/user 或刷新页面时,popstate 根本不会触发,导致页面空白或渲染首页。

原因在于该事件只在历史栈变化(前进/后退)时抛出,首次加载属于“进入”,不是“切换”。

实操建议:

  • 页面初始化时,必须显式读取 location.pathname 并调用渲染函数:render(location.pathname)
  • 把初始化逻辑和 popstate 回调抽成同一个函数,避免重复代码
  • 如果用了 pushState 初始化状态(比如从服务器注入当前路径),也要确保 state 对象正确传入,否则 e.statenull
路由真正的复杂点不在跳转本身,而在路径解析的健壮性、状态同步的一致性,以及服务端如何配合(比如 404 时返回 index.html)。这些细节不处理好,开发阶段看不出问题,上线后就容易出现白屏或返回异常。