核心概念与本质区别

  1. 共同目标:

    1. 实现 无刷新页面跳转 (SPA 核心需求)
    2. 支持浏览器前进 / 后退按钮 (维持浏览历史栈)
    3. 关联 URL 与页面状态 (便于分享、书签收藏)
  2. 本质区别:

    维度 Hash API History API
    URL 表现 带有 # (锚点标识),如 http://a.com/#/user #,如 http://a.com/user (正常 URL)
    底层原理 基于 URL 中的 hash 片段 (锚点),仅客户端可见 基于 HTML5 新增的浏览器历史栈 API,可操作历史记录
    服务器依赖 (刷新页面时服务器仅接收 # 前的内容) (刷新页面需服务器配置路由转发,否则 404)
    历史记录修改权限 仅能新增历史记录 (hashchange 触发) 可新增、替换、删除历史记录 (灵活操作栈)
    状态数据存储 需编码到 hash 字符串中 (长度有限) 支持 state 参数存储任意类型数据 (客户端存储)
    兼容性 IE8+ 支持 (兼容性极好) IE10+ 支持 (现代浏览器主流)

Hash API 详解

  1. HashURL# 及其后面的部分 (如 #/user/123),原本用于页面内锚点定位,前端路由借其特性实现无刷新跳转;

  2. Hash 变化的本质是浏览器对 历史栈的新增操作:每次修改 hash,浏览器会在历史栈中添加一条新记录,因此支持前进 / 后退;

  3. 锚点定位与路由的区别:原生锚点 (如 <a href=“#top”>) 会滚动到页面对应 id 元素,而路由会通过 hashchange 阻止默认行为,实现组件切换;

核心特性

  1. URL 变化不触发页面刷新:修改 window.location.hash 时,浏览器仅更新 URL 片段,不会向服务器发送请求;

  2. hash 仅客户端可见:服务器接收请求时,会自动忽略 # 及后面的内容 (仅解析 # 前的路径)

  3. 触发 hashchange 事件:当 hash 变化时 (手动修改 URL、前进 / 后退、代码修改),浏览器会触发该事件,前端可监听并响应;

核心 API 与用法

  1. 读取 / 修改 Hash:通过 window.location.hash 直接操作

    // 读取 hash(含 # 符号)
    console.log(window.location.hash); // 例如 "#/user/123"
    
    // 修改 hash(会新增一条历史记录,支持前进/后退)
    window.location.hash = "/user/123"; // URL 变为 http://a.com/#/user/123
    
    // 清空 hash(两种方式)
    window.location.hash = ""; // URL 变为 http://a.com/#(空 hash)
    window.location.hash = "#"; // 效果同上
    
  2. 监听 Hash 变化:通过 hashchange 事件响应 URL 变化,实现路由跳转逻辑

    window.addEventListener("hashchange", (event) => {
      // event.oldURL:变化前的完整 URL
      // event.newURL:变化后的完整 URL
      console.log("旧 URL:", event.oldURL);
      console.log("新 URL:", event.newURL);
    
      // 解析当前 hash(去除 # 符号)
      const currentPath = window.location.hash.slice(1); // 例如 "/user/123"
      // 执行路由匹配逻辑(如渲染对应组件)
      renderComponent(currentPath);
    });
    
  3. 手动触发 Hash 变化 (兼容处理):部分场景下需手动触发逻辑,可直接修改 hash 或模拟事件

    // 方式1:直接修改 hash(会触发 hashchange)
    window.location.hash = "/about";
    
    // 方式2:兼容旧浏览器(手动触发事件)
    function triggerHashChange(newHash) {
      window.location.hash = newHash;
      // 若未触发事件(极端情况),手动派发
      const event = new Event("hashchange");
      window.dispatchEvent(event);
    }
    

History API 详解

History APIHTML5 新增的浏览器 API,允许开发者直接操作浏览器的历史记录栈,提供更灵活的 URL 管理 (无 # 符号)

核心特性

  1. URL# 符号:URL 与传统多页应用一致 (如 http://a.com/user),更美观、符合用户习惯;

  2. 操作历史栈:支持新增、替换、删除历史记录 (pushState、replaceState、go 等方法)

  3. state 数据存储:可在历史记录中关联任意类型数据 (无需编码到 URL)

  4. 需服务器配置:刷新页面时,浏览器会向服务器请求完整 URL,若服务器未配置路由转发,会返回 404

核心 API 与用法

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

    1. 功能:向历史栈 新增一条记录 (不刷新页面)
    2. 参数:
      • state:任意类型数据 (如路由状态、用户信息),会存储在历史记录中,可通过 history.state 读取;
      • title:页面标题 (目前多数浏览器忽略该参数,建议传空字符串)
      • url:新 URL (相对路径或绝对路径,需与当前页面同源,否则报错)
    3. 示例:
      // 新增历史记录,URL 变为 http://a.com/user/123
      history.pushState(
        { id: 123, name: "张三" }, // state 数据
        "", // title(忽略)
        "/user/123" // 目标 URL
      );
      
      // 读取当前历史记录的 state 数据
      console.log(history.state); // { id: 123, name: "张三" }
      
  2. history.replaceState(state, title, url)

    1. 功能:替换当前历史记录 (不新增栈,不刷新页面)
    2. 场景:用于不需要后退的跳转 (如表单提交后替换 URL,避免回退到表单页)
    3. 示例:
      // 替换当前历史记录,URL 变为 http://a.com/user/456
      history.replaceState(
        { id: 456, name: "李四" },
        "",
        "/user/456"
      );
      
  3. history.go(n) / history.back() / history.forward()

    1. 功能:操作历史栈的前进 / 后退 (与浏览器按钮功能一致)
    2. 参数:
      • history.go(n):n 为整数,n=1 前进 1 步,n=-1 后退 1 步,n=0 刷新页面;
      • history.back():等价于 go(-1),后退 1 步;
      • history.forward():等价于 go(1),前进 1 步;
    3. 示例:
      history.back(); // 后退
      history.forward(); // 前进
      history.go(-2); // 后退2步
      
  4. 监听历史记录变化:popstate 事件

    1. 注意:pushState / replaceState 不会触发 popstate 事件!
    2. 触发时机:仅当用户点击前进 / 后退按钮,或调用 history.go() / back() / forward() 时触发;
    3. 用途:监听历史栈变化,同步页面状态;
    4. 示例:
      window.addEventListener("popstate", (event) => {
        // event.state:当前历史记录的 state 数据(pushState 时传入的)
        console.log("当前状态:", event.state);
        // 解析当前 URL,执行路由匹配
        const currentPath = window.location.pathname;
        renderComponent(currentPath);
      });
      
  5. 手动触发路由更新 (解决 pushState 不触发 popstate)

    1. 由于 pushState / replaceState 不触发 popstate,需手动封装路由方法,同步页面状态:
    2. 示例:
      // 封装 push 路由
      function pushRoute(url, state = {}) {
        history.pushState(state, "", url);
        // 手动触发页面渲染(模拟 popstate 效果)
        renderComponent(window.location.pathname);
      }
      
      // 封装 replace 路由
      function replaceRoute(url, state = {}) {
        history.replaceState(state, "", url);
        renderComponent(window.location.pathname);
      }
      
      // 使用
      pushRoute("/about", { from: "home" });
      

服务器配置(关键!)

  1. History API 最大的坑:刷新页面会 404,原因如下:

    1. 正常跳转:pushState 仅修改 URL,不向服务器发请求;
    2. 刷新页面:浏览器会向服务器请求当前 URL (如 http://a.com/user/123),而服务器端没有对应的路由配置,返回 404
  2. 解决方案:服务器路由转发,需配置服务器,将所有路由请求转发到 index.html (SPA 入口文件),让前端路由接管;

    1. Nginx 配置示例
      server {
        listen 80;
        server_name a.com;
        root /usr/share/nginx/html; # SPA 打包后的目录
      
        location / {
          try_files $uri $uri/ /index.html; # 关键:所有请求转发到 index.html
        }
      }
      
    2. Apache 配置示例(.htaccess)
      RewriteEngine On
      RewriteBase /
      RewriteCond %{REQUEST_FILENAME} !-f
      RewriteCond %{REQUEST_FILENAME} !-d
      RewriteRule ^(.*)$ index.html [L]
      
    3. 开发环境(Vue/React)
      • Vue CLI:配置 vue.config.jspublicPath/,并使用 history 模式;
      • Create React App:需配合 react-router-domBrowserRouter,并在开发环境使用 rewirecraco 配置转发;

Hash 与 History API 对比总结

特性 Hash API History API
URL 美观度 (带 #) (正常 URL)
兼容性 极佳 (IE8+) 良好 (IE10+,现代浏览器支持)
历史栈操作 仅新增 (修改 hash) 新增 (push)、替换 (replace)、跳转 (go)
状态存储 依赖 hash 字符串 (编码 / 解码繁琐) 支持 state 参数 (任意类型数据)
服务器配置 无需配置 必须配置路由转发 (否则刷新 404)
锚点冲突 可能与页面原生锚点冲突 无冲突 (URL 为正常路径)
长度限制 hash 长度有限制 (不同浏览器略有差异) URL 长度限制 (state 存储无压力)

实战场景示例(简化版前端路由)

Hash 路由实现

class HashRouter {
  constructor() {
    this.routes = {}; // 存储路由映射:path -> 组件渲染函数
    this.init(); // 初始化
  }

  // 初始化:监听 hashchange
  init() {
    window.addEventListener("hashchange", () => this.render());
    // 页面加载时执行一次(处理初始 hash)
    window.addEventListener("load", () => this.render());
  }

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

  // 渲染对应组件
  render() {
    const currentPath = window.location.hash.slice(1) || "/"; // 默认 /
    const callback = this.routes[currentPath] || (() => console.log("404"));
    callback();
  }
}

// 使用示例
const router = new HashRouter();
// 注册路由
router.route("/", () => console.log("渲染首页"));
router.route("/user", () => console.log("渲染用户页"));
router.route("/about", () => console.log("渲染关于页"));

// 跳转路由(直接修改 hash)
window.location.hash = "/user"; // 输出 "渲染用户页"

History 路由实现

class HistoryRouter {
  constructor() {
    this.routes = {};
    this.init();
  }

  init() {
    // 监听 popstate(前进/后退)
    window.addEventListener("popstate", () => this.render());
    // 页面加载时执行一次
    window.addEventListener("load", () => this.render());
    // 拦截 <a> 标签点击(避免刷新页面)
    this.interceptLinks();
  }

  // 拦截 <a> 标签点击,使用 pushState 跳转
  interceptLinks() {
    document.addEventListener("click", (e) => {
      const target = e.target.closest("a");
      if (target && target.getAttribute("href").startsWith("/")) {
        e.preventDefault(); // 阻止默认跳转(刷新页面)
        this.push(target.getAttribute("href"));
      }
    });
  }

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

  // 新增历史记录(push)
  push(path, state = {}) {
    history.pushState(state, "", path);
    this.render();
  }

  // 替换历史记录(replace)
  replace(path, state = {}) {
    history.replaceState(state, "", path);
    this.render();
  }

  // 渲染组件
  render() {
    const currentPath = window.location.pathname;
    const callback = this.routes[currentPath] || (() => console.log("404"));
    callback();
  }
}

// 使用示例
const router = new HistoryRouter();
router.route("/", () => console.log("渲染首页"));
router.route("/user", () => console.log("渲染用户页"));

// 跳转路由
router.push("/user", { id: 123 }); // 输出 "渲染用户页"

常见问题与注意事项

  1. Hash 路由的锚点冲突:

    1. 问题:若页面有原生锚点 (如 <a href=“#top”>),会触发 hashchange 事件,导致路由冲突;
    2. 解决:路由 hash 统一添加前缀 (如 #/),区分原生锚点 (#top),在 hashchange 中过滤非路由 hash
  2. History APIpopstate 触发时机:

    1. 记住:pushState / replaceState 不会触发 popstate,仅前进 / 后退或 go() 会触发;
    2. 解决方案:封装路由方法时手动调用渲染逻辑 (如上述 HistoryRouter 的 push 方法)
  3. 跨域 URL 限制:

    1. History APIpushState / replaceStateurl 参数必须与当前页面同源 (协议、域名、端口一致),否则报错;
    2. Hash API 无此限制 (但跨域后 hashchange 可能无法监听)
  4. 状态数据持久化:

    1. History APIstate 数据仅存储在浏览器内存中,页面刷新后会丢失;
    2. 若需持久化状态 (如用户登录状态),需配合 localStorage / sessionStorage

面试题

什么是 Hash/History API?它们的核心作用是什么?

  1. 两者都是浏览器提供的前端路由核心 API,核心作用是 在不刷新页面的前提下修改 URL、管理浏览历史栈,支撑单页应用 (SPA) 的无刷新跳转;

  2. Hash API:基于 URL 中的 # 及其后续片段 (锚点),通过修改 window.location.hash 和监听 hashchange 事件实现路由;

  3. History APIHTML5 新增 API,通过 window.history 对象操作浏览器历史栈 (如 pushState、replaceState),实现无 # 的标准 URL 路由;

Hash 变化时,浏览器为什么不会刷新页面?

  1. HashURL 中的「片段标识符」,其设计初衷是用于页面内锚点定位 (如滚动到 id=“top” 的元素)

  2. 浏览器的核心机制是:

    1. 仅当 URL「协议、域名、端口、路径」 发生变化时,才会向服务器发送请求刷新页面;
    2. Hash 变化属于 URL「片段部分变化」,不会触发浏览器的网络请求,仅在客户端更新 URL 和历史栈,因此不会刷新页面;

Hash API 的核心原理是什么?如何监听 Hash 变化?

  1. 核心原理:

    1. 修改 window.location.hash 时,浏览器会在历史栈中新增一条记录 (不发请求)
    2. Hash 变化 (手动改 URL、前进 / 后退、代码修改) 会触发 hashchange 事件;
    3. 前端通过监听 hashchange 事件,解析当前 hash 路径,实现组件切换;
  2. 监听方式:

    // 监听 hash 变化
    window.addEventListener('hashchange', (e) => {
      const oldHash = e.oldURL.split('#')[1]; // 旧 hash(不含 #)
      const newHash = window.location.hash.slice(1); // 新 hash(去除 #)
      console.log('路由变化:', oldHash, '→', newHash);
      // 执行路由匹配逻辑
    });
    

History API 中 pushState 和 replaceState 的区别?它们会触发 popstate 事件吗?(高频坑点)

  1. 核心区别:

    1. pushState(state, title, url):向历史栈 新增一条记录,后续可通过 「后退」 回到上一条记录;
    2. replaceState(state, title, url):替换当前历史记录,不会新增栈条目,无法通过 「后退」 回到替换前的状态 (适用于表单提交后、权限跳转等无需回退的场景)
  2. 关键坑点:

    1. 两者 都不会触发 popstate 事件!
    2. popstate 仅在以下场景触发:
      • 用户点击浏览器 「前进 / 后退」 按钮;
      • 代码调用 history.back() / history.forward() / history.go(n)
  3. 解决方案:封装路由方法时,手动调用页面渲染逻辑 (模拟 popstate 效果)

    function pushRoute(url, state = {}) {
      history.pushState(state, '', url);
      renderComponent(window.location.pathname); // 手动渲染
    }
    

History API 中的 state 参数有什么作用?刷新页面后还存在吗?

  1. 作用:存储与当前历史记录关联的任意类型数据 (如路由状态、用户信息、跳转来源等),无需编码到 URL 中,更安全且无长度限制,可通过 history.statepopstate 事件的 event.state 读取;

  2. 生命周期:仅存储在浏览器内存中,页面刷新后会丢失 (刷新会重置历史栈的 state);若需持久化状态 (如登录状态),需配合 localStorage / sessionStorage

实际项目中,如何选择 Hash 还是 History API?

  1. 根据项目核心需求决策,优先级:「兼容性」→「URL 美观度」→「服务器权限」;

  2. Hash API 的场景:

    1. 需兼容旧浏览器 (如 IE8/9)
    2. 服务器无配置权限 (如静态页面部署在 GitHub Pages,无法修改服务器配置)
    3. 快速开发,无需额外配置服务器;
  3. History API 的场景:

    1. 追求 URL 美观 (无 #),提升用户体验;
    2. 需要灵活操作历史栈 (如替换记录、存储复杂状态)
    3. 现代 SPA 项目 (Vue/React/Angular),框架路由已封装,配合服务器配置即可 (如 Vue Router 的 history 模式、React Router 的 BrowserRouter)

Hash 路由中,如何避免与页面原生锚点(如 <a href=“#top”>)冲突?

  1. 核心思路:区分 「路由 hash」「原生锚点 hash」,避免 hashchange 事件误触发;

  2. 方案 1:给路由 hash 加统一前缀 (如 #/),原生锚点用无前缀的 #top,在 hashchange 中过滤:

    window.addEventListener('hashchange', () => {
      const hash = window.location.hash.slice(1);
      // 仅处理路由 hash(以 / 开头),忽略原生锚点(如 top)
      if (hash.startsWith('/')) {
        renderComponent(hash);
      } else {
        // 原生锚点逻辑:滚动到对应元素
        const target = document.getElementById(hash);
        target && target.scrollIntoView();
      }
    });
    
  3. 方案 2:使用 history.pushState 模拟锚点滚动 (用「路径参数 / 查询参数」替代 # 锚点,通过 JS 手动控制页面滚动,同时更新 URL 并维护历史栈,可以保留锚点的「跳转 + 历史记录」功能),彻底避免 # 冲突 (适合现代浏览器)

前端路由的「历史栈」是什么?Hash 和 History API 如何操作历史栈?

  1. 「历史栈」 是浏览器维护的一个栈结构,存储用户的浏览记录 (每个记录包含 URL、state 等信息),支持 「先进后出」 操作 (前进 / 后退本质是栈指针移动)

  2. 操作逻辑:

    1. Hash API:每次修改 window.location.hash,浏览器自动向历史栈 新增一条记录 (栈长度 + 1)
    2. History API
      1. pushState:新增记录 (栈长度 + 1)
      2. replaceState:替换当前栈顶记录 (栈长度不变)
      3. back() / forward() / go(n):移动栈指针 (栈长度不变)

History API 中,history.go(n) 的 n 为 0 时会发生什么?与刷新页面有区别吗?

  1. history.go(0) 会 刷新当前页面 (等价于 location.reload() 的默认行为)

  2. 与手动刷新 (F5) 的区别:

    1. 相同点:都会重新加载页面,重置 history.state
    2. 不同点:history.go(0) 属于 「历史栈操作」,不会新增 / 修改栈记录;而手动刷新会在历史栈中新增一条当前页面的记录 (后退时会回到刷新前的状态)
打赏作者
您的打赏是我前进的动力
微信
支付宝
评论

你好👏🏻,我是 ✍🏻   疯狂 codding 中...

粽子

这有关于前端开发的技术文档和你分享。

相信你可以在这里找到对你有用的知识和教程。

了解更多

目录

  1. 1. 核心概念与本质区别
  2. 2. Hash API 详解
    1. 2.1. 核心特性
    2. 2.2. 核心 API 与用法
  3. 3. History API 详解
    1. 3.1. 核心特性
    2. 3.2. 核心 API 与用法
    3. 3.3. 服务器配置(关键!)
  4. 4. Hash 与 History API 对比总结
  5. 5. 实战场景示例(简化版前端路由)
    1. 5.1. Hash 路由实现
    2. 5.2. History 路由实现
  6. 6. 常见问题与注意事项
  7. 7. 面试题
    1. 7.1. 什么是 Hash/History API?它们的核心作用是什么?
    2. 7.2. Hash 变化时,浏览器为什么不会刷新页面?
    3. 7.3. Hash API 的核心原理是什么?如何监听 Hash 变化?
    4. 7.4. History API 中 pushState 和 replaceState 的区别?它们会触发 popstate 事件吗?(高频坑点)
    5. 7.5. History API 中的 state 参数有什么作用?刷新页面后还存在吗?
    6. 7.6. 实际项目中,如何选择 Hash 还是 History API?
    7. 7.7. Hash 路由中,如何避免与页面原生锚点(如 <a href=“#top”>)冲突?
    8. 7.8. 前端路由的「历史栈」是什么?Hash 和 History API 如何操作历史栈?
    9. 7.9. History API 中,history.go(n) 的 n 为 0 时会发生什么?与刷新页面有区别吗?