前端路由:不刷新页面的 “障眼法”,History API 是幕后黑手?

193 阅读6分钟

你有没有想过:为什么现在的网站点来点去,URL 变了,页面却不刷新?比如在 React 项目里,点 “首页” 跳 “关于我们”,地址栏从 / 变成 /about,屏幕内容换了,浏览器却没转圈 —— 这背后到底是怎么做到的?

其实这都是前端路由的 “障眼法”,而 HTML5 的 History API 就是这套魔术的 “幕后黑手”。今天从 “网址里的小尾巴”(hash 路由)说起,聊聊前端路由是怎么从 “土味实现” 进化到 “丝滑体验” 的。

一、早年的 “土办法”:带 # 的 hash 路由

在 HTML5 还没普及的时候,前端想实现不刷新跳转,全靠 URL 里的 “小尾巴”——#(哈希)。比如这个 URL:

http://xxx.com/index.html#home

这里的 #home 就是 hash,它有个特点:改变 hash 不会触发页面刷新,但会记录到浏览器历史里。配合 hashchange 事件,就能实现简单的路由:

<!-- 土味 hash 路由示例 -->
<a href="#home">首页</a>
<a href="#about">关于我们</a>
<div id="content"></div>

<script>
  // 监听 hash 变化
  window.addEventListener('hashchange', () => {
    const hash = window.location.hash;
    const content = document.getElementById('content');
    // 根据 hash 显示不同内容
    if (hash === '#home') {
      content.innerHTML = '<h1>欢迎来到首页</h1>';
    } else if (hash === '#about') {
      content.innerHTML = '<h1>关于我们</h1>';
    }
  });
</script>

优点:

  • 兼容性极好:哪怕是 IE6 这种 “老古董” 浏览器也支持;
  • 简单粗暴:不用后端配合,纯前端就能搞定。

缺点:

  • URL 不美观:带个 # 像 “网址里的小尾巴”,显得不专业;
  • 功能有限:只能通过 hashchange 监听变化,无法精细控制历史记录;
  • SEO 不友好:搜索引擎可能会忽略 # 后面的内容。

二、HTML5 来了:History API 让 URL 变 “干净”

随着 HTML5 的出现,history 对象被赋予了新功能 ——pushState 和 replaceState 方法。它们能让你在不刷新页面的情况下,直接修改 URL 并添加到浏览器历史,从此告别 “小尾巴”!

核心方法:

  • history.pushState(state, title, url):新增一条历史记录,URL 变为 url(不会刷新页面);
  • history.replaceState(state, title, url):替换当前历史记录,不新增;
  • history.go(n):跳转到第 n 条历史记录(n=1 前进,n=-1 后退,类似 history.back())。

举个例子:

<!-- 无刷新跳转示例 -->
<button onclick="goToAbout()">去关于我们</button>
<div id="content"></div>

<script>
  const content = document.getElementById('content');

  // 跳转到 /about,URL 变了但不刷新
  function goToAbout() {
    // 新增历史记录,URL 改为 /about
    history.pushState({ page: 'about' }, '关于我们', '/about');
    // 手动更新页面内容(History API 不会自动更新,需要自己写逻辑)
    content.innerHTML = '<h1>关于我们</h1>';
  }

  // 监听历史记录变化(用户点击前进/后退按钮时触发)
  window.addEventListener('popstate', (e) => {
    if (e.state?.page === 'about') {
      content.innerHTML = '<h1>关于我们</h1>';
    } else {
      content.innerHTML = '<h1>首页</h1>';
    }
  });
</script>

优点:

  • URL 干净美观:比如 /home/about,和后端路由一模一样;
  • 功能强大:可以通过 state 传递数据,精细控制历史记录;
  • 更符合直觉:用户看起来就是正常的页面跳转,体验更好。

注意点:

  • pushState 只会修改 URL 和历史记录,不会触发页面刷新或请求后端,所以页面内容需要自己手动更新;
  • 刷新页面时,浏览器会真的请求 /about 这个 URL,所以需要后端配合(比如配置所有路由都返回 index.html,避免 404)。

亲手试试:SPA 路由模拟

下面这个例子,用原生 JS 实现了一个极简的 SPA 路由,完全基于 History API,你可以复制到 HTML 文件里跑一跑:

<h2>SPA路由模拟</h2>
<!-- 按钮控制路由跳转 -->
<button onclick="navigate('/home')">首页</button>
<button onclick="navigate('/about')">关于</button>
<button onclick="navigate('/contact')">联系</button>
<button onclick="navigate('/login')">登录</button>
<button onclick="replace('/pay')">支付(替换当前记录)</button>
<!-- 原生a标签会刷新页面,作为对比 -->
<a href="http://www.zhihu.com">知乎(会刷新)</a>
<!-- 显示当前视图的区域 -->
<div id="view">当前视图</div>

