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 实际项目中的注意事项

最后分享几个我在实战中总结的经验:

  1. hash 值的规范化:我习惯统一用 / 开头,比如 #/home 而不是 #home。这样和 History 路由的路径风格一致,以后迁移也方便。
  2. 处理默认路由:页面首次加载时,如果 hash 为空,应该自动跳转到首页。这个逻辑别忘了。
  3. 路由守卫:虽然 Hash 路由实现简单,但路由守卫(比如登录验证)还是要加的。可以在 handleRoute 方法里加一层拦截逻辑。
  4. 性能优化:如果路由表很大,建议用 Map 而不是对象来存储。Map 的查找性能更稳定。

好了,Hash 路由的内容就讲到这里。你想想看,其实原理并不复杂,关键是要理解它「不刷新页面」这个核心特性。下一节我们会讲 History 路由,到时候你就能对比出两者的差异了。