【问题标题】:How to tell the difference between 'real' and 'virtual' onpopstate events如何区分“真实”和“虚拟”onpopstate 事件
【发布时间】:2012-10-21 12:46:19
【问题描述】:

我正在构建一个在非 JavaScript 后备 (http://biologos.org/resources/find) 之上使用 AJAX 和 pushState/replaceState 的工具。基本上,它是一个返回真实 HTML 链接列表的搜索工具(单击链接会退出工具)。

我正在使用onpopstate,因此用户可以浏览由pushState 创建的查询历史记录。此事件在从真实链接(使用pushState 创建但由实际浏览器导航创建的NOT)返回时触发。我不想让它在这里开火。

所以这是我的问题:我如何区分来自 pushState 历史项目的 onpopstate 事件与来自真实导航的事件?

我想做这样的事情:

window.onpopstate = function(event){
  if(event.realClick) return;

  // otherwise do something

}

我试过onpopstate handler - ajax back button,但没有运气:(

提前致谢!

编辑: 这里的一个问题是不同浏览器处理onpopstate 事件的方式。以下是似乎正在发生的事情:

  • 真实虚拟事件上触发onpopstate
  • 实际上重新运行 javascript(所以设置 loaded=false 实际上会测试为 false)
  • 上面链接中的解决方案确实有效!

火狐

  • 仅在虚拟事件上触发onpopstate
  • 实际上是重新运行javascript(所以设置loaded=false实际上会测试为false)
  • 要使链接的解决方案真正起作用,需要在页面加载时将 loaded 设置为 true,这会破坏 Chrome!

Safari

  • 真实虚拟事件上触发onpopstate
  • 似乎在事件之前重新运行 javascript(所以如果之前设置为 true,loaded 将是 true!)

希望我只是错过了一些东西......

【问题讨论】:

    标签: javascript html browser-history html5-history


    【解决方案1】:

    您也许可以使用history.js。它应该为您提供一个在所有主要平台上都表现一致的 API(尽管它可能无法解决这个特定问题;您必须尝试找出答案)。

    但是,在我看来,处理这个(以及其他相关问题)的最佳方法是设计您的应用程序,使这些问题无关紧要。自己跟踪应用程序的状态,而不是完全依赖历史堆栈中的状态对象。

    跟踪您的应用程序当前显示的页面。在变量中跟踪它——与window.location 分开。当导航事件(包括 popstate)到达时,将您已知的current page 与请求的next page 进行比较。首先确定是否确实需要更改页面。如果是这样,则呈现请求的页面,并在必要时调用 pushState(仅调用 pushState 进行“正常”导航——从不响应 popstate 事件)。

    处理 popstate 的相同代码也应该处理您的正常导航。就您的应用程序而言,应该没有区别(除了普通导航包含对 pushState 的调用,而 popstate 驱动的导航则没有)。

    这是代码中的基本思想(请参阅实时示例at jsBin

    // keep track of the current page.
    var currentPage = null;
    
    // This function will be called every time a navigation
    // is requested, whether the navigation request is due to
    // back/forward button, or whether it comes from calling
    // the `goTo` function in response to a user's click... 
    // either way, this function will be called. 
    // 
    // The argument `pathToShow` will indicate the pathname of
    // the page that is being requested. The var `currentPage` 
    // will contain the pathname of the currently visible page.
    // `currentPage` will be `null` if we're coming in from 
    // some other site.
    // 
    // Don't call `_renderPage(path)` directly.  Instead call
    // `goTo(path)` (eg. in response to the user clicking a link
    // in your app).
    //
    function _renderPage(pathToShow) {
      if (currentPage === pathToShow) {
        // if we're already on the proper page, then do nothing.
        // return false to indicate that no actual navigation 
        // happened.
        //
        return false;
      }
    
      // ...
      // your data fetching and page-rendering 
      // logic goes here
      // ...
    
      console.log("renderPage");
      console.log("  prev page  : " + currentPage);
      console.log("  next page  : " + pathToShow);
    
      // be sure to update `currentPage`
      currentPage = pathToShow;
    
      // return true to indicate that a real navigation
      // happened, and should be stored in history stack
      // (eg. via pushState - see `function goTo()` below).
      return true;
    }
    
    // listen for popstate events, so we can handle 
    // fwd/back buttons...
    //
    window.addEventListener('popstate', function(evt) {
      // ask the app to show the requested page
      // this will be a no-op if we're already on the
      // proper page.
      _renderPage(window.location.pathname);
    });
    
    // call this function directly whenever you want to perform
    // a navigation (eg. when the user clicks a link or button).
    // 
    function goTo(path) {
    
      // turn `path` into an absolute path, so it will compare
      // with `window.location.pathname`. (you probably want
      // something a bit more robust here... but this is just 
      // an example).
      //
      var basePath, absPath;
      if (path[0] === '/') {
        absPath = path;
      } else {
        basePath = window.location.pathname.split('/');
        basePath.pop();
        basePath = basePath.join('/');    
        absPath = basePath + '/' + path;
      }
    
      // now show that page, and push it onto the history stack.
      var changedPages = _renderPage(absPath);
      if (changedPages) {
        // if renderPage says that a navigation happened, then
        // store it on the history stack, so the back/fwd buttons
        // will work.
        history.pushState({}, document.title, absPath);
      }
    }
    
    // whenever the javascript is executed (or "re-executed"), 
    // just render whatever page is indicated in the browser's 
    // address-bar at this time.
    //
    _renderPage(window.location.pathname);
    

    如果您查看example on jsBin,您会看到每次应用请求转换到新页面时都会调用_renderPage 函数——无论是由于popstate(例如,back/fwd按钮),或者是由于调用goTo(page)(例如,某种用户操作)。它甚至在页面第一次加载时被调用。

    您的逻辑,在_renderPage 函数中可以使用currentPage 的值来确定“请求来自何处”。如果我们来自外部站点,那么 currentPage 将是 null,否则,它将包含当前可见页面的路径名。

    【讨论】:

    • 这非常接近,我会考虑回答。不同浏览器实现 real 历史状态的方式更加复杂,Safari 做的事情与 Chrome 和 Firefox 非常不同(在我的特定示例中实际上要好得多)。如果有人想在野外看到这个,去here,选择几个标签,滚动到底部并加载另一个页面,然后点击一个结果。现在单击浏览器的返回按钮,注意 Safari 中的不同之处——它恢复了先前的状态,并且 ajax 加载的内容已经到位。
    • 谢谢。但我认为你有点忽略了我的意思。我的意思是你的代码根本不应该关心“真实”和“虚拟”导航之间的区别。相反,根据始终可用的两条信息来处理 所有 导航:[a] 当前状态(我现在在哪个页面上?)和 [b] 下一个状态(请求哪个页面通过触发此导航的事件?)。像处理任何其他导航事件一样处理popstate;并且不依赖于存储在历史状态对象中的信息,所有导航仅基于 URL。做这些事情,浏览器的差异就无关紧要了。
    • @MikeMarcacci,一个有趣的旁白:我确实看到了你在page you linked 上描述的 Safari 行为。奇怪的是,我无法在this test page 上复制这种行为。在该测试中,当从外部站点返回时,Safari 会重新运行 javascript,并触发 popstate 事件——与 Chrome 相同。我怀疑 Safari 正在使用某种启发式方法来逐页决定采用哪种方法。无论如何,我的建议仍然有效。无论 Safari 决定如何运行,我展示的代码都应该可以工作。
    • 我同意你的观点,并且正在使用你建议的策略。实际上,我的问题被 Safari 的独特处理方式误导了,我在尝试其他浏览器之前就注意到了这一点。您的绝对是处理此类事情的正确方法,并且适用于所有情况。感谢您的精彩回答!
    猜你喜欢
    • 1970-01-01
    • 2023-03-22
    • 1970-01-01
    • 2019-06-24
    • 2013-05-27
    • 2015-03-14
    • 1970-01-01
    • 1970-01-01
    • 2012-11-08
    相关资源
    最近更新 更多