实现一个 Hash 路由

手写实现基于 hash 的前端路由系统,支持路由注册、跳转、监听等功能

问题

在单页应用(SPA)中,需要实现一个基于 URL hash 的路由系统。该系统需要支持:

  • 路由注册和匹配
  • 监听 hash 变化并触发对应的回调
  • 编程式导航(跳转到指定路由)
  • 支持路由参数解析

解答

class HashRouter {
  constructor() {
    // 存储路由路径和对应的回调函数
    this.routes = {};
    // 当前路由
    this.currentUrl = '';
    // 初始化监听
    this.init();
  }

  // 注册路由
  route(path, callback) {
    this.routes[path] = callback;
  }

  // 刷新路由,执行对应的回调
  refresh() {
    // 获取当前 hash 值,去掉 # 号
    this.currentUrl = location.hash.slice(1) || '/';
    
    // 解析路由参数
    const [path, query] = this.currentUrl.split('?');
    const params = this.parseQuery(query);
    
    // 查找匹配的路由
    const matchedRoute = this.matchRoute(path);
    
    if (matchedRoute) {
      // 执行对应的回调函数
      matchedRoute.callback(params);
    }
  }

  // 匹配路由(支持动态路由)
  matchRoute(path) {
    // 精确匹配
    if (this.routes[path]) {
      return { callback: this.routes[path], params: {} };
    }

    // 动态路由匹配,如 /user/:id
    for (let route in this.routes) {
      const routeRegex = this.pathToRegex(route);
      const match = path.match(routeRegex);
      
      if (match) {
        const params = this.extractParams(route, path);
        return { 
          callback: (queryParams) => this.routes[route]({ ...params, ...queryParams }),
          params 
        };
      }
    }

    return null;
  }

  // 将路由路径转换为正则表达式
  pathToRegex(path) {
    // 将 /user/:id 转换为 /user/([^/]+)
    const pattern = path
      .replace(/\//g, '\\/')
      .replace(/:\w+/g, '([^\\/]+)');
    return new RegExp(`^${pattern}$`);
  }

  // 提取动态路由参数
  extractParams(route, path) {
    const params = {};
    const routeParts = route.split('/');
    const pathParts = path.split('/');

    routeParts.forEach((part, index) => {
      if (part.startsWith(':')) {
        const paramName = part.slice(1);
        params[paramName] = pathParts[index];
      }
    });

    return params;
  }

  // 解析查询参数
  parseQuery(query) {
    if (!query) return {};
    
    const params = {};
    query.split('&').forEach(param => {
      const [key, value] = param.split('=');
      params[key] = decodeURIComponent(value || '');
    });
    
    return params;
  }

  // 编程式导航
  push(path) {
    location.hash = path;
  }

  // 替换当前路由(不产生历史记录)
  replace(path) {
    location.replace(`${location.pathname}${location.search}#${path}`);
  }

  // 后退
  back() {
    history.back();
  }

  // 前进
  forward() {
    history.forward();
  }

  // 初始化监听
  init() {
    // 监听 hash 变化
    window.addEventListener('hashchange', () => {
      this.refresh();
    });

    // 监听页面加载
    window.addEventListener('load', () => {
      this.refresh();
    });
  }
}

使用示例

// 创建路由实例
const router = new HashRouter();

// 注册路由
router.route('/', () => {
  console.log('首页');
  document.getElementById('app').innerHTML = '<h1>首页</h1>';
});

router.route('/about', () => {
  console.log('关于页面');
  document.getElementById('app').innerHTML = '<h1>关于我们</h1>';
});

// 动态路由
router.route('/user/:id', (params) => {
  console.log('用户详情页', params);
  document.getElementById('app').innerHTML = 
    `<h1>用户ID: ${params.id}</h1>`;
});

// 带查询参数的路由
router.route('/search', (params) => {
  console.log('搜索页面', params);
  document.getElementById('app').innerHTML = 
    `<h1>搜索关键词: ${params.keyword || '无'}</h1>`;
});

// HTML 中使用
// <a href="#/">首页</a>
// <a href="#/about">关于</a>
// <a href="#/user/123">用户123</a>
// <a href="#/search?keyword=vue">搜索</a>

// 编程式导航
router.push('/about');
router.push('/user/456');
router.push('/search?keyword=react');

// 路由操作
router.back();    // 后退
router.forward(); // 前进
router.replace('/'); // 替换当前路由

关键点

  • 原理:利用 URL 的 hash 部分(# 后面的内容)来实现路由,hash 变化不会导致页面刷新

  • 事件监听:通过监听 hashchange 事件来捕获路由变化,通过 load 事件处理页面首次加载

  • 路由匹配:支持静态路由精确匹配和动态路由参数匹配(如 /user/:id

  • 参数解析

    • 动态路由参数:从路径中提取(如 :id
    • 查询参数:从 ? 后面解析(如 ?keyword=vue
  • 编程式导航:提供 pushreplacebackforward 等方法,方便在代码中控制路由跳转

  • 正则匹配:将路由路径转换为正则表达式,实现灵活的路由匹配规则

  • 兼容性好:hash 路由兼容性强,支持所有浏览器,不需要服务器配置