1. 定义
Hash 路由(又称锚点路由)是一种基于浏览器 URL 中 # 号之后的部分来实现前端页面状态管理与视图切换的技术方案。其核心特征为:路径变更不触发服务器请求,完全由前端 JavaScript 控制渲染逻辑。
在单页应用(SPA)架构中,Hash 路由与 History 路由并列为两大主流前端路由实现方式。
2. 技术基础:浏览器底层机制
Hash 路由的可行性建立在两个浏览器原生机制之上:
2.1 # 的 HTTP 协议语义
根据 RFC 3986(URI 通用语法规范),URL 中的 # 标识符称为 fragment(片段标识符),其语义为"指向当前资源内的一个次级资源"(如章节锚点)。
关键特性:
- fragment 部分不会被发送至服务器,仅由客户端解析。
- 修改 fragment 不会触发页面重新加载,因为资源定位符(scheme + host + path + query)未发生变化。
- 浏览器不会在 fragment 变化时向服务器发起新请求。
技术内涵:这实质上是将"路由解析权"从服务端转移至客户端,使前端获得了路径状态的管理能力。
2.2 hashchange 事件
HTML5 规范定义了 hashchange 事件,当 URL 的片段标识符(即 # 及之后的部分)发生变化时,浏览器自动触发该事件。
事件对象属性:
| 属性 | 描述 |
|---|---|
oldURL |
变化前的完整 URL |
newURL |
变化后的完整 URL |
通过监听此事件,前端应用可以感知路由变化并执行相应的视图更新逻辑。
3. 核心工作原理
Hash 路由的运行流程可拆解为四个环节:
① 用户操作(点击链接 / 浏览器前进后退 / 手动修改地址栏)
↓
② location.hash 发生变更
↓
③ 浏览器触发 hashchange 事件(页面不刷新)
↓
④ 事件监听回调执行路由匹配 → 渲染对应视图
3.1 路由变更的触发方式
| 触发方式 | 是否触发 hashchange |
备注 |
|---|---|---|
<a href="#/about"> 点击 |
✅ 是 | 浏览器默认行为 |
window.location.hash = '#/about' |
✅ 是 | 唯一正确的编程式修改方式 |
window.location.hash = '/about'(不带 #) |
❌ 否 | 赋值会被忽略(# 是必需的前缀) |
history.pushState(null, '', '#/about') |
❌ 否 | pushState() 修改哈希值时不会触发 hashchange 事件。且根据 HTML 标准,pushState 的 URL 参数中 fragment 部分可能被忽略或处理方式与预期不同。不推荐通过此方式修改 hash。 |
history.replaceState(null, '', '#/about') |
❌ 否 | replaceState() 修改哈希值时同样不会触发 hashchange 事件。 |
| 浏览器前进/后退(同源内 hash 变化) | ✅ 是 | 仅当实际 hash 值发生变化时触发 |
| 页面刷新 | ❌ 否(通过 DOMContentLoaded 恢复) |
刷新后 location.hash 保留,需手动读取并渲染 |
核心结论:
- 编程式修改 hash 的唯一正确方式是
window.location.hash = '#/path'。该方法会同步更新地址栏、location.hash属性,并自动触发hashchange事件。pushState()和replaceState()无论是否修改了 hash,都不会触发hashchange事件。
3.2 路由匹配策略
路由匹配的具体算法是各路由库的设计选择,而非 Hash 路由的固有原理。不同的路由库(如 React Router、Vue Router)对动态路由(如 /user/:id)的匹配优先级和算法(如正则、路径评分)各不相同。
以下为一种常见的匹配策略示例:
// 1. 定义路由表(路径 → 视图构造函数/组件)
const routeTable = new Map([
['/home', HomeComponent],
['/user/:id', UserComponent],
['/settings', SettingsComponent]
]);
// 2. 解析当前路径(支持动态参数)
function parseHash() {
const raw = location.hash.slice(1); // 去掉 '#'
const [path, queryString] = raw.split('?');
// 解析查询参数
const params = new URLSearchParams(queryString || '');
return { path, params };
}
// 3. 路由匹配(示例:精确匹配优先)
function matchRoute(path) {
// 精确匹配优先,再遍历动态路由
// 返回 { component, params }
}
4. 优势分析
4.1 零服务器依赖
Hash 路由完全在前端运行,无需任何服务端配置(如 Nginx rewrite 规则)。即便使用 file:// 协议直接打开 HTML 文件,路由依然正常工作。
4.2 浏览器兼容性
hashchange 事件在主流浏览器中支持良好。在 IE8 中,该事件可通过 attachEvent('onhashchange', callback) 方式实现监听。onhashchange 属性方式在 IE9 之后得到更广泛支持。对于 IE7 及更早版本,通常需要通过 setInterval 轮询 location.hash 来模拟。
4.3 状态持久化
页面刷新后,location.hash 保留不变,应用可通过 DOMContentLoaded 事件读取并恢复至刷新前的路由状态。
4.4 天然的导航历史
每次 hash 变更自动产生一条浏览器历史记录,用户的前进/后退操作得到原生支持,无需额外维护历史栈。
4.5 隔离性
Hash 路由的路径信息与服务器资源路径隔离,不会与后端 API 路由产生命名冲突(例如 /api/users 与 #/users 互不干扰)。
5. 局限性
| 局限 | 说明 |
|---|---|
| SEO 不友好 | 搜索引擎爬虫通常不执行 JavaScript,无法抓取 # 后的动态内容(Google 虽可索引部分 SPA 内容,但存在限制与延迟) |
| URL 美观度 | 地址栏包含 #,视觉上不如纯净路径 |
| 锚点冲突 | 页面原生锚点跳转(如 #section-1)与路由路径(#/home)共用同一命名空间。实际工程中,采用 #/ 前缀统一风格即可规避冲突,传统锚点功能可通过 JS 滚动控制替代,属于工程实践问题,可通过设计规范轻松规避,并非根本性局限 |
| 服务端接收不到 | # 后的所有内容不发送至服务端,日志分析、服务端鉴权等场景需通过其他方式(如 Cookie、Header)传递状态 |
6. 与 History 路由的技术对比
| 对比维度 | Hash 路由 | History 路由(基于 History API) |
|---|---|---|
| 实现基础 | hashchange 事件 |
history.pushState + popstate |
| 地址格式 | xxx.com/#/path |
xxx.com/path |
| 服务器配置 | 无需 | 需配置 fallback(否则刷新返回 404) |
| SEO 支持 | 差 | 优(配合 SSR/预渲染) |
| 路径参数传递 | URL 长度限制(同 HTTP) | URL 长度限制(同 HTTP),额外支持 state 对象传递非 URL 数据(注意:该数据不持久化,刷新页面后丢失,需另行存储如 localStorage) |
| 服务端日志记录 | 无法记录完整路径 | 可记录完整路径 |
7. 实现参考(框架无关)
以下为一个遵循关注点分离原则的 Hash 路由最小实现:
class HashRouter {
constructor(routes, { defaultRoute = '/home' } = {}) {
this.routes = routes;
this.defaultRoute = defaultRoute;
this.currentPath = defaultRoute;
// 绑定事件
window.addEventListener('hashchange', this._onHashChange.bind(this));
window.addEventListener('DOMContentLoaded', this._onHashChange.bind(this));
// 初始化
this._navigate(this._getCurrentPath());
}
_getCurrentPath() {
return location.hash.slice(1) || this.defaultRoute;
}
_onHashChange() {
const path = this._getCurrentPath();
this._navigate(path);
}
_navigate(path) {
const route = this.routes.find(r => r.path === path);
if (route) {
this.currentPath = path;
route.render();
} else {
// 404 处理
this._handleNotFound(path);
}
}
_handleNotFound(path) {
const fallback = this.routes.find(r => r.path === '*') || null;
if (fallback) {
fallback.render();
} else {
console.warn(`[HashRouter] 未匹配到路由: ${path}`);
}
}
// 编程式导航 —— 直接操作 location.hash
push(path) {
// 标准方式:赋值给 location.hash,自动触发 hashchange
location.hash = path.startsWith('#') ? path : `#${path}`;
}
// 替换当前历史记录条目
replace(path) {
const normalizedPath = path.startsWith('#') ? path : `#${path}`;
// 直接通过 location.hash 修改,会触发 hashchange
// 注意:这会新增一条历史记录,如需替换语义,可配合 history.replaceState 使用
// 但需注意 replaceState 本身不会触发 hashchange
location.hash = normalizedPath;
// 如需替换当前历史条目而非新增,可额外调用:
// history.replaceState(null, '', location.href);
}
}
7.1 关于 hashchange 与 History API 的重要说明
pushState()和replaceState()无论是否修改了 hash,都不会触发hashchange事件。- 修改 hash 并触发
hashchange的唯一标准方式是操作location.hash。 - 如需在修改 hash 的同时控制历史栈(如替换而非新增),可组合使用
location.hash赋值与history.replaceState,但需明确后者不会触发hashchange,事件仍由前者的赋值操作触发。
8. 适用场景
| 场景 | 是否适用 | 理由 |
|---|---|---|
| 后台管理系统 | ✅ 强烈推荐 | 无需 SEO,配置简单,开发效率高 |
| 移动端 H5 / WebApp | ✅ 推荐 | 兼容性广,历史记录原生支持 |
| 浏览器扩展 / Electron | ✅ 推荐 | 无服务器环境,hash 路由完美适配 |
| 官网 / 营销落地页 | ⚠️ 谨慎 | 若需 SEO,建议使用 History 模式 + SSR |
| 博客 / 内容型站点 | ❌ 不推荐 | SEO 需求优先级高于开发便捷性 |
9. 总结
Hash 路由是前端路由演进过程中的经典方案,其核心价值在于利用浏览器原生特性将路由控制权完整赋予前端,同时保持极低的接入成本和良好的兼容性。在单页应用、后台系统、H5 页面等场景中,Hash 路由至今仍是极具竞争力的技术选型。对于需要 SEO 优化的场景,可考虑在其基础上平滑升级至 History 路由方案。
还没有评论,来抢沙发吧~