Firefox 4 中的历史记录 API 更改

这是一篇由 Gecko 开发人员之一 Jonas Sicking 撰写的客座文章。

正如您所知,我们正在准备发布 Firefox 4。并且您
可能知道 Firefox 4 包含 历史记录 API(包括 HTML5 中定义的 pushState() 和 replaceState() 方法)。Safari 和 Chrome 也实现了此 API,但 Firefox 4 存在重要差异,我将在本文中进行说明。

几周前,有人发现了 pushState API 中一个相当大的缺陷。问题是,如果您使用 *state* 参数来调用 pushState() 或 replaceState(),并且用户稍后使用此状态重新加载页面,则在 load 事件触发之前无法访问该状态。这是因为访问该状态的唯一方法是通过 popstate 事件,而该事件直到 load 事件触发后才会触发。

这意味着对于使用 *state* 参数的页面,页面必须在不知道该状态的情况下进行渲染,并且只有在页面完全加载后才能向用户显示正确状态。

请注意,我在这里谈论的“状态”是传递给 pushState()/replaceState() 的 *state* 参数。URL(可以说,它是 pushState()/replaceState() 的更有用的参数)始终可以使用常规 API(如 document.locationwindow.location)进行访问。

为了解决此问题,与当前工作草案相比,我们在实现中进行了两处更改

  • 始终通过 window.history.state 属性公开当前 *state*。这样,页面可以立即访问页面的当前状态,而无需等到第一个 popstate 事件触发。
  • 不要总是在 load 事件之后立即触发 popstate 事件。
    相反,仅在实际的会话历史记录转换期间触发它(即,当用户单击“后退”或“前进”或调用 history.back()/forward()/go() 时)。
    此额外 popstate 事件的全部目的是提供对页面状态的访问。但是,window.history.state 属性使此操作变得多余。我们发现,页面只是发现此事件出乎意料且容易导致错误。

第一个更改应该完全向后兼容,因为它是一个纯粹的附加更改。它不会影响现有代码,这些代码可能不使用此属性。

第二个更改是更大的问题。如果您的代码期望此事件始终触发,则可能会导致问题。缓解此更改风险的另一件事是,Safari 5 似乎误解了此问题上的工作草案,并且除非明确将 *state* 传递给 pushState()/replaceState(),否则不会触发此 popstate。因此,基本上,只要您不使用 *state* 参数,Firefox 的行为就与 Safari 5 相同。

我们还在进行第三个更改

  • 允许在页面加载时触发 popstate

当前的工作草案存在一个有点令人惊讶的限制,即它禁止在页面的 load 事件触发之前触发任何 popstate 事件。如果用户在页面加载过程中(例如,由于图像加载缓慢)单击了一个 pushState 支持的链接,然后按“后退”按钮,则不会触发 popstate 事件。只有在页面的 load 事件触发后,才允许第一个 popstate 触发。我们已删除此限制,并在按下“后退”或“前进”按钮或调用 history.back()/forward()/go() 时始终触发 popstate

我进行了一些测试,到目前为止还没有发现由于这些更改而导致的任何问题。不幸的是,由于如此晚才发现这些问题,这些更改直到 Firefox 4 RC 才会出现在 Firefox 测试版中。有一些 可用的测试版本,您可以立即使用它们进行测试。

关于 Jonas Sicking

Jonas 在网络浏览器方面已经潜心研究了十多年。他于 2000 年开始作为开源贡献者,为新开源的 mozilla 项目做出贡献。2005 年,他全职加入 mozilla,此后一直致力于 DOM 和 Web 平台的其他部分。他现在是 mozilla Web API 项目的技术负责人,也是 W3C IndexedDB 和文件 API 规范的编辑。

更多 Jonas Sicking 撰写的文章…


