SPA 中的 Hash 和 History 模式
在单页应用(SPA)中,路由是核心功能之一。Hash 和 History 是两种主流的客户端路由实现方式,它们解决了在不刷新页面的情况下改变 URL 并渲染不同内容的问题。
📍 Hash 模式
工作原理
利用 URL 中的 hash (# 后面的部分)来模拟路由。当 hash 改变时,浏览器不会向服务器发送请求,也不会刷新页面,只会触发 hashchange 事件。
URL 示例
bash
http://example.com/#/user/profile
↑ 这部分是 hash
核心特点
- # 后面的内容不会被发送到服务器 ,服务器收到的请求始终是
http://example.com/ - 改变
window.location.hash会改变 URL,但页面不刷新 - 监听
window.onhashchange事件来响应路由变化
简单实现
javascript
class HashRouter {
constructor(routes) {
this.routes = routes;
window.addEventListener('hashchange', () => this.render());
window.addEventListener('DOMContentLoaded', () => this.render());
}
render() {
const hash = window.location.hash.slice(1) || '/';
const component = this.routes[hash];
if (component) {
document.getElementById('app').innerHTML = component;
}
}
}
// 使用
const router = new HashRouter({
'/': '<h1>首页</h1>',
'/about': '<h1>关于</h1>',
'/user': '<h1>个人中心</h1>'
});
📍 History 模式
工作原理
利用 HTML5 新增的 History API (pushState 和 replaceState)来操作浏览器历史记录栈,结合 popstate 事件实现路由切换。
URL 示例
bash
http://example.com/user/profile
↑ 没有 # 符号,看起来像正常 URL
核心 API
| API | 作用 |
|---|---|
pushState(state, title, url) |
添加一条历史记录,改变 URL 但不刷新页面 |
replaceState(state, title, url) |
替换当前历史记录,改变 URL 但不刷新页面 |
popstate 事件 |
监听浏览器前进/后退按钮操作 |
简单实现
javascript
class HistoryRouter {
constructor(routes) {
this.routes = routes;
// 监听前进后退
window.addEventListener('popstate', () => this.render());
// 拦截链接点击
document.body.addEventListener('click', (e) => {
if (e.target.tagName === 'A' && e.target.getAttribute('href')) {
e.preventDefault();
const url = e.target.getAttribute('href');
this.navigateTo(url);
}
});
this.render();
}
navigateTo(url) {
history.pushState(null, null, url);
this.render();
}
render() {
const path = window.location.pathname;
const component = this.routes[path] || this.routes['/404'];
if (component) {
document.getElementById('app').innerHTML = component;
}
}
}
📊 详细对比
| 对比维度 | Hash 模式 | History 模式 |
|---|---|---|
| URL 外观 | 包含 # 符号,不够美观 |
正常的 URL,干净美观 |
| 服务器配置 | 不需要特殊配置,所有请求都指向同一 HTML | 需要配置(见下文),否则刷新会 404 |
| 路由传参 | 只能通过 hash 传递,长度受限 | 可以使用 query string,更灵活 |
| 锚点滚动 | 冲突!# 既用于路由也用于页面内锚点 |
无此问题,可以正常使用锚点 |
| SEO 友好度 | ❌ 较差,搜索引擎可能忽略 # 后的内容 |
✅ 友好,URL 真实且可被抓取 |
| 浏览器兼容性 | ✅ 所有浏览器,包括 IE8+ | ⚠️ IE10+,需要 HTML5 History API |
| Hash 变化 | 触发 hashchange 事件 |
触发 popstate 事件 |
⚠️ History 模式的关键:服务器配置
为什么需要配置?
在 History 模式下,直接访问 http://example.com/user/profile 时,浏览器会向服务器请求 /user/profile 这个路径。由于这是 SPA 的路由,服务器上并没有这个实际文件,因此会返回 404 Not Found。
解决方案:兜底配置
将所有请求都重定向到 index.html,让 SPA 路由接管后续处理。
Nginx 配置
nginx
location / {
try_files $uri $uri/ /index.html;
}
Apache 配置 (.htaccess)
apache
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
RewriteRule ^index\.html$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.html [L]
</IfModule>
Node.js (Express) 配置
javascript
const express = require('express');
const path = require('path');
const app = express();
// 静态文件服务
app.use(express.static(path.join(__dirname, 'dist')));
// 所有路由都返回 index.html
app.get('*', (req, res) => {
res.sendFile(path.join(__dirname, 'dist', 'index.html'));
});
🎯 如何选择?
选择 Hash 模式的场景
- 小型项目、内部工具、管理后台(不关心 SEO)
- 没有服务器配置权限(如使用 GitHub Pages、纯静态托管)
- 需要兼容老旧浏览器(IE 8/9)
- 临时演示或快速原型
选择 History 模式的场景
- 需要 SEO 的公开项目(企业官网、博客、电商)
- 追求更干净的 URL,符合现代 Web 标准
- 有权限配置服务器(Nginx/Apache/Node.js)
- 需要复杂的 query 参数传递 (
?id=123&name=test)
💡 主流框架中的使用
Vue Router
javascript
// Vue 3 示例
import { createRouter, createWebHashHistory, createWebHistory } from 'vue-router'
// Hash 模式
const router = createRouter({
history: createWebHashHistory(),
routes: [...]
})
// History 模式
const router = createRouter({
history: createWebHistory(),
routes: [...]
})
React Router
jsx
// React Router v6
import { HashRouter, BrowserRouter } from 'react-router-dom'
// Hash 模式
<HashRouter>
<App />
</HashRouter>
// History 模式 (BrowserRouter)
<BrowserRouter>
<App />
</BrowserRouter>
🔧 常见问题
Q: History 模式下刷新页面 404 怎么解决? A: 配置服务器将所有请求指向 index.html(参考上面的配置示例)。
Q: Hash 模式能用于生产环境吗? A: 可以,许多知名的管理后台(如 Ant Design Pro)默认就使用 Hash 模式,简单可靠。
Q: 两种模式可以混用吗? A: 不建议。在一个项目中应统一使用一种模式。
Q: 锚点(页面内跳转)在 Hash 模式下怎么办? A: 可以使用 element.scrollIntoView() 来替代传统锚点,或者采用其他命名方案避免冲突。