你点的“刷新”是假刷新?前端路由的瞒天过海术

1 阅读5分钟

为什么单页应用切换页面时,浏览器没有真正刷新?地址栏变了,页面却没白一下?今天我们来拆穿前端路由的“魔术”——它根本没去服务器要新页面,而是自己偷偷换了内容。看完这篇,你也能实现一个自己的前端路由。

前言

你有没有注意过,现在很多网站(比如知乎、B站、Github)点开一个新页面,地址栏变了,但页面没有那种“白屏-加载-闪现”的过程,而是瞬间切换内容。这就像你走进一家餐厅,菜单上写着“换桌”,你以为换了个房间,结果服务员只是把你桌上的桌布换了。

这就是前端路由干的“好事”。它让页面看起来跳转了,实际上只是JS在背后偷偷换了DOM,地址栏的变化也是骗你的。今天我们就来揭开这个魔术的奥秘,顺便自己写一个简单的路由。

一、什么是前端路由?

传统网站,点击链接会向服务器请求一个新HTML,浏览器刷新整个页面。这叫后端路由

单页应用(SPA)里,所有页面逻辑都在一个HTML里。切换“页面”时,不会请求新HTML,而是JS擦掉旧内容,画上新内容。同时,通过某种手段改变浏览器的地址栏URL,让用户感觉像换了个页面。这就是前端路由

前端路由的实现依赖两个“戏法”:

  • 改变URL但不刷新页面
  • 监听URL变化并渲染对应组件

二、Hash模式:带#号的“假跳转”

早期前端路由用的是hash(也就是URL里#后面的部分)。改变#后的值,不会触发页面刷新,也不会向服务器发请求。浏览器自己会记录历史(前进后退可用)。

// 改变hash
window.location.hash = 'home';

// 监听hash变化
window.addEventListener('hashchange', () => {
  const hash = window.location.hash.slice(1); // 去掉#
  renderPage(hash);
});

比如https://example.com/#/home,你改成#/about,页面不会刷新,但hashchange事件会触发,你可以在回调里根据hash渲染不同内容。

优点:兼容性好,IE也能用。
缺点:URL有个丑陋的#;服务端无法捕获#后面的内容(因为#之后的部分不会发到服务器)。

三、History模式:看起来像真的

HTML5新增了pushStatereplaceState,可以改变URL路径,同样不刷新页面。加上popstate事件监听,就能实现干净的路由(没有#)。

// 改变URL(添加一条历史记录)
history.pushState({ page: 'home' }, 'Home', '/home');

// 替换当前历史记录(不新增)
history.replaceState({ page: 'about' }, 'About', '/about');

// 监听前进后退
window.addEventListener('popstate', (event) => {
  const state = event.state; // pushState时传的数据
  renderPage(location.pathname);
});

优点:URL干净,像真实多页面。
缺点:需要服务端配合——因为刷新页面时,浏览器会按真实路径请求服务器,如果服务器没配置,会404。解决方案:所有路由都返回同一个HTML(即index.html)。

四、手写一个迷你前端路由

我们来实现一个最简单的Hash路由,包含三个“页面”:首页、关于、404。

<nav>
  <a href="#/home">首页</a>
  <a href="#/about">关于</a>
  <a href="#/nothing">不存在</a>
</nav>
<div id="app">内容会变</div>
function renderPage(path) {
  const app = document.getElementById('app');
  if (path === '/home') {
    app.innerHTML = '<h2>🏠 首页</h2><p>欢迎来到我的网站</p>';
  } else if (path === '/about') {
    app.innerHTML = '<h2>📖 关于</h2><p>这是一个前端路由演示</p>';
  } else {
    app.innerHTML = '<h2>❌ 404</h2><p>页面不存在</p>';
  }
}

// 监听hash变化
window.addEventListener('hashchange', () => {
  const hash = window.location.hash.slice(1); // 去掉#
  renderPage(hash || '/home');
});

// 页面加载时执行一次
window.addEventListener('load', () => {
  const hash = window.location.hash.slice(1);
  renderPage(hash || '/home');
});

就这么几行,你已经实现了一个前端路由。当然,实际框架里的路由更复杂(嵌套路由、动态参数、路由守卫等),但核心原理就是监听URL变化 + 渲染对应组件。

五、前端路由与后端路由的区别

特性后端路由前端路由
请求方式每次跳转都请求服务器不请求服务器(JS切换内容)
刷新页面会重新下载HTML会刷新但需要服务端配合(history模式)
首屏加载只加载当前页面通常要加载所有JS(可代码分割)
用户体验有白屏、闪烁切换流畅
SEO友好较差(需SSR或预渲染)

六、常见坑点与解决方案

1. History模式刷新404

配置Nginx将所有路由指向index.html:

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

2. 路由跳转但页面不滚动

单页切换时,滚动条位置可能保留在上一个页面的位置。需要在路由变化后手动window.scrollTo(0, 0)

3. 动态路由参数

比如/user/:id,你需要从路径中提取id。可以用正则或简单分割:

function matchRoute(path, routePath) {
  const pathParts = path.split('/');
  const routeParts = routePath.split('/');
  if (pathParts.length !== routeParts.length) return null;
  const params = {};
  for (let i = 0; i < pathParts.length; i++) {
    if (routeParts[i].startsWith(':')) {
      params[routeParts[i].slice(1)] = pathParts[i];
    } else if (routeParts[i] !== pathParts[i]) {
      return null;
    }
  }
  return params;
}

七、总结

  • 前端路由让单页应用切换页面时不刷新,体验流畅。
  • Hash模式# + hashchange,兼容性好,但URL丑。
  • History模式pushState + popstate,URL干净,需服务端配合。
  • 原理很简单:监听URL变化 → 根据路径渲染不同内容。
  • 现代框架(React Router、Vue Router)都是在此基础上增强。

下次再看到地址栏变了但页面没白,你就可以自信地说:“哼,不过是在演我。”

如果你喜欢今天的“魔术揭秘”,点个赞让更多人看到。明天我们将聊聊Webpack的Loader和Plugin原理,从零理解构建工具的核心。我们明天见!