前端路由:History 和 Hash 的深入解析

432 阅读3分钟

前言

前端路由是现代单页应用(SPA)的核心机制之一,它允许在不刷新页面的情况下改变URL并渲染不同内容。下面我将详细讲解history和hash两种路由模式的表现、API以及相关概念。

1. History 路由模式

基本原理

History 路由模式是基于 HTML5 History API 实现的客户端路由方案,它通过操作浏览器的历史记录栈来改变 URL,而不引起页面刷新。这种模式消除了传统哈希路由中的 # 符号,使 URL 看起来更加自然。

核心API

1. history.pushState(state, unused, url)

详细工作机制

  • 向浏览器历史记录栈中添加一个新条目
  • 仅修改 URL 和状态对象,不触发页面加载或刷新
  • 新 URL 必须与当前页面同源(协议+域名+端口相同)

参数详解

  • state:可以存储任意可序列化的数据(最大约 640k)

    • 可用于保存页面状态,如滚动位置、表单数据等
    • 可通过 history.state 或 popstate 事件的 event.state 访问
  • title:目前大多数浏览器忽略此参数

  • url:新历史条目的 URL

    • 可以是绝对路径或相对路径
    • 如果只修改查询参数或哈希,可以使用 location.search 或 location.hash

使用场景

// 导航到新页面
function navigateTo(page) {
  const state = { page, timestamp: Date.now() };
  history.pushState(state, '', `/pages/${page}`);
  updateView(page); // 手动更新视图
}

// 修改查询参数
function updateQuery(params) {
  const url = `${location.pathname}?${new URLSearchParams(params)}`;
  history.pushState({ params }, '', url);
}

2. history.replaceState(state, title, url)

与 pushState 的区别

  • 不创建新的历史记录,而是替换当前记录
  • 不会增加历史记录栈的长度
  • 后退按钮不会返回到被替换前的 URL

典型应用场景

// 修正当前URL(如去掉跟踪参数)
if (location.search.includes('utm_source')) {
  const cleanUrl = location.pathname + location.hash;
  history.replaceState(history.state, '', cleanUrl);
}

// 更新状态不改变URL
history.replaceState({ ...history.state, data: newData }, '');

3. window.onpopstate

触发条件

  • 用户点击浏览器前进/后退按钮
  • 调用 history.back()history.forward() 或 history.go()
  • 不会在 pushState 或 replaceState 调用时触发

事件对象属性

  • state:关联的历史记录状态对象
  • type:事件类型("popstate")

高级处理模式

window.addEventListener('popstate', (event) => {
  const state = event.state || {};
  console.log('Navigating to:', window.location.pathname);
  
  // 恢复页面状态
  if (state.scrollPosition) {
    window.scrollTo(state.scrollPosition);
  }
  
  // 根据URL更新视图
  updateViewFromURL();
});

// 保存滚动位置
window.addEventListener('scroll', () => {
  history.replaceState(
    { ...history.state, scrollPosition: window.scrollY },
    ''
  );
});

4. history.state

特点

  • 只读属性,返回当前历史记录的状态对象副本
  • 初始值为 null
  • 状态对象会被浏览器序列化,因此每次访问返回的都是新对象

使用技巧

// 合并更新状态
function updateState(newState) {
  history.replaceState(
    { ...history.state, ...newState },
    ''
  );
}

// 获取当前状态
function getCurrentState() {
  return history.state || {};
}

当然还有一些常用的API,大家可以去看看History,里面基本有所有关于History的方法和属性,可以点击相应的链接进行查看和使用

2. Hash 路由模式

基本原理