<script>
  // 根据路径渲染对应内容
  function render(path) {
    document.getElementById('view').textContent = `当前视图:${path}`;
  }

  // 替换当前历史记录(不会新增记录)
  function replace(path) {
    // 参数:状态对象、标题(目前没用)、新URL
    history.replaceState({ path }, '', path);
    render(path);
  }

  // 新增历史记录(点击浏览器后退能回到上一页)
  function navigate(path) {
    // 参数:状态对象(可存额外数据)、标题、新URL
    history.pushState({ path }, null, path);
    render(path);
  }

  // 监听浏览器前进/后退按钮(popstate事件)
  window.addEventListener('popstate', (event) => {
    console.log('历史记录变化:', event.state);
    // 从状态对象里取路径,没有就用当前URL
    render(event.state?.path || location.pathname);
  });
</script>

核心方法解析:

  • history.pushState(state, title, url)
    新增一条历史记录,URL 变为 url(不刷新页面)。state 可以存额外数据(比如当前页面的状态),后续通过 popstate 事件能取到。

    比如点击 “首页” 按钮,调用 navigate('/home'),URL 变成 /home,视图区域显示 “当前视图:/home”,浏览器历史里会新增一条记录(点后退能回去)。

  • history.replaceState(state, title, url)
    替换当前历史记录,不会新增。比如点击 “支付” 按钮,URL 变成 /pay,但历史记录数量不变(后退不会回到 “支付” 前的页面,因为替换了)。

  • popstate 事件
    当用户点击浏览器的 “前进”“后退” 按钮时触发,通过 event.state 能拿到之前存在历史记录里的数据(比如 path),从而重新渲染对应内容。

为什么这个例子不会刷新页面?

对比一下:点击 “关于” 按钮,调用 navigate('/about'),URL 变了但页面没刷新,因为用了 pushState;而点击 “知乎” 链接,浏览器会发起新请求,页面会刷新 —— 这就是前端路由和传统跳转的核心区别。

三、现代框架怎么做?以 React Router 为例

手动用 History API 写路由逻辑还是有点麻烦(比如监听 URL 变化、匹配路由规则)。现代框架(如 React、Vue)都有成熟的路由库,比如 React 的 react-router-dom,它底层就是封装了 History API 或 hash,让路由开发变得像 “搭积木”。

用 React Router 实现路由:

// App.jsx
import { BrowserRouter as Router, Routes, Route, Link } from 'react-router-dom';
import Home from './pages/Home';
import About from './pages/About';

function App() {
  return (
    <Router> {/* 路由容器,底层用 History API */}
      <nav>
        {/* Link 类似 a 标签,但点击不刷新页面 */}
        <Link to="/">首页</Link>
        <Link to="/about">关于我们</Link>
      </nav>
      <Routes> {/* 路由匹配容器 */}
        {/* 当 URL 是 / 时,显示 Home 组件 */}
        <Route path="/" element={<Home />} />
        {/* 当 URL 是 /about 时,显示 About 组件 */}
        <Route path="/about" element={<About />} />
      </Routes>
    </Router>
  );
}

核心组件解析:

  • Router:路由的 “总开关”,有两种模式:

    • BrowserRouter:基于 HTML5 History API,URL 干净(推荐生产环境用);
    • HashRouter:基于 hash 路由,URL 带 #(适合简单场景或后端不配合时)。
  • Link:替代原生 a 标签,点击时通过 pushState 改变 URL,不刷新页面。

  • Routes 和 RouteRoutes 是路由规则的容器,Route 定义 “URL 路径” 和 “对应组件” 的映射(比如 path="/about" 对应 <About />)。

为什么用框架的路由库?

  • 自动监听 URL 变化:不用自己写 popstate 事件;
  • 声明式路由:用 <Route path="/" element={<Home />} 定义规则,直观易懂;
  • 内置功能丰富:支持嵌套路由、路由守卫、参数传递等高级功能。

四、总结:前端路由的进化史

从 “土味 hash 路由” 到 “丝滑 History API”,再到框架封装的路由库,前端路由的进化史就是一部 “追求更好用户体验” 的历史:

方案原理优点缺点
后端路由服务器返回不同页面实现简单每次跳转都刷新页面
hash 路由基于 URL 中的 #兼容性好、纯前端URL 带 #、功能有限
History API基于 HTML5 新特性URL 干净、功能强大需要后端配合、兼容性稍差
框架路由库封装 History API/hash开发效率高、功能丰富需学习框架特定语法

简单说,前端路由的核心就是:用 History API 或 hash 改变 URL,不刷新页面,同时手动更新页面内容。框架的作用就是把这些逻辑封装起来,让我们能更专注于业务开发。