核心概念与本质区别
-
共同目标:
- 实现 无刷新页面跳转 (SPA 核心需求);
- 支持浏览器前进 / 后退按钮 (维持浏览历史栈);
- 关联 URL 与页面状态 (便于分享、书签收藏);
-
本质区别:
维度 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 详解
Hash 即 URL 中 # 及其后面的部分 (如 #/user/123),原本用于页面内锚点定位,前端路由借其特性实现无刷新跳转;
Hash 变化的本质是浏览器对 历史栈的新增操作:每次修改 hash,浏览器会在历史栈中添加一条新记录,因此支持前进 / 后退;
锚点定位与路由的区别:原生锚点 (如 <a href=“#top”>) 会滚动到页面对应 id 元素,而路由会通过 hashchange 阻止默认行为,实现组件切换;
核心特性
-
URL 变化不触发页面刷新:修改 window.location.hash 时,浏览器仅更新 URL 片段,不会向服务器发送请求;
-
hash 仅客户端可见:服务器接收请求时,会自动忽略 # 及后面的内容 (仅解析 # 前的路径);
-
触发 hashchange 事件:当 hash 变化时 (手动修改 URL、前进 / 后退、代码修改),浏览器会触发该事件,前端可监听并响应;
核心 API 与用法
-
读取 / 修改 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 = "#"; // 效果同上 -
监听 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); }); -
手动触发 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 API 是 HTML5 新增的浏览器 API,允许开发者直接操作浏览器的历史记录栈,提供更灵活的 URL 管理 (无 # 符号);
核心特性
-
URL 无 # 符号:URL 与传统多页应用一致 (如 http://a.com/user),更美观、符合用户习惯;
-
操作历史栈:支持新增、替换、删除历史记录 (pushState、replaceState、go 等方法);
-
state 数据存储:可在历史记录中关联任意类型数据 (无需编码到 URL);
-
需服务器配置:刷新页面时,浏览器会向服务器请求完整 URL,若服务器未配置路由转发,会返回 404;
核心 API 与用法
-
history.pushState(state, title, url)- 功能:向历史栈 新增一条记录 (不刷新页面);
- 参数:
- state:任意类型数据 (如路由状态、用户信息),会存储在历史记录中,可通过 history.state 读取;
- title:页面标题 (目前多数浏览器忽略该参数,建议传空字符串);
- url:新 URL (相对路径或绝对路径,需与当前页面同源,否则报错);
- 示例:
// 新增历史记录,URL 变为 http://a.com/user/123 history.pushState( { id: 123, name: "张三" }, // state 数据 "", // title(忽略) "/user/123" // 目标 URL ); // 读取当前历史记录的 state 数据 console.log(history.state); // { id: 123, name: "张三" }
-
history.replaceState(state, title, url)- 功能:替换当前历史记录 (不新增栈,不刷新页面);
- 场景:用于不需要后退的跳转 (如表单提交后替换 URL,避免回退到表单页);
- 示例:
// 替换当前历史记录,URL 变为 http://a.com/user/456 history.replaceState( { id: 456, name: "李四" }, "", "/user/456" );
-
history.go(n) / history.back() / history.forward()- 功能:操作历史栈的前进 / 后退 (与浏览器按钮功能一致);
- 参数:
- history.go(n):n 为整数,n=1 前进 1 步,n=-1 后退 1 步,n=0 刷新页面;
- history.back():等价于 go(-1),后退 1 步;
- history.forward():等价于 go(1),前进 1 步;
- 示例:
history.back(); // 后退 history.forward(); // 前进 history.go(-2); // 后退2步
-
监听历史记录变化:popstate 事件
- 注意:pushState / replaceState 不会触发 popstate 事件!
- 触发时机:仅当用户点击前进 / 后退按钮,或调用 history.go() / back() / forward() 时触发;
- 用途:监听历史栈变化,同步页面状态;
- 示例:
window.addEventListener("popstate", (event) => { // event.state:当前历史记录的 state 数据(pushState 时传入的) console.log("当前状态:", event.state); // 解析当前 URL,执行路由匹配 const currentPath = window.location.pathname; renderComponent(currentPath); });
-
手动触发路由更新 (解决 pushState 不触发 popstate)
- 由于 pushState / replaceState 不触发 popstate,需手动封装路由方法,同步页面状态:
- 示例:
// 封装 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" });
服务器配置(关键!)
-
History API 最大的坑:刷新页面会 404,原因如下:
- 正常跳转:pushState 仅修改 URL,不向服务器发请求;
- 刷新页面:浏览器会向服务器请求当前 URL (如 http://a.com/user/123),而服务器端没有对应的路由配置,返回 404;
-
解决方案:服务器路由转发,需配置服务器,将所有路由请求转发到 index.html (SPA 入口文件),让前端路由接管;
- Nginx 配置示例
server { listen 80; server_name a.com; root /usr/share/nginx/html; # SPA 打包后的目录 location / { try_files $uri $uri/ /index.html; # 关键:所有请求转发到 index.html } } - Apache 配置示例(.htaccess)
RewriteEngine On RewriteBase / RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d RewriteRule ^(.*)$ index.html [L] - 开发环境(Vue/React)
- Vue CLI:配置 vue.config.js 的 publicPath 为 /,并使用 history 模式;
- Create React App:需配合 react-router-dom 的 BrowserRouter,并在开发环境使用 rewire 或 craco 配置转发;
- Nginx 配置示例
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 }); // 输出 "渲染用户页"
常见问题与注意事项
-
Hash 路由的锚点冲突:
- 问题:若页面有原生锚点 (如 <a href=“#top”>),会触发 hashchange 事件,导致路由冲突;
- 解决:路由 hash 统一添加前缀 (如 #/),区分原生锚点 (#top),在 hashchange 中过滤非路由 hash;
-
History API 的 popstate 触发时机:
- 记住:pushState / replaceState 不会触发 popstate,仅前进 / 后退或 go() 会触发;
- 解决方案:封装路由方法时手动调用渲染逻辑 (如上述 HistoryRouter 的 push 方法);
-
跨域 URL 限制:
- History API 的 pushState / replaceState 的 url 参数必须与当前页面同源 (协议、域名、端口一致),否则报错;
- Hash API 无此限制 (但跨域后 hashchange 可能无法监听);
-
状态数据持久化:
- History API 的 state 数据仅存储在浏览器内存中,页面刷新后会丢失;
- 若需持久化状态 (如用户登录状态),需配合 localStorage / sessionStorage;
面试题
什么是 Hash/History API?它们的核心作用是什么?
-
两者都是浏览器提供的前端路由核心 API,核心作用是 在不刷新页面的前提下修改 URL、管理浏览历史栈,支撑单页应用 (SPA) 的无刷新跳转;
-
Hash API:基于 URL 中的 # 及其后续片段 (锚点),通过修改 window.location.hash 和监听 hashchange 事件实现路由; -
History API:HTML5 新增 API,通过 window.history 对象操作浏览器历史栈 (如 pushState、replaceState),实现无 # 的标准 URL 路由;
Hash 变化时,浏览器为什么不会刷新页面?
-
Hash 是 URL 中的「片段标识符」,其设计初衷是用于页面内锚点定位 (如滚动到 id=“top” 的元素);
-
浏览器的核心机制是:
- 仅当 URL 的 「协议、域名、端口、路径」 发生变化时,才会向服务器发送请求刷新页面;
- 而 Hash 变化属于 URL 的 「片段部分变化」,不会触发浏览器的网络请求,仅在客户端更新 URL 和历史栈,因此不会刷新页面;
Hash API 的核心原理是什么?如何监听 Hash 变化?
-
核心原理:
- 修改 window.location.hash 时,浏览器会在历史栈中新增一条记录 (不发请求);
- Hash 变化 (手动改 URL、前进 / 后退、代码修改) 会触发 hashchange 事件;
- 前端通过监听 hashchange 事件,解析当前 hash 路径,实现组件切换;
-
监听方式:
// 监听 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 事件吗?(高频坑点)
-
核心区别:
- pushState(state, title, url):向历史栈 新增一条记录,后续可通过 「后退」 回到上一条记录;
- replaceState(state, title, url):替换当前历史记录,不会新增栈条目,无法通过 「后退」 回到替换前的状态 (适用于表单提交后、权限跳转等无需回退的场景);
-
关键坑点:
- 两者 都不会触发 popstate 事件!
- popstate 仅在以下场景触发:
- 用户点击浏览器 「前进 / 后退」 按钮;
- 代码调用 history.back() / history.forward() / history.go(n);
-
解决方案:封装路由方法时,手动调用页面渲染逻辑 (模拟 popstate 效果):
function pushRoute(url, state = {}) { history.pushState(state, '', url); renderComponent(window.location.pathname); // 手动渲染 }
History API 中的 state 参数有什么作用?刷新页面后还存在吗?
-
作用:存储与当前历史记录关联的任意类型数据 (如路由状态、用户信息、跳转来源等),无需编码到 URL 中,更安全且无长度限制,可通过 history.state 或 popstate 事件的 event.state 读取;
-
生命周期:仅存储在浏览器内存中,页面刷新后会丢失 (刷新会重置历史栈的 state);若需持久化状态 (如登录状态),需配合 localStorage / sessionStorage;
实际项目中,如何选择 Hash 还是 History API?
-
根据项目核心需求决策,优先级:「兼容性」→「URL 美观度」→「服务器权限」;
-
选 Hash API 的场景:
- 需兼容旧浏览器 (如 IE8/9);
- 服务器无配置权限 (如静态页面部署在 GitHub Pages,无法修改服务器配置);
- 快速开发,无需额外配置服务器;
-
选 History API 的场景:
- 追求 URL 美观 (无 #),提升用户体验;
- 需要灵活操作历史栈 (如替换记录、存储复杂状态);
- 现代 SPA 项目 (Vue/React/Angular),框架路由已封装,配合服务器配置即可 (如 Vue Router 的 history 模式、React Router 的 BrowserRouter);
Hash 路由中,如何避免与页面原生锚点(如 <a href=“#top”>)冲突?
-
核心思路:区分 「路由 hash」 和 「原生锚点 hash」,避免 hashchange 事件误触发;
-
方案 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(); } }); -
方案 2:使用 history.pushState 模拟锚点滚动 (用「路径参数 / 查询参数」替代 # 锚点,通过 JS 手动控制页面滚动,同时更新 URL 并维护历史栈,可以保留锚点的「跳转 + 历史记录」功能),彻底避免 # 冲突 (适合现代浏览器);
前端路由的「历史栈」是什么?Hash 和 History API 如何操作历史栈?
-
「历史栈」 是浏览器维护的一个栈结构,存储用户的浏览记录 (每个记录包含 URL、state 等信息),支持 「先进后出」 操作 (前进 / 后退本质是栈指针移动);
-
操作逻辑:
Hash API:每次修改 window.location.hash,浏览器自动向历史栈 新增一条记录 (栈长度 + 1);History API:- pushState:新增记录 (栈长度 + 1);
- replaceState:替换当前栈顶记录 (栈长度不变);
- back() / forward() / go(n):移动栈指针 (栈长度不变);
History API 中,history.go(n) 的 n 为 0 时会发生什么?与刷新页面有区别吗?
-
history.go(0) 会 刷新当前页面 (等价于 location.reload() 的默认行为);
-
与手动刷新 (F5) 的区别:
- 相同点:都会重新加载页面,重置 history.state;
- 不同点:history.go(0) 属于 「历史栈操作」,不会新增 / 修改栈记录;而手动刷新会在历史栈中新增一条当前页面的记录 (后退时会回到刷新前的状态);
API 篇:localStorage、sessionStorage
上一篇
目录
- 1. 核心概念与本质区别
- 2. Hash API 详解
- 3. History API 详解
- 4. Hash 与 History API 对比总结
- 5. 实战场景示例(简化版前端路由)
- 6. 常见问题与注意事项
- 7. 面试题
- 7.1. 什么是 Hash/History API?它们的核心作用是什么?
- 7.2. Hash 变化时,浏览器为什么不会刷新页面?
- 7.3. Hash API 的核心原理是什么?如何监听 Hash 变化?
- 7.4. History API 中 pushState 和 replaceState 的区别?它们会触发 popstate 事件吗?(高频坑点)
- 7.5. History API 中的 state 参数有什么作用?刷新页面后还存在吗?
- 7.6. 实际项目中,如何选择 Hash 还是 History API?
- 7.7. Hash 路由中,如何避免与页面原生锚点(如 <a href=“#top”>)冲突?
- 7.8. 前端路由的「历史栈」是什么?Hash 和 History API 如何操作历史栈?
- 7.9. History API 中,history.go(n) 的 n 为 0 时会发生什么?与刷新页面有区别吗?