你有没有想过:为什么现在的网站点来点去,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和Route:Routes是路由规则的容器,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,不刷新页面,同时手动更新页面内容。框架的作用就是把这些逻辑封装起来,让我们能更专注于业务开发。