2、Hash路由实现:从原理到实战
说到前端路由,我最早接触的就是 Hash 路由。那时候还在用 jQuery 写单页应用,每次页面切换都要手动管理 URL,说实话挺痛苦的。后来发现了 window.location.hash 这个宝贝,才算是打开了新世界的大门。
2.1 什么是 Hash 路由?
说白了,Hash 路由就是利用 URL 中 # 后面的部分来做页面导航。比如 http://example.com/#/home,这里的 #/home 就是 hash 值。
你可能会问:为什么选 hash 来做路由?原因很简单——hash 的变化不会导致页面刷新。这是浏览器天生的特性,也是 SPA 能实现「无刷新切换」的基础。
核心要点:
- hash 值以
#开头,后面跟任意字符串 - 改变 hash 不会触发页面重新加载
- hash 值可以通过
window.location.hash获取或设置
2.2 window.location.hash 详解
先看个最简单的例子。打开浏览器控制台,试试这几行代码:
// 获取当前 hash
console.log(window.location.hash); // 比如 "#/home"
// 设置 hash
window.location.hash = '/about';
// 此时 URL 变成 http://example.com/#/about
// 注意:设置 hash 时,# 号可以省略
window.location.hash = 'profile';
// 效果同上,URL 变成 http://example.com/#/profile
嗯,这里要注意一点:window.location.hash 返回的值是包含 # 号的。我刚开始写的时候经常忘记这个细节,导致字符串比较时出 bug。
2.3 hashchange 事件
光能设置 hash 还不够,我们还得监听 hash 的变化。这时候 hashchange 事件就派上用场了。
window.addEventListener('hashchange', function(event) {
console.log('旧的 hash:', event.oldURL);
console.log('新的 hash:', event.newURL);
console.log('当前 hash:', window.location.hash);
});
这个事件会在 hash 发生变化时触发。注意,它不会在页面首次加载时触发。如果你需要在页面加载时也执行路由逻辑,得手动调用一次。
我的小技巧: 我习惯把路由初始化逻辑封装成一个函数,然后在页面加载和 hashchange 时都调用它。这样代码更干净,也避免重复。
2.4 手动实现简易 Hash 路由
理论说完了,咱们动手写一个。我会带你一步步实现一个麻雀虽小五脏俱全的 Hash 路由。
2.4.1 基础版本
class HashRouter {
constructor() {
this.routes = {}; // 路由表
this.currentHash = ''; // 当前 hash
// 监听 hash 变化
window.addEventListener('hashchange', () => {
this.handleRoute();
});
// 页面加载时也要处理
window.addEventListener('load', () => {
this.handleRoute();
});
}
// 注册路由
register(path, callback) {
this.routes[path] = callback;
}
// 处理路由
handleRoute() {
this.currentHash = window.location.hash.slice(1) || '/';
const callback = this.routes[this.currentHash];
if (callback) {
callback();
} else {
// 404 处理
console.warn(`路由 ${this.currentHash} 未注册`);
}
}
// 跳转到指定路径
navigate(path) {
window.location.hash = path;
}
}
// 使用示例
const router = new HashRouter();
router.register('/home', () => {
document.getElementById('app').innerHTML = '<h2>首页</h2>';
});
router.register('/about', () => {
document.getElementById('app').innerHTML = '<h2>关于我们</h2>';
});
// 默认跳转到首页
router.navigate('/home');
这个版本够简单吧?但实际项目中,我们还需要更多功能。
2.4.2 支持参数传递
我在项目中遇到过这样的需求:用户详情页的 URL 是 #/user/123,需要把 123 这个 ID 提取出来。来看看怎么实现:
class HashRouter {
constructor() {
this.routes = [];
this.currentHash = '';
window.addEventListener('hashchange', () => this.handleRoute());
window.addEventListener('load', () => this.handleRoute());
}
// 支持动态路由,比如 /user/:id
register(path, callback) {
// 把 :id 这样的参数转换成正则
const paramNames = [];
const regexStr = path.replace(/:([^/]+)/g, (match, name) => {
paramNames.push(name);
return '([^/]+)';
});
this.routes.push({
regex: new RegExp(`^${regexStr}$`),
paramNames,
callback
});
}
handleRoute() {
this.currentHash = window.location.hash.slice(1) || '/';
for (const route of this.routes) {
const match = this.currentHash.match(route.regex);
if (match) {
// 提取参数
const params = {};
route.paramNames.forEach((name, index) => {
params[name] = match[index + 1];
});
route.callback({ params });
return;
}
}
// 没有匹配到路由
console.warn(`路由 ${this.currentHash} 未匹配`);
}
navigate(path) {
window.location.hash = path;
}
}
// 使用
const router = new HashRouter();
router.register('/user/:id', ({ params }) => {
console.log('用户 ID:', params.id);
document.getElementById('app').innerHTML = `<h2>用户详情:${params.id}</h2>`;
});
router.navigate('/user/42');
我曾经踩过的坑: 正则匹配时要注意转义特殊字符。比如路径中有 . 或 ? 时,记得用 \\ 转义。不然匹配结果会出乎你的意料。
2.5 Hash 路由优缺点分析
用了这么多年 Hash 路由,它的优缺点我太清楚了。咱们客观地分析一下:
| 优点 | 缺点 |
|---|---|
| 兼容性好,支持 IE8+ | URL 带 # 号,不够美观 |
| 实现简单,不需要服务端配合 | SEO 不友好,搜索引擎抓取不到 hash 后的内容 |
| 不会触发页面刷新,体验流畅 | 锚点功能会冲突(比如页面内跳转) |
| 支持浏览器前进后退 | 分享链接时可能丢失 hash 状态 |
我的建议:
- 如果是内部管理系统、后台面板,Hash 路由完全够用
- 如果是面向用户的官网、博客,建议用 History 路由
- 如果项目需要 SEO,Hash 路由基本没戏,得用服务端渲染
2.6 实际项目中的注意事项
最后分享几个我在实战中总结的经验:
- hash 值的规范化:我习惯统一用
/开头,比如#/home而不是#home。这样和 History 路由的路径风格一致,以后迁移也方便。 - 处理默认路由:页面首次加载时,如果 hash 为空,应该自动跳转到首页。这个逻辑别忘了。
- 路由守卫:虽然 Hash 路由实现简单,但路由守卫(比如登录验证)还是要加的。可以在
handleRoute方法里加一层拦截逻辑。 - 性能优化:如果路由表很大,建议用 Map 而不是对象来存储。Map 的查找性能更稳定。
好了,Hash 路由的内容就讲到这里。你想想看,其实原理并不复杂,关键是要理解它「不刷新页面」这个核心特性。下一节我们会讲 History 路由,到时候你就能对比出两者的差异了。