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 | 不刷新 | 任意 URL | History 路由 |
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
| 特性 | pushState | replaceState |
|---|---|---|
| 历史记录 | 新增一条 | 替换当前 |
| 后退按钮 | 可回到旧 URL | 回到更早的 URL |
| 适用场景 | 页面导航 | URL 规范化、重定向 |
参数说明
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
state | any | 是 | 状态对象,存储与 URL 关联的数据 |
title | string | 是 | 页面标题(目前浏览器基本忽略) |
url | string | 否 | 新 URL(不传则不改变地址栏) |
详细说明
state 对象
state 对象存储与当前 URL 关联的数据,可通过 history.state 和 popstate 事件的 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>注意事项
- 必须同源:
pushState和replaceState的 URL 必须与当前页面同源 - 不会触发 popstate:
pushState和replaceState不会触发popstate事件 - 服务器端配合:需要服务器配置将所有路由指向同一个 HTML 文件
- title 参数被忽略:目前主流浏览器都不使用
pushState的 title 参数 - state 大小限制:过大的 state 可能导致性能问题
最佳实践
- 导航用 pushState:用户点击链接导航时使用
- URL 规范化用 replaceState:重定向或清理 URL 参数时使用
- state 存储最小数据:只存储路由恢复所需的关键信息
- 始终监听 popstate:处理浏览器前进/后退
- 初始调用 replaceState:页面加载时用 replaceState 替换初始历史记录
下一节
继续学习:popstate 事件