> For the complete documentation index, see [llms.txt](https://anjia1.gitbook.io/web/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://anjia1.gitbook.io/web/browser/history.md).

# History API

History API 操作浏览器的 session history，通过 History 对象。History 对象的引用可以通过 window 的只读属性 `history` 获取到，即 `window.history`。

[session history](https://developer.mozilla.org/en-US/docs/Web/API/History) 不同于 [browser history](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/history)：

* session history 是指加载当前页面的 tab 或 frame 访问过的页面
* browser history 是指用户访问过的页面的时间记录

## 1. 接口定义

History 接口不继承任何方法，它的定义如下：

```typescript
enum ScrollRestoration { "auto", "manual" };

[Exposed=Window]
interface History {
  readonly attribute unsigned long length;
  attribute ScrollRestoration scrollRestoration; // 允许 Web 应用显式设置默认的滚动恢复行为
  readonly attribute any state;
  undefined go(optional long delta = 0);
  undefined back();
  undefined forward();
  undefined pushState(any data, DOMString unused, optional USVString? url = null);
  undefined replaceState(any data, DOMString unused, optional USVString? url = null);
};
```

## 2. 两个只读属性

1. `history.length` 表示在 history stack 里的 pages 的数量
   * 其初始值是 1，包括当前加载的页面
2. `history.state` 表示 history stack 栈顶的那个 state 对象
   * 其初始值是 `null`，除非开始调用了 `pushState()` 或 `replaceState()`

![](/files/StEOlvCI3sGTG9lhhkIo)

## 3. 前进和后退

可以使用以下三个方法在 session history 中前进和后退：

1. `history.back()`：后退
2. `history.forward()`：前进
3. `history.go(num)`：从 session history 里加载特定页面
   * 位置相对于当前 page，正数表前进、负数表后退
   * 浏览器兼容性：IE 要求是个字符串，而不是整数

```javascript
// 等价写法
history.go(-1) // history.back()
history.go(1)  // history.forward()
history.go(0)  // location.reload()
history.go()   // location.reload()
```

执行这三个方法时，浏览器地址栏里的 URL 会变，同时也会触发 `popstate` 事件。如果越界了，这三个方法不会产生任何效果，也不会报错和抛异常。

![](/files/aAMWGLBpUsy5qqTmLl6r)

```javascript
window.onpopstate = (event) => {
  console.log('====' + src)
  console.log(history)
}
```

这三个方法都是异步的，可以通过监听 `popstate` 事件来判断导航何时完成。

## 4. 维护 history 栈

要操作浏览器 session history 栈，有两个方法：

1. `pushState()`, add a history entry，添加一条 history 条目
2. `replaceState()`, modify the history entry，修改 history 条目
   * 修改当前的 history 条目，将其替换为传入的 `state` 对象和 URL
   * 该方法适用于处理对用户行为的响应

```javascript
history.pushState(state, unused)
history.pushState(state, unused, url)

history.replaceState(state, unused)
history.replaceState(state, unused, url)
```

### 4.1 参数说明

1. 参数 `state`：是一个可序列化的 JavaScript 对象
   * `state` 对象序列化后的大小不能超过 16 MiB，因为 Firefox 会将 `state` 对象保存到用户的磁盘中，以便在用户重启浏览器后可以恢复它们
   * 如果大小超了，方法会抛出异常
   * 如果真的需要大的存储空间，则可以使用 `sessionStorage` 或 `localStorage`
2. 参数 `unused`：因为历史原因不能为空，通常会传一个空字符串
   * 历史上表示页面的标题，目前[除 Safari 之外](https://github.com/whatwg/html/issues/2174)所有的浏览器都忽略了该参数
   * 建议传个空字符串，以向后兼容
3. 参数 `url`：是新 history 条目的 URL
   * 新 URL 必须和当前 URL 同源，否则方法会抛出异常
   * 新 URL 可以是绝对的，也可以是相对的（相对当前 URL）
   * 如果该参数未指定，默认是文档的当前 URL
   * 浏览器只改变地址栏里的 URL，并不会 reload 页面

### 4.2 只替换 URL

调用 `pushState()` 和 `replaceState()` 这两个方法，只会替换浏览器地址栏里的 URL，并不会触发 `popstate` 事件。

eg1. 页面初始化时调用 `init()`

```javascript
function init() {
  const routes = [
    {
      state: {
        page: 'Home Page'
      },
      url: '/home'
    },
    {
      state: {
        page: 'About Page'
      },
      url: '/about'
    }
  ]
  routes.forEach(item => {
    // URL 会变，但不会触发 popstate 事件
    history.pushState(item.state, '', item.url)
  })
  // 地址栏最终显示的 URL 是 routes 的最后一个
  // history.state 也是 routes 的最后一个
}
```

![](/files/bcAZmbxnMEklhvgjW9ZP)

eg2. 响应页面上按钮的 click 事件

```javascript
// 依然是：只变 URL，不触发 popstate 事件
pushBtn.onclick = () => {
  history.pushState({ page: 'Push Test' }, '', '/push-test')
}
replaceBtn.onclick = () => {
  history.replaceState({ page: 'Replace Test' }, '', '/replace-test')
}
```

![](/files/NtcgyWLQwOFy4bN7g0t0)![](/files/yy4DiUiS1OXl2Gwog3Pb)

### 4.3 `pushState()` 的位置

需要注意的是，`pushState()` 是直接在 stack 当前位置的后面插入的。

1. 如果当前位置正是栈顶，那么就直接入栈，此时 length 会增加 1
2. 否则先清空当前位置（不含）上面的所有元素，然后再入栈，此时 length 的长度取决于当前位置

> 将这种入栈逻辑对应到【点击浏览器的前进+后退箭头】+【在相同页签打开当前网页中的超链接】，就会觉得还算符合应用场景。

来看个例子感受下。

在页面执行了 `init()` 方法之后，此时 session history stack 的长度是 3，信息如下：

* 2 `{page: 'About Page'}`, `/about`
* 1 `{page: 'Home Page'}`, `/home`
* 0 `null`

初始的 history 如下图蓝框中标出的，位置在 2-`/about`。

执行了两次 `history.back()` 之后，位置在 0-`null`，如下图绿框中标出的。

此时点击了 `pushState` 按钮，位置 1 的内容变成了 `{page: 'Push Test'}`, `/push-test`，而 history stack 的长度变成了 2。如下图红框中标出的。

![](/files/AohIkkxlLgPlUQ714yoo)

其它入栈场景，可自行测试（比如连续入栈相同的、比如要入栈的和下一个是相同的），结论都如上所述——只和是否在栈顶有关，又或者是一律先清空当前位置（不含）上面的栈元素，再入栈新元素。

### 4.4 `replaceState()`

相比 `pushState()` 方法的逻辑，`replaceState()` 的逻辑就直观了很多：直接替换当前的。

## 5. `popstate` 事件

### 5.1 触发条件

以下两个条件必须同时满足：

1. 触发时机：当用户在 session history 栈里来回导航时触发（即便连续的两条记录的内容是相同的）
2. 触发操作：只能是点击浏览器上的前进/后退的按钮或是[调用相应的三个方法](#3.-qian-jin-he-hou-tui)（stack 未越界）
   * 而执行 `pushState()`, `replaceState()` 方法[并不会触发该事件](#4.1-zhi-ti-huan-url)，虽然它两也会改变 the active history 条目

当 session history 栈里连续的两条记录的内容相同时，来回切换也会触发 `popstate` 事件。比如先连续 push 三条，然后再点击浏览器的回退按钮，此时依然会触发 `popstate` 事件：

![](/files/t0XgHgNgs19h6oQoqEhH)

### 5.2 写法

```javascript
// 可以附加的对象有：window, <body> 元素, <svg> 元素
addEventListener('popstate', (event) => {
    console.log(event.state)
})
onpopstate = (event) => { 
    console.log(event.state)
}
```

`event.state` 是个只读属性，是提供给 `pushState()` 或 `replaceState()` 方法的 `state` 参数的拷贝。

### 5.3 发送时机

在浏览器可能触发 `popstate` 事件的情况下，遵循的步骤大约是：

1. 如果新条目不包含现有的 Document，那么浏览器会先拉取内容并创建其 `Document`，即发送 `DOMContentLoaded` 和 `load` 事件，同时也会进行下面的步骤
2. 处理当前条目
   1. 如果当前条目的标题不是通过 History API 设置的，那么就将其设置为 `document.title` 属性返回的字符串
   2. 如果当前条目有持久的用户状态（persisted user state）要保存，那么在离开它之前，浏览器会把那些信息和当前条目一起存储，比如文档的滚动位置、表单 inputs 的值等其它类似数据
3. 如果新条目和当前条目的 Document 对象不同，则 `document` 属性要指向新条目的
   1. 新条目 `Document` 里的所有表单控件，对于有 `autocomplete` 的会自动完成配置
   2. 如果新条目的文档已经完全加载并准备就绪（即 `readyState` 是 `complete`）并且该文档还不可见，那么就将其变为可见，然后在 `PageTransitionEvent` 的 `persisted` 属性是 `true` 的文档中触发 `pageshow` 事件
4. 把文档的 URL 设置成新条目的 URL
   1. 如果是在启用替换的情况下执行 history 遍历，那么就从 history 中删除目标条目之前的那条
   2. 如果新条目没有持久的用户状态，并且它的 URL 片段不为空，则文档将滚动到该片段
5. 将当前条目设置为新条目
   1. 如果新条目有一起保存的序列化 `state` 信息，那么该信息会被反序列化为 `History.state`，否则，`state` 为 `null`
   2. 发送 `popstate` 事件：如果 `state` 的值变了，那么就会发送 `popstate` 事件
   3. 恢复持久化用户状态信息
   4. 发送 `hashchange` 事件：如果原始条目和新条目共享同一个文档，但它们的 URL 中有不同的片段，则会发送 `hashchange` 事件

![](/files/BizBnzx84lh6WsX175U8)

## 6. 主要参考

* <https://developer.mozilla.org/en-US/docs/Web/API/History>
* <https://developer.mozilla.org/en-US/docs/Web/API/Window/popstate_event>
* <https://html.spec.whatwg.org/multipage/nav-history-apis.html#the-history-interface>