10 条评论

  1. Christoph Pojer

    太棒了,popstate 在页面加载时触发的事实非常违反直觉,我在我的插件中对其进行了规避。我希望规范能够调整以采用您的更改。

    2011 年 3 月 4 日 14:25

  2. Wahooney

    如果我理解错了,请原谅我(如果我错了,请纠正我),但是更改用户历史记录的功能是不是有点像一个巨大的安全漏洞?一定有无数种方法可以用这种方式来欺骗用户?推送垃圾邮件状态、创建虚假历史记录等。

    2011 年 3 月 4 日 22:38

  3. André Luís

    是的!谢谢。我甚至想过忽略第一个 popstate,但我们如何知道它不是与后退/前进按钮的真实交互?!

    您是否已将此更改建议发送给 what-wg?

    2011 年 3 月 7 日 02:33

  4. André Luís

    @Wahooney,好吧……您已经在自己的网站上拥有用户。这仅适用于“相同来源”的 URL……因此,如果您可以在 wordpress.com 或 blogspot 等托管网站上运行 javascript,则确实存在威胁……但由于所有个人区域都是子域名,因此它们是不同的来源,因此没什么大不了的。

    如果您可以在 paypal.com 中运行 javascript,那么游戏就结束了。;)

    2011 年 3 月 7 日 09:45

  5. radu

    在加载事件后立即触发 popstate 事件对我来说很烦人,我认为在每次按下后退/前进按钮时触发它,或者调用 history.bask()、history>forward 或 history.go() 是一个好主意。

    2011 年 3 月 8 日 05:59

  6. Benjamin Lupton

    完全同意此更改。History.js 的用户可以使用 History.getState() 方法获取当前状态,并且在页面加载时调用 popstate 至少令人恼火——它会导致每个人都编写类似以下的代码
    var first = true; $(‘body’).bind(‘popstate’,function(){if ( first ) { first = false; return; } …

    这简直是愚蠢的。我将在 History.js 的新版本 (v1.6) 中实现这些更改。不过需要注意的是,使用变量作为状态 (history.state) 似乎有点危险,如果他们正在使用状态,那么状态就会改变——对象引用将会改变!在当前正在执行的代码中导致副作用!哦哦……似乎执行 history.getState() 更明智,它将返回当前状态对象的副本——以避免此问题。想法?

    2011 年 3 月 11 日 07:43

  7. John

    开始了。沿着“哦,我不喜欢规范,所以让我以不同的方式实现它,这样您就必须专门为 Firefox 4 进行开发”这条滑坡滚落。
    我同意 API 有缺陷,但更改 API 是一种卑鄙的行为。
    我想我会花时间阻止 Firefox 4.0,而不是支持它。

    2011 年 3 月 31 日 18:07

    1. Benjamin Lupton

      哇,John,放松点。

      规范是草案,每个(我的意思是每个)HTML5 浏览器都以不同的方式实现规范。只要规范仍然是草案,就鼓励对其进行更改以使其变得更好——事实上,Firefox 的此举导致规范发生了变化,因此实际上他们现在正在遵循规范。最新的 WebKit nightly 版本也采用了此更改。

      实现之间始终存在差异,这就是存在 polyfill 以确保兼容性的原因
      https://github.com/balupton/history.js

      因此,世界并没有终结,它正在前进,您可以通过使用 polyfill 来支持所有浏览器。生活是美好的。

      2011 年 3 月 31 日 21:17

  8. Chris

    我认为这是一种更明智的做法,但是这种方法与其他现有实现之间的差异很难适应。

    以前,我检测第一个 onpopstate 调用(在页面加载后立即触发的那个)并将其忽略。然后,所有后续对 onpopstate 的调用都会导致对内容的 ajax 请求。这在 WebKit 中运行良好。

    现在,我无法忽略第一个 onpopstate 调用,因为在 Firefox 中,它不会是页面加载后多余的调用。它将是用户单击“后退”按钮。但是,我不能_不_忽略它,因为那样 WebKit 在页面加载后调用 onpopstate 将导致页面加载两次(一次完整页面加载,一次 ajax 请求)。

    似乎无法区分 WebKit 因页面加载而调用的 onpopstate 调用与由历史记录遍历触发的 onpopstate 调用。您可以检查状态是否为 null,但如果遍历历史记录回到页面的初始状态,它也将为 null。

    我想我的困境源于 WebKit 的方法很愚蠢,但在 Firefox 决定以不同的方式做事之前,我有一个合理的解决方法。我宁愿避免根据用户代理字符串运行不同的代码。这甚至不起作用,因为我听说 WebKit 也将采用这种方式。

    我更喜欢这种方法,但是我如何优雅地适应这两种现有实现呢?

    2011 年 6 月 21 日 07:51

  9. Alan Kesselmann

    你好

    我偶然发现了与前一位评论者 (Chris) 所写完全相同的问题。然后我开始搜索,并开始使用 history.js(我最初放弃了它,因为我想自己解决——真正理解发生了什么),并发现它解决了我的问题。

    现在我感兴趣的是找出为什么 History.js 在两个浏览器中都能工作?我猜是因为使用了 statechange 而不是 popstate……哎呀..与其在这里写,我可能应该去文档中阅读一下:)。

    我想我在这里的评论只是为了说明 History.js 修复了之前的问题:P

    Alan

    2011 年 7 月 14 日 07:40

本文的评论已关闭。