前端知库

hash 路由和 history 路由的原理是什么?服务器要怎样配置回退?

hash 路由把路径放在

参考答案

简答

  • hash 路由:地址形如 /#/user/1。# 后面的片段不会随请求发给服务器,修改 location.hash 不会刷新页面,值变化时触发 hashchange 事件。
  • history 路由:history.pushState(state, '', url) 修改地址栏并新增历史记录,浏览器不会加载该地址,也不会触发 popstate,路由库要在调用后自己渲染新页面。
  • popstate 在用户前进、后退等切换历史记录时触发,路由库监听它来同步界面。pushState 的地址必须与当前页面同源。
  • history 模式下刷新或直接打开 /user/1 时,浏览器会真正请求这个路径。静态服务器上没有对应文件,需要配置为返回 index.html,由前端路由处理。

路由要管理的数据、深链接和权限边界见《前端路由除切换组件外还需要管理什么?》,本题只讲两种模式怎样手写,以及服务器怎样配置回退。

hash 路由

JavaScript
function renderByHash() {
  const path = location.hash.slice(1) || '/';
  render(path);
}

window.addEventListener('hashchange', renderByHash);
renderByHash();

// 跳转
location.hash = '/user/1';

给 location.hash 赋与当前相同的值不会触发 hashchange。hash 模式不依赖服务器配置,代价是地址带 #,并且与页内锚点共用片段。

history 路由

JavaScript
function navigate(url) {
  history.pushState(null, '', url);
  render(location.pathname); // pushState 不会触发 popstate,要手动渲染
}

window.addEventListener('popstate', () => render(location.pathname));

document.addEventListener('click', (event) => {
  const link = event.target.closest('a[href^="/"]');
  // 保留新标签页、下载和其他脚本已处理的点击
  if (
    !link ||
    event.defaultPrevented ||
    event.button !== 0 ||
    event.metaKey ||
    event.ctrlKey ||
    event.shiftKey ||
    event.altKey ||
    link.target ||
    link.hasAttribute('download')
  )
    return;
  event.preventDefault();
  navigate(link.getAttribute('href'));
});

replaceState 参数相同,只是替换当前记录,适合规范化查询参数这类不应产生新历史的场景。

服务器回退

浏览器调用 pushState 时不请求新地址,但之后刷新、复制链接打开或重启浏览器恢复标签页时都会请求它。服务器必须能响应所有前端路由路径。

Nginx 的常见写法是先找真实文件,找不到再返回入口页:

nginx
location / {
  try_files $uri $uri/ /index.html;
}

回退之后服务器对任何未知路径都返回 200 和入口页,真正不存在的页面要由前端渲染 404 界面。静态资源路径写错时也可能拿到 HTML 而不是脚本,排查时注意响应类型。

部署在子路径(例如 /app/)时,路由库的 base 和构建工具的资源前缀都要设成同一个子路径,回退目标也要是该子路径下的 index.html。不支持回退配置的托管平台上,可以改用 hash 模式。

常见追问

  • 为什么 history 模式对 SEO 更友好?每个页面有独立的真实路径,服务端渲染或预渲染时可以按路径输出不同内容;hash 片段不发给服务器,无法据此返回不同页面。
  • pushState 的第二个参数有用吗?它是历史遗留的标题参数,浏览器基本忽略,通常传空字符串。