实现一个简单的路由

手写 Hash 路由和 History 路由

问题

实现一个简单的前端路由,支持 Hash 模式和 History 模式。

解答

Hash 路由

class HashRouter {
  constructor() {
    this.routes = {}
    this.currentPath = ''
    
    // 监听 hash 变化
    window.addEventListener('hashchange', this.onHashChange.bind(this))
    // 页面加载时也要处理
    window.addEventListener('load', this.onHashChange.bind(this))
  }
  
  // 注册路由
  route(path, callback) {
    this.routes[path] = callback
  }
  
  // hash 变化时触发
  onHashChange() {
    // 获取 # 后面的路径
    this.currentPath = window.location.hash.slice(1) || '/'
    const callback = this.routes[this.currentPath]
    if (callback) {
      callback()
    }
  }
  
  // 跳转
  push(path) {
    window.location.hash = path
  }
}

// 使用示例
const router = new HashRouter()

router.route('/', () => {
  console.log('首页')
})

router.route('/about', () => {
  console.log('关于页')
})

router.route('/user', () => {
  console.log('用户页')
})

// 跳转到 /about
router.push('/about')

History 路由

class HistoryRouter {
  constructor() {
    this.routes = {}
    this.currentPath = ''
    
    // 监听浏览器前进后退
    window.addEventListener('popstate', this.onPopState.bind(this))
  }
  
  // 注册路由
  route(path, callback) {
    this.routes[path] = callback
  }
  
  // popstate 触发时执行
  onPopState() {
    this.currentPath = window.location.pathname
    const callback = this.routes[this.currentPath]
    if (callback) {
      callback()
    }
  }
  
  // 跳转(会添加历史记录)
  push(path) {
    window.history.pushState({}, '', path)
    this.currentPath = path
    const callback = this.routes[path]
    if (callback) {
      callback()
    }
  }
  
  // 替换(不添加历史记录)
  replace(path) {
    window.history.replaceState({}, '', path)
    this.currentPath = path
    const callback = this.routes[path]
    if (callback) {
      callback()
    }
  }
  
  // 前进/后退
  go(n) {
    window.history.go(n)
  }
  
  back() {
    this.go(-1)
  }
  
  forward() {
    this.go(1)
  }
}

// 使用示例
const router = new HistoryRouter()

router.route('/', () => {
  console.log('首页')
})

router.route('/about', () => {
  console.log('关于页')
})

router.push('/about')

支持参数的路由

class Router {
  constructor(mode = 'hash') {
    this.mode = mode
    this.routes = []
    this.currentPath = ''
    
    this.init()
  }
  
  init() {
    if (this.mode === 'hash') {
      window.addEventListener('hashchange', this.handleChange.bind(this))
      window.addEventListener('load', this.handleChange.bind(this))
    } else {
      window.addEventListener('popstate', this.handleChange.bind(this))
    }
  }
  
  // 获取当前路径
  getPath() {
    if (this.mode === 'hash') {
      return window.location.hash.slice(1) || '/'
    }
    return window.location.pathname
  }
  
  // 注册路由,支持动态参数如 /user/:id
  route(path, callback) {
    // 将 /user/:id 转换为正则
    const paramNames = []
    const regexPath = path.replace(/:([^/]+)/g, (_, name) => {
      paramNames.push(name)
      return '([^/]+)'
    })
    
    this.routes.push({
      path,
      regex: new RegExp(`^${regexPath}$`),
      paramNames,
      callback
    })
  }
  
  // 路径变化处理
  handleChange() {
    this.currentPath = this.getPath()
    
    for (const route of this.routes) {
      const match = this.currentPath.match(route.regex)
      if (match) {
        // 提取参数
        const params = {}
        route.paramNames.forEach((name, index) => {
          params[name] = match[index + 1]
        })
        route.callback(params)
        return
      }
    }
  }
  
  push(path) {
    if (this.mode === 'hash') {
      window.location.hash = path
    } else {
      window.history.pushState({}, '', path)
      this.handleChange()
    }
  }
}

// 使用示例
const router = new Router('hash')

router.route('/user/:id', (params) => {
  console.log('用户ID:', params.id)
})

router.route('/post/:id/comment/:commentId', (params) => {
  console.log('文章ID:', params.id, '评论ID:', params.commentId)
})

router.push('/user/123')  // 输出: 用户ID: 123

关键点

  • Hash 路由通过 hashchange 事件监听 URL 中 # 后面的变化
  • History 路由通过 popstate 事件监听浏览器前进后退,pushState/replaceState 不会触发该事件
  • History 模式需要服务端配合,所有路由都返回同一个 HTML
  • 动态路由参数可以用正则匹配,将 /user/:id 转换为 /user/([^/]+)
  • Hash 模式兼容性好,History 模式 URL 更美观