Hash 路由模式是利用浏览器 URL 中哈希部分(# 及其后面的内容)的变化来实现客户端路由的一种技术方案。哈希部分的改变不会导致浏览器向服务器发起请求,但会创建历史记录条目,这使得它成为实现单页应用路由的理想选择。

哈希路由的工作原理

  1. URL 结构解析

    • 完整URL格式:https://example.com/#/path?query=value
    • 哈希部分:从 # 开始到 URL 结束的所有内容
    • 浏览器会将哈希部分视为页面内的锚点,不会发送到服务器
  2. 路由变化机制

    • 当哈希部分改变时,浏览器会触发 hashchange 事件
    • 页面不会重新加载,但会在浏览历史中添加记录
    • 可以通过前进/后退按钮导航哈希变化历史
  3. 与传统锚点的区别

    • 传统锚点:#section1 用于页面内跳转
    • 路由哈希:#/path 模拟完整路径结构,通常带有斜杠

核心API

1. window.location.hash

详细工作机制

  • 可读写属性,用于获取或设置当前 URL 的哈希部分
  • 设置新哈希值会创建新的浏览器历史记录
  • 哈希值应包括 # 符号(设置时可省略,但获取时包含)

参数详解

  • 获取时:返回包含 # 的完整哈希字符串(如 "#/home"
  • 设置时:可以带或不带 #"#/home" 或 "/home" 效果相同)

使用场景

// 基本导航
function navigateTo(path) {
  window.location.hash = `#${path}`;
  // 等效于 window.location.hash = path;
}

// 获取当前路由
function getCurrentRoute() {
  return window.location.hash.substring(1) || '/'; // 去掉#并处理空哈希
}

// 修改哈希但不创建历史记录(替换当前记录)
function replaceHash(path) {
  const href = window.location.href.replace(/(#.+)?$/, `#${path}`);
  window.location.replace(href);
}

2. window.onhashchange

触发条件

  • URL 的哈希部分发生改变时触发
  • 包括用户手动修改 URL、脚本修改 location.hash、浏览器前进/后退操作
  • 哈希值相同不会触发(如从 #/home 再次设置为 #/home

事件对象属性

  • oldURL:变化前的完整 URL(部分浏览器可能不支持)
  • newURL:变化后的完整 URL(部分浏览器可能不支持)

高级处理模式

window.addEventListener('hashchange', (event) => {
  // 兼容性处理
  const oldHash = event.oldURL 
    ? new URL(event.oldURL).hash 
    : previousHash;
  
  const newHash = event.newURL 
    ? new URL(event.newURL).hash 
    : window.location.hash;
  
  console.log(`Hash changed from ${oldHash} to ${newHash}`);
  
  // 路由处理
  handleRouteChange(newHash);
  
  // 保存当前哈希
  previousHash = newHash;
});

// 手动跟踪前一个哈希(兼容不支持oldURL的浏览器)
let previousHash = window.location.hash;

特点

  1. 兼容性好(支持旧浏览器)
  2. 不需要服务器特殊配置
  3. URL中有#号,不太美观
  4. 没有同源限制
  5. 不能存储状态对象

3. 两种模式的比较

特性History模式Hash模式
URL美观度美观有#号
服务器配置需要特殊配置不需要
兼容性IE10+几乎所有浏览器
状态管理支持state对象不支持
同源限制
SEO友好度较好较差

4. 实际应用示例

History模式实现

// 路由表
const routes = {
  '/': 'Home Page',
  '/about': 'About Page',
  '/contact': 'Contact Page'
};

// 初始化路由
function navigate() {
  const path = window.location.pathname;
  document.getElementById('content').innerText = routes[path] || '404 Not Found';
}

// 监听popstate事件
window.addEventListener('popstate', navigate);

// 编程式导航
function push(path) {
  history.pushState({}, '', path);
  navigate();
}

// 初始加载
navigate();

Hash模式实现

// 路由表
const routes = {
  '': 'Home Page',
  '#/about': 'About Page',
  '#/contact': 'Contact Page'
};

// 初始化路由
function navigate() {
  const hash = window.location.hash || '#/';
  document.getElementById('content').innerText = routes[hash] || '404 Not Found';
}

// 监听hashchange事件
window.addEventListener('hashchange', navigate);

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

// 初始加载
navigate();

总结

前端路由是现代Web应用的核心技术之一。History模式提供了更干净的URL但需要服务器支持,Hash模式兼容性更好但URL不够美观。现代前端框架都有成熟的路由解决方案,理解底层原理有助于更好地使用这些工具和解决复杂路由场景。

在实际项目中,选择哪种路由模式取决于你的需求:

  • 如果需要更好的SEO和美观的URL,使用History模式
  • 如果需要更好的兼容性或无法配置服务器,使用Hash模式