HTML5历史记录API: “popstate”在从另一页返回时未被调用

6

我需要维护一个带有 AJAX 搜索表单的网站(比如,搜索书籍),并且希望搜索结果成为浏览器历史的一部分。

因此,我设置了"popstate"事件处理程序。

window.addEventListener('popstate', function(e) {
    alert("popstate: " + JSON.stringify(e.state));
});

在成功的AJAX请求后调用pushState()

var stateObj = { q: "harry potter" };
window.history.pushState(stateObj, "Result", "/search");

我的点击流程如下:

  1. 加载带有搜索表单的页面 - 普通请求 - 路径为 /
  2. 输入查询字符串并提交表单 - AJAX 请求 - 路径更改为 /search
  3. 点击搜索结果 - 普通请求 - 路径更改为 /books/harry-potter

  1. 当我现在通过点击浏览器的后退按钮返回时,我期望"popstate"事件会被触发,并显示相应的状态对象。但是什么都没有发生。 - 路径更改为 /search

  1. 当我再次点击后退按钮时,我会收到一个弹出窗口,其中包含popstate: null - 路径更改为 /

  2. 然后,当我向前导航后,我会收到popstate: {"q":"harry potter"} - 路径更改为 /search


所以,重要的 popstate 事件(带有查询字符串的那个)只有在向前导航时才会被触发,而在返回时则不会。

这是因为我正在从一个历史记录条目被自动创建而不是通过 pushState 编程方式创建的网站返回吗?但如果是这样,那么 History API 只对完整的单页面应用程序有意义。

在任何浏览器中行为都是相同的。希望你能帮我解决问题 :)


Popstate事件仅在URL更改但仍停留在同一页面时触发。因此,当您从/books/harry-potter/按下后退按钮时,只会加载/search/,并且不会触发popstate事件。因此,解决方法是:在/search/页面加载时,您需要读取URL并提取搜索查询。 - Darryl Huffman
谢谢你的回答,但是你说的“只有在 URL 改变时”是什么意思?URL 一直在改变(从 / 到 /search,从 /search 到 /books 等等)。 - jarvisccc
1
假设您正在/search/{query2}页面,然后按下返回按钮... URL将更改为/search/{query1},因为文档上下文仍然与/search/{query1}相同,所以会触发popstate事件。相反,当您在/books/harry-potter/页面并按下返回按钮时,URL将更改,并且整个文档将被替换为/search/{query2} - 这样就不会触发popstate事件,因为它是不同的文档上下文。如果用户在您的搜索页面上,然后直接转到Google,然后按下返回按钮...该“返回”操作未在您的网页上发生,因此也不会发生popstate事件。 - Darryl Huffman
啊,这一切都与文档上下文有关,谢谢!正如我在问题中提到的那样,历史记录 API 只对单页应用程序有意义。 我在网上找不到任何关于这个(重要)点的信息。相反,德语 Mozilla 文档完全是误导性的:根据它们的说法,当您导航到完全不同的页面(如 Google)并返回时,确实会触发 popstate 事件。 即使在原始的英语文档中,您也可以找到这段文字,但至少被划掉了... https://developer.mozilla.org/en-US/docs/Web/API/History_API#Example_of_pushState()_method - jarvisccc
@DarrylHuffman 但是你的解决方法(从URL中提取查询)不幸的是对我没有太大帮助,因为当在浏览器中前进/后退时,javascript并不能在所有浏览器中可靠地被调用。 - jarvisccc
你尝试过使用onunload吗?https://developer.mozilla.org/en-US/docs/Web/API/WindowEventHandlers/onunload 也许那里有你要找的答案!另一个解决方法是在服务器端提取URL参数并将其输入到页面中,这似乎可以解决问题。希望能找到一个解决方法! - Darryl Huffman
2个回答

8
对于想要更多解释的人,为什么popstate事件只在某些情况下发生,这对我有所帮助:
MDN popstate event page上,它指出:
一个popstate事件会在同一文档的两个历史记录条目之间更改时分派到窗口。
而且
只有通过执行浏览器操作(例如单击后退按钮(或在JavaScript中调用history.back()))在两个历史记录条目之间导航for the same document时才会触发popstate事件。
因此,似乎只有在同一文档内导航到不同的URL时才会调用popstate。 这对于SPA非常有效,但对于其他任何内容都不起作用。
作为额外的说明,HTML5规范中关于会话历史的说明似乎并没有明确说明何时调用popstate事件:

在导航到会话历史条目时,某些情况下将触发popstate事件。

这可能表明不同的浏览器会有不同的处理方式。
我希望能从其他人那里获得更多见解。

1

至少有一种方法可以使其工作,基于 Mozilla 的 使用历史记录 API 中的这个摘录:

... 当页面重新加载时,页面将接收到一个 onload 事件,但没有 popstate 事件。然而,如果你读取 history.state 属性,你将得到一个状态对象,就像一个 popstate 已经触发了一样。

在下面的代码片段中可以找到一个最小的工作示例。

运行代码片段并按照以下步骤查看它的操作:

  1. 点击 "populate" 添加一些文本到显示器中,动态地(基本上是模拟 AJAX)
  2. 点击 "away" 导航到 example.org(不用担心,这发生在 iframe 内部)
  3. 点击浏览器的 "后退" 按钮:显示器恢复到其填充状态
  4. 再次点击浏览器的 "后退" 按钮以清除显示器(初始空状态)

const populate = document.getElementById('populate');
const display = document.getElementById('display');

// connect the button
populate.addEventListener('click', () => {
  const state = 'hit browser back button to clear';
  updateDisplay(state);
  // push state onto history stack
  history.pushState(state, '', '#populated');
});

// listen for popstate event, fired e.g. after using browser back button
window.addEventListener('popstate', (event) => updateDisplay(event.state));

// listen for load event, in case we navigate away and then go back
window.addEventListener('load', () => updateDisplay(history.state), false);

function updateDisplay(state) {
  if (state) {
    display.textContent = state;
  } else {
    display.textContent = '';
  }
}
/* the styling is only cosmetic */

.button {
  text-decoration: underline;
  cursor: pointer;
  color: blue;
}
<div class="button" id="populate">populate</div>
<a class="button" href="https://example.org">away</a>
<div id="display"></div>

此处所述:

state对象可以是任何可序列化的内容。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接