🗒️History API

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

session history 不同于 browser history

  • session history 是指加载当前页面的 tab 或 frame 访问过的页面

  • browser history 是指用户访问过的页面的时间记录

1. 接口定义

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

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()

3. 前进和后退

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

  1. history.back():后退

  2. history.forward():前进

  3. history.go(num):从 session history 里加载特定页面

    • 位置相对于当前 page,正数表前进、负数表后退

    • 浏览器兼容性:IE 要求是个字符串,而不是整数

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

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

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

    • 该方法适用于处理对用户行为的响应

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 对象保存到用户的磁盘中,以便在用户重启浏览器后可以恢复它们

    • 如果大小超了,方法会抛出异常

    • 如果真的需要大的存储空间,则可以使用 sessionStoragelocalStorage

  2. 参数 unused:因为历史原因不能为空,通常会传一个空字符串

    • 历史上表示页面的标题,目前除 Safari 之外所有的浏览器都忽略了该参数

    • 建议传个空字符串,以向后兼容

  3. 参数 url:是新 history 条目的 URL

    • 新 URL 必须和当前 URL 同源,否则方法会抛出异常

    • 新 URL 可以是绝对的,也可以是相对的(相对当前 URL)

    • 如果该参数未指定,默认是文档的当前 URL

    • 浏览器只改变地址栏里的 URL,并不会 reload 页面

4.2 只替换 URL

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

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

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 的最后一个
}

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

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

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。如下图红框中标出的。

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

4.4 replaceState()

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

5. popstate 事件

5.1 触发条件

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

  1. 触发时机:当用户在 session history 栈里来回导航时触发(即便连续的两条记录的内容是相同的)

  2. 触发操作:只能是点击浏览器上的前进/后退的按钮或是调用相应的三个方法(stack 未越界)

    • 而执行 pushState(), replaceState() 方法并不会触发该事件,虽然它两也会改变 the active history 条目

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

5.2 写法

// 可以附加的对象有: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,即发送 DOMContentLoadedload 事件,同时也会进行下面的步骤

  2. 处理当前条目

    1. 如果当前条目的标题不是通过 History API 设置的,那么就将其设置为 document.title 属性返回的字符串

    2. 如果当前条目有持久的用户状态(persisted user state)要保存,那么在离开它之前,浏览器会把那些信息和当前条目一起存储,比如文档的滚动位置、表单 inputs 的值等其它类似数据

  3. 如果新条目和当前条目的 Document 对象不同,则 document 属性要指向新条目的

    1. 新条目 Document 里的所有表单控件,对于有 autocomplete 的会自动完成配置

    2. 如果新条目的文档已经完全加载并准备就绪(即 readyStatecomplete)并且该文档还不可见,那么就将其变为可见,然后在 PageTransitionEventpersisted 属性是 true 的文档中触发 pageshow 事件

  4. 把文档的 URL 设置成新条目的 URL

    1. 如果是在启用替换的情况下执行 history 遍历,那么就从 history 中删除目标条目之前的那条

    2. 如果新条目没有持久的用户状态,并且它的 URL 片段不为空,则文档将滚动到该片段

  5. 将当前条目设置为新条目

    1. 如果新条目有一起保存的序列化 state 信息,那么该信息会被反序列化为 History.state,否则,statenull

    2. 发送 popstate 事件:如果 state 的值变了,那么就会发送 popstate 事件

    3. 恢复持久化用户状态信息

    4. 发送 hashchange 事件:如果原始条目和新条目共享同一个文档,但它们的 URL 中有不同的片段,则会发送 hashchange 事件

6. 主要参考

Last updated