Skip to content

history.pushState / replaceState

history.pushState()history.replaceState() 是 HTML5 History API 的两个核心方法,允许在不刷新页面的情况下修改浏览器地址栏的 URL 和历史记录。这是实现单页应用(SPA)路由的基础技术,让前端路由在不依赖 URL 哈希(#)的情况下也能正常工作。

前置知识

阅读本节前,建议先了解:SSE vs WebSocket 对比

基础概念

传统 URL 变更方式

方式刷新页面URL 格式适用场景
<a href> 点击刷新完整 URL页面导航
location.href刷新完整 URL编程式导航
location.hash不刷新#hash哈希路由
history.pushState不刷新任意 URLHistory 路由
history.replaceState不刷新任意 URL替换当前记录

History API 的优势

  • URL 更干净(无 # 符号)
  • 支持服务端渲染(SSR)和 SEO
  • 完全控制 URL 和状态对象
  • 与浏览器前进/后退按钮兼容

语法与 API

pushState()

pushState() 向浏览器历史栈添加一条新记录:

javascript
// history.pushState(state, title, url)
history.pushState(
  { page: 'about', id: 42 },  // state: 状态对象(可存储任何可序列化数据)
  '关于我们',                    // title: 页面标题(大部分浏览器忽略)
  '/about'                       // url: 新的 URL(必须同源)
);

// 执行后:
// - 地址栏变为 /about
// - 历史栈新增一条记录
// - 点击后退可以回到之前的页面
// - 页面不会刷新

replaceState()

replaceState() 替换当前历史记录(不新增):

javascript
// history.replaceState(state, title, url)
history.replaceState(
  { page: 'settings', tab: 'profile' },
  '设置',
  '/settings/profile'
);

// 执行后:
// - 地址栏变为 /settings/profile
// - 历史栈不变(替换当前记录)
// - 点击后退不会回到替换前的 URL
// - 页面不会刷新

pushState vs replaceState

特性pushStatereplaceState
历史记录新增一条替换当前
后退按钮可回到旧 URL回到更早的 URL
适用场景页面导航URL 规范化、重定向

参数说明

参数类型必填说明
stateany状态对象,存储与 URL 关联的数据
titlestring页面标题(目前浏览器基本忽略)
urlstring新 URL(不传则不改变地址栏)

详细说明

state 对象

state 对象存储与当前 URL 关联的数据,可通过 history.statepopstate 事件的 event.state 访问:

javascript
// 设置状态
history.pushState(
  { page: 'product', id: 123, filter: 'price-asc' },
  '商品详情',
  '/product/123?sort=price-asc'
);

// 读取状态
console.log(history.state);
// { page: 'product', id: 123, filter: 'price-asc' }

state 对象的限制

规则说明
大小限制通常建议不超过 640KB
可序列化必须可被结构化克隆
不含 DOM不能存储 DOM 节点
不含函数不能存储函数
不含 Symbol不能存储 Symbol
javascript
// 可以存储
history.pushState({ id: 1, name: 'test', items: [1, 2, 3] }, '', '/page');

// 不能存储
history.pushState({ el: document.body }, '', '/page');  // 错误
history.pushState({ fn: () => {} }, '', '/page');          // 错误

URL 参数

javascript
// 绝对路径
history.pushState(null, '', '/home');
history.pushState(null, '', '/product/detail?id=123');

// 相对路径(相对于当前 URL)
history.pushState(null, '', 'detail');

// 完整 URL(必须同源)
history.pushState(null, '', 'https://example.com/about');

// 不传 URL(只修改 state,不改变地址栏)
history.pushState({ scrollPos: 500 }, '',);

// 仅修改 hash
history.pushState(null, '', '#section-2');

同源限制

javascript
// 当前页面: https://www.example.com/page

// 允许(同源)
history.pushState(null, '', '/about');
history.pushState(null, '', 'https://www.example.com/about');

// 不允许(跨协议)
history.pushState(null, '', 'http://www.example.com/about');

// 不允许(跨域)
history.pushState(null, '', 'https://other.example.com/about');

// 不允许(跨端口)
history.pushState(null, '', 'https://www.example.com:8080/about');

实战示例

简易 SPA 路由

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>History API 演示</title>
  <style>
    body { font-family: -apple-system, sans-serif; padding: 20px; margin: 0; }

    .nav {
      display: flex; gap: 0; margin-bottom: 20px;
      border-bottom: 2px solid #e0e0e0;
    }

    .nav a {
      padding: 12px 24px; text-decoration: none;
      color: #666; font-weight: 600; font-size: 14px;
      border-bottom: 3px solid transparent;
      margin-bottom: -2px;
    }

    .nav a.active {
      color: #1a73e8;
      border-bottom-color: #1a73e8;
    }

    .nav a:hover { color: #1a73e8; }

    .page {
      max-width: 600px; padding: 20px;
      background: white; border-radius: 12px;
      box-shadow: 0 2px 8px rgba(0,0,0,0.1);
    }

    .page h1 { font-size: 24px; color: #1a1a2e; margin-bottom: 16px; }
    .page p { color: #666; line-height: 1.6; }

    .state-display {
      margin-top: 20px; padding: 16px; background: #f0f0f0;
      border-radius: 8px; font-family: monospace; font-size: 13px;
    }

    .breadcrumb {
      margin-bottom: 16px; font-size: 13px; color: #888;
    }
  </style>
</head>
<body>
  <nav class="nav" id="nav">
    <a href="/home" class="nav-link active" data-page="home">首页</a>
    <a href="/about" class="nav-link" data-page="about">关于</a>
    <a href="/products" class="nav-link" data-page="products">产品</a>
    <a href="/contact" class="nav-link" data-page="contact">联系我们</a>
  </nav>

  <div class="page" id="pageContent"></div>

  <script>
    // 页面内容
    const pages = {
      home: {
        title: '首页',
        content: '<p>这是首页内容。使用 History API 实现无刷新页面切换。</p>',
        state: { page: 'home', timestamp: Date.now() }
      },
      about: {
        title: '关于我们',
        content: '<p>我们是一家专注于前端技术的公司。History API 让 SPA 的 URL 更优雅。</p>',
        state: { page: 'about', section: 'intro' }
      },
      products: {
        title: '产品列表',
        content: '<p>产品一、产品二、产品三。每个产品都有独立的 URL,支持 SEO 和浏览器前进后退。</p>',
        state: { page: 'products', category: 'all' }
      },
      contact: {
        title: '联系我们',
        content: '<p>地址:北京市海淀区<br>邮箱:hello@example.com<br>电话:010-12345678</p>',
        state: { page: 'contact', formSubmitted: false }
      }
    };

    // 路由处理函数
    function navigate(pageName) {
      const page = pages[pageName];
      if (!page) return;

      // 更新页面内容
      document.getElementById('pageContent').innerHTML = `
        <div class="breadcrumb">当前路径: <strong>${location.pathname}</strong></div>
        <h1>${page.title}</h1>
        ${page.content}
        <div class="state-display">
          history.state = ${JSON.stringify(page.state)}
        </div>
      `;

      // 更新导航高亮
      document.querySelectorAll('.nav-link').forEach(link => {
        link.classList.toggle('active', link.dataset.page === pageName);
      });

      // 更新页面标题
      document.title = `${page.title} - History API 演示`;
    }

    // 点击导航链接
    document.querySelectorAll('.nav-link').forEach(link => {
      link.addEventListener('click', (e) => {
        e.preventDefault();
        const pageName = link.dataset.page;
        const page = pages[pageName];

        // 使用 pushState 改变 URL
        history.pushState(page.state, page.title, link.href);
        navigate(pageName);
      });
    });

    // 监听浏览器前进/后退
    window.addEventListener('popstate', (event) => {
      if (event.state) {
        navigate(event.state.page);
      }
    });

    // 初始化
    navigate('home');
    // 初始化历史记录
    history.replaceState(pages.home.state, pages.home.title, '/home');
  </script>
</body>
</html>

注意事项

  1. 必须同源pushStatereplaceState 的 URL 必须与当前页面同源
  2. 不会触发 popstatepushStatereplaceState 不会触发 popstate 事件
  3. 服务器端配合:需要服务器配置将所有路由指向同一个 HTML 文件
  4. title 参数被忽略:目前主流浏览器都不使用 pushState 的 title 参数
  5. state 大小限制:过大的 state 可能导致性能问题

最佳实践

  1. 导航用 pushState:用户点击链接导航时使用
  2. URL 规范化用 replaceState:重定向或清理 URL 参数时使用
  3. state 存储最小数据:只存储路由恢复所需的关键信息
  4. 始终监听 popstate:处理浏览器前进/后退
  5. 初始调用 replaceState:页面加载时用 replaceState 替换初始历史记录

下一节

继续学习:popstate 事件

参考链接