HashRouter 和 HistoryRouter(通常称 hash 模式与 history 模式)是前端单页应用(SPA)中实现客户端路由的两种机制,核心区别在于如何管理 URL 且不触发页面刷新。
- HashRouter(哈希模式):基于 URL 中 `` 后面的部分(如
//about),通过监听hashchange事件实现路由切换。hash 不会发送给服务器,因此无需后端配置,兼容所有浏览器(包括 IE8+),但 URL 不够美观,且可能干扰锚点定位。 - HistoryRouter(history 模式):利用 HTML5 History API(
pushState()/replaceState()+popstate事件)创建"干净"URL(如/about),外观与传统服务端路由一致,更利于 SEO 和用户体验;但刷新或直接访问深层路径时,若服务器未配置 fallback(如将所有请求重定向至index.html),会返回 404。
两者都不真正发起 HTTP 请求到服务器(除首次加载),由前端 JS 动态渲染视图。Vue Router 默认用 hash 模式;React Router 的 BrowserRouter 对应 history 模式,HashRouter 对应 hash 模式。部署时:hash 模式开箱即用,history 模式需后端配合(如 Nginx 的 try_files 或 Express 的 history-api-fallback)。
SPA 模式下的路由模式详解
SPA(Single Page Application,单页应用)的路由模式核心原理是:在前端控制页面的切换,不向服务器发送请求,通过 URL 的变化来渲染不同的组件/页面。
一、两种核心路由模式
1. Hash 模式(默认)
工作原理
使用 URL 中的 # 符号(hash)作为路由标识,hash 变化不会触发页面刷新。
javascript
// URL 示例
http://example.com/#/home
http://example.com/#/about
http://example.com/#/user/123
// hash 部分变化
location.hash = '#/home' // 不会刷新页面
location.hash = '#/about' // 不会刷新页面
实现原理
javascript
class HashRouter {
constructor() {
this.routes = {};
this.currentUrl = '';
// 监听 hash 变化事件
window.addEventListener('hashchange', () => {
this.loadRoute();
});
// 监听页面加载
window.addEventListener('load', () => {
this.loadRoute();
});
}
// 注册路由
route(path, callback) {
this.routes[path] = callback;
}
// 加载当前路由对应的组件
loadRoute() {
// 获取 hash(去掉 # 号)
this.currentUrl = location.hash.slice(1) || '/';
// 执行对应的回调函数
if (this.routes[this.currentUrl]) {
this.routes[this.currentUrl]();
}
}
// 跳转路由
push(path) {
location.hash = path;
}
}
// 使用示例
const router = new HashRouter();
router.route('/home', () => {
document.getElementById('app').innerHTML = '<h1>首页</h1>';
});
router.route('/about', () => {
document.getElementById('app').innerHTML = '<h1>关于</h1>';
});
// 跳转
router.push('/about');
优缺点
| 优点 | 缺点 |
|---|---|
| ✅ 兼容性好(支持 IE8+) | ❌ URL 带有 #,不够美观 |
| ✅ 无需服务器配置 | ❌ SEO 不友好(爬虫忽略 # 后内容) |
| ✅ 实现简单,不会刷新页面 | ❌ 微信分享等场景可能丢失参数 |
2. History 模式(HTML5)
工作原理
利用 HTML5 的 History API 来管理路由,实现无刷新的 URL 变化。
javascript
// URL 示例(更美观)
http://example.com/home
http://example.com/about
http://example.com/user/123
// 核心 API
history.pushState(state, title, url) // 添加历史记录
history.replaceState(state, title, url) // 替换当前历史记录
window.onpopstate // 监听浏览器前进后退
实现原理
javascript
class HistoryRouter {
constructor() {
this.routes = {};
// 监听 popstate(浏览器前进/后退)
window.addEventListener('popstate', (event) => {
this.loadRoute(location.pathname);
});
}
// 注册路由
route(path, callback) {
this.routes[path] = callback;
}
// 加载路由
loadRoute(path) {
if (this.routes[path]) {
this.routes[path]();
} else {
// 404 处理
this.routes['/404'] && this.routes['/404']();
}
}
// 跳转路由
push(path) {
// 修改 URL 并添加历史记录
history.pushState({}, '', path);
this.loadRoute(path);
}
// 替换路由(不添加历史记录)
replace(path) {
history.replaceState({}, '', path);
this.loadRoute(path);
}
// 前进/后退
go(n) {
history.go(n);
}
}
// 使用示例
const router = new HistoryRouter();
router.route('/home', () => {
document.getElementById('app').innerHTML = '<h1>首页</h1>';
});
router.route('/about', () => {
document.getElementById('app').innerHTML = '<h1>关于</h1>';
});
// 跳转
router.push('/about');
服务器配置(关键点)
History 模式必须配置服务器,否则刷新页面会 404:
nginx
# Nginx 配置
location / {
try_files $uri $uri/ /index.html;
}
apache
# 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>
javascript
// Node.js/Express 配置
const express = require('express');
const app = express();
app.use(express.static('dist'));
app.get('*', (req, res) => {
res.sendFile(path.resolve(__dirname, 'dist', 'index.html'));
});
优缺点
| 优点 | 缺点 |
|---|---|
| ✅ URL 美观,无 # | ❌ 需要服务器配置支持 |
| ✅ SEO 友好 | ❌ 兼容性稍差(IE10+) |
| ✅ 符合 Web 标准 | ❌ 实现相对复杂 |
二、框架中的实现
1. Vue Router
javascript
// Vue 3 + Vue Router 4
import { createRouter, createWebHashHistory, createWebHistory } from 'vue-router'
// Hash 模式
const hashRouter = createRouter({
history: createWebHashHistory(),
routes: [
{ path: '/', component: Home },
{ path: '/about', component: About }
]
})
// History 模式
const historyRouter = createRouter({
history: createWebHistory(),
routes: [
{ path: '/', component: Home },
{ path: '/about', component: About }
]
})
// 使用
app.use(historyRouter)
2. React Router
jsx
// React Router v6
import { BrowserRouter, HashRouter, Routes, Route } from 'react-router-dom'
// Hash 模式
function HashModeApp() {
return (
<HashRouter>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
</Routes>
</HashRouter>
)
}
// History 模式
function HistoryModeApp() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
</Routes>
</BrowserRouter>
)
}
3. 原生 JavaScript 实现简单路由
javascript
// 完整的 SPA 路由实现
class SPARouter {
constructor(options = {}) {
this.mode = options.mode || 'hash'; // 'hash' 或 'history'
this.routes = [];
this.currentView = null;
this.init();
}
init() {
if (this.mode === 'hash') {
window.addEventListener('hashchange', () => this.handleRoute());
window.addEventListener('load', () => this.handleRoute());
} else {
window.addEventListener('popstate', () => this.handleRoute());
// 拦截所有链接点击
document.addEventListener('click', (e) => {
const link = e.target.closest('a');
if (link && link.getAttribute('data-router') !== 'false') {
e.preventDefault();
const href = link.getAttribute('href');
if (href && !href.startsWith('http')) {
this.push(href);
}
}
});
}
}
handleRoute() {
let path;
if (this.mode === 'hash') {
path = window.location.hash.slice(1) || '/';
} else {
path = window.location.pathname;
}
const route = this.routes.find(r => r.path === path);
if (route) {
this.currentView = route.component;
this.render();
} else if (this.routes.find(r => r.path === '/404')) {
this.currentView = this.routes.find(r => r.path === '/404').component;
this.render();
} else {
console.error('路由未找到');
}
}
push(path) {
if (this.mode === 'hash') {
window.location.hash = path;
} else {
history.pushState({}, '', path);
this.handleRoute();
}
}
replace(path) {
if (this.mode === 'hash') {
const url = window.location.href.split('#')[0] + '#' + path;
window.location.replace(url);
} else {
history.replaceState({}, '', path);
this.handleRoute();
}
}
render() {
const app = document.getElementById('app');
if (app && this.currentView) {
app.innerHTML = this.currentView;
}
}
addRoute(path, component) {
this.routes.push({ path, component });
}
}
// 使用
const router = new SPARouter({ mode: 'history' });
router.addRoute('/', '<h1>首页</h1><a href="/about">关于</a>');
router.addRoute('/about', '<h1>关于</h1><a href="/">返回首页</a>');
router.addRoute('/404', '<h1>404 页面未找到</h1>');
router.handleRoute(); // 初始化
三、路由参数处理
javascript
// 动态路由参数解析
class RouteParser {
// 解析路由参数
static parseRoute(routePattern, currentPath) {
const patternParts = routePattern.split('/');
const pathParts = currentPath.split('/');
if (patternParts.length !== pathParts.length) {
return null;
}
const params = {};
for (let i = 0; i < patternParts.length; i++) {
if (patternParts[i].startsWith(':')) {
// 动态参数
const paramName = patternParts[i].slice(1);
params[paramName] = pathParts[i];
} else if (patternParts[i] !== pathParts[i]) {
return null;
}
}
return params;
}
// 查询参数解析
static parseQuery(queryString) {
const params = {};
const search = queryString.startsWith('?') ? queryString.slice(1) : queryString;
if (!search) return params;
search.split('&').forEach(param => {
const [key, value] = param.split('=');
params[decodeURIComponent(key)] = decodeURIComponent(value || '');
});
return params;
}
// 构建查询字符串
static buildQuery(params) {
const query = Object.entries(params)
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
.join('&');
return query ? `?${query}` : '';
}
}
// 使用示例
class Router extends SPARouter {
pushWithParams(path, params = {}, query = {}) {
let finalPath = path;
// 替换路径参数
Object.entries(params).forEach(([key, value]) => {
finalPath = finalPath.replace(`:${key}`, value);
});
// 添加查询参数
const queryString = RouteParser.buildQuery(query);
finalPath += queryString;
this.push(finalPath);
}
getCurrentParams(routePattern) {
const currentPath = this.mode === 'hash'
? window.location.hash.slice(1)
: window.location.pathname;
return RouteParser.parseRoute(routePattern, currentPath);
}
getCurrentQuery() {
const search = window.location.search;
return RouteParser.parseQuery(search);
}
}
四、路由守卫实现
javascript
class RouterWithGuard extends HistoryRouter {
constructor() {
super();
this.beforeHooks = [];
this.afterHooks = [];
}
// 注册前置守卫
beforeEach(callback) {
this.beforeHooks.push(callback);
}
// 注册后置守卫
afterEach(callback) {
this.afterHooks.push(callback);
}
// 重写 push 方法,添加守卫
async push(path) {
// 执行前置守卫
for (const hook of this.beforeHooks) {
const result = await hook(path);
if (result === false) {
console.log('导航被取消');
return;
}
if (typeof result === 'string') {
// 重定向
return super.push(result);
}
}
// 执行路由跳转
super.push(path);
// 执行后置守卫
for (const hook of this.afterHooks) {
hook(path);
}
}
}
// 使用守卫
const router = new RouterWithGuard();
// 登录验证守卫
router.beforeEach((to) => {
const isLoggedIn = localStorage.getItem('token');
if (to === '/admin' && !isLoggedIn) {
alert('请先登录');
return '/login';
}
return true;
});
// 日志守卫
router.afterEach((to) => {
console.log(`路由跳转到: ${to}`, new Date());
});
五、性能优化
javascript
// 路由懒加载
const router = new Router();
// 方式1:动态导入
router.addRoute('/about', async () => {
const module = await import('./pages/About.js');
return module.default;
});
// 方式2:组件缓存
class LazyRouter extends Router {
constructor() {
super();
this.componentCache = new Map();
}
async loadComponent(path, loader) {
// 检查缓存
if (this.componentCache.has(path)) {
return this.componentCache.get(path);
}
// 加载组件
const component = await loader();
this.componentCache.set(path, component);
return component;
}
}
// 路由预加载
class PreloadRouter extends Router {
preload(paths) {
paths.forEach(path => {
const route = this.routes.find(r => r.path === path);
if (route && route.preload) {
route.preload(); // 提前加载组件
}
});
}
}
// 鼠标悬停时预加载
document.querySelectorAll('a[data-router]').forEach(link => {
link.addEventListener('mouseenter', () => {
const path = link.getAttribute('href');
router.preload([path]);
});
});
六、选择建议
使用 Hash 模式的场景
- ✅ 项目不需要 SEO
- ✅ 无需服务器配置(如 GitHub Pages)
- ✅ 需要兼容老旧浏览器
- ✅ 内部管理系统
使用 History 模式的场景
- ✅ 需要 SEO 优化
- ✅ 追求 URL 美观
- ✅ 可以配置服务器
- ✅ 面向现代浏览器
实际项目配置示例
javascript
// vue.config.js (Vue CLI)
module.exports = {
publicPath: '/',
devServer: {
historyApiFallback: true // 开发环境支持 History 模式
}
}
// 生产环境 nginx 配置
server {
listen 80;
server_name example.com;
root /var/www/dist;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
# 静态资源缓存
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
}
总结
| 对比项 | Hash 模式 | History 模式 |
|---|---|---|
| URL | /#/about |
/about |
| 原理 | hashchange 事件 | History API + popstate |
| 服务器配置 | 不需要 | 需要 |
| SEO | 不友好 | 友好 |
| 兼容性 | IE8+ | IE10+ |
| 实现复杂度 | 简单 | 中等 |
| 适用场景 | 管理系统、工具类应用 | 官网、博客、C端产品 |
最佳实践:
- 新项目优先考虑 History 模式(配置好服务器即可)
- 无法配置服务器或临时项目用 Hash 模式
- 开发环境两种模式都支持,通过配置切换
- 注意处理刷新 404 问题(History 模式必须配置 fallback)