拦截我的 AJAX 应用程序中返回按钮的调用

86

我有一个 AJAX 应用程序。用户点击按钮后,页面的显示会更改。他们单击返回按钮,希望返回到原始状态,但实际上,他们回到了浏览器中的前一页。

如何拦截并重新指定返回按钮事件?我已经研究了像 RSH 这样的库(但我无法让它工作...),我听说使用哈希标签可以在某种程度上帮助,但我搞不懂。

13个回答

127
啊,浏览器的后退按钮。你或许想象“后退”会触发JavaScript事件,然后你可以通过以下方式简单地取消它:
// this does not work
document.onHistoryGo = function() { return false; }

不是这样的。实际上并没有这样的事件。

如果你确实拥有一个Web应用程序(而不仅仅是具有某些Ajax功能的网站),那么采取控制后退按钮(使用URL片段,如你所提到的)是合理的。 Gmail就是这样做的。我说的是当你的Web应用程序全部在一个页面中,全部是自包含的时候。

技术很简单 - 每当用户执行修改内容的操作时,重定向到与您已经在的相同的URL,但使用不同的哈希片段。例如:

window.location.hash = "#deleted_something";
...
window.location.hash = "#did_something_else";
如果你的 Web 应用的整个状态可以哈希化,那么这是使用哈希的好地方。假设你有一组电子邮件,你可以将它们的 ID 和已读/未读状态连接起来并取一个 MD5 哈希值,然后将其用作片段标识符。
这种重定向(仅哈希)不尝试从服务器获取任何内容,但它确实在浏览器的历史列表中插入一个占位符。因此,在上面的示例中,用户点击“后退”按钮,然后在地址栏中显示“#deleted_something”。他们再次点击“后退”按钮,他们仍然停留在你的页面上,但没有哈希了。然后再次点击“后退”按钮,他们就会“实际”返回到他们来自的页面。
现在难点在于让 JavaScript 检测用户何时点击了“后退”按钮(以便您可以还原状态)。你要做的就是观察窗口位置并查看它何时发生更改。通过轮询。(我知道,轮询很糟糕,但目前没有更好的跨浏览器方法)。你无法确定他们是前进还是后退,所以你必须在哈希标识符中进行创意设计。(也许是与序列号相连接的哈希...)
这是代码的主要内容。(这也是 jQuery History 插件的工作原理。)
var hash = window.location.hash;
setInterval(function(){
    if (window.location.hash != hash) {
        hash = window.location.hash;
        alert("User went back or forward to application state represented by " + hash);
    }
}, 100);

2
你应该在你的第一个代码块中添加注释,比如 // 不起作用,这样那些快速浏览答案的人就知道你并不是在提供解决方案。 - matt forsythe

62

为了对一个古老(但广受欢迎)的问题提供最新的答案:

HTML5引入了history.pushState()history.replaceState()方法,允许您分别添加和修改历史记录条目。这些方法与window.onpopstate事件配合使用。

使用history.pushState()会更改在您更改状态后创建的XMLHttpRequest对象中使用的HTTP头的引用者。引用者将是文档的URL,该文档的窗口在创建XMLHttpRequest对象时是this

来源:来自Mozilla开发者网络的操作浏览器历史记录


10

使用jQuery,我已经做出了一个简单的解决方案:

$(window).on('hashchange', function() {
    top.location = '#main';
    // Eventually alert the user describing what happened
});

到目前为止,仅在Google Chrome中进行了测试。

这为我基于AJAX的Web应用程序解决了问题。

这可能有点“hackish” - 但我会称其为优雅的黑客技巧;-)每当您尝试向后导航时,它会在URI中弹出一个哈希部分,这在技术上是它尝试向后导航的内容。

它拦截了单击浏览器按钮和鼠标按钮。并且您也无法通过每秒点击多次来强制它向后移动,这是使用setTimeout或setInterval基于解决方案中会出现的问题。


我假设右键单击返回按钮并一次性返回两步仍然有效,对吗? - Hjulle

9

禁用浏览器返回按钮非常容易,像下面的JQuery代码:

// History API 
if( window.history && window.history.pushState ){

  history.pushState( "nohb", null, "" );
  $(window).on( "popstate", function(event){
    if( !event.originalEvent.state ){
      history.pushState( "nohb", null, "" );
      return;
    }
  });
}

你可以在这里查看它的运行情况和更多示例:dotnsfthecssninja。谢谢!

2
这是此页面上唯一在Chrome v52.0上有效的代码片段。谢谢。 - Ralpharama
这个可以工作。但是用户可以通过右键点击后退按钮回到上一页。 - Vimal
@VimalS 是的!就是这样!你必须进行配置!请阅读我发送的相关页面!历史API中还有更多内容...此外:普通用户知道“右键单击”吗?只是我认为...请根据您的需求进行调整! - Roberval Sena 山本
这应该是被接受的答案。它运行得非常好! - code

9

我非常感谢darkporter的答案中所给出的解释,但我认为可以通过使用"hashchange"事件来进行改进。就像darkporter所解释的那样,你需要确保所有按钮都能更改window.location.hash的值。

  • 一种方法是使用<button>元素,然后将事件附加到它们上面,然后设置window.location.hash = "#!somehashstring";
  • 另一种方法是仅使用链接作为按钮,例如<a href="#!somehashstring">Button 1</a>。当单击这些链接时,哈希自动更新。

我在哈希符号后面加上感叹号是为了满足Google的“哈希马铃薯”范例(阅读更多),如果您想被搜索引擎索引,这是有用的。您的哈希字符串通常将是名称/值对,例如#!color=blue&shape=triangle或列表,例如#!/blue/triangle - 这取决于您的Web应用程序。

然后,您只需要添加此代码片段,它将在哈希值更改时触发(包括单击“后退”按钮时)。似乎不需要轮询循环。

window.addEventListener("hashchange", function(){
    console.log("Hash changed to", window.location.hash);
    // .... Do your thing here...
});

我只在Chrome 36中进行过测试,但根据caniuse.com的数据,这应该可用于IE8+,FF21+,Chrome21+和大多数浏览器,除了Opera Mini。


1
只是一个更新 - 谷歌不再推荐“哈希铃声”方法,因为他们现在能够成功地爬行 https://webmasters.googleblog.com/2015/10/deprecating-our-ajax-crawling-scheme.html - rmirabelle

7

我找到一种方式可以覆盖正常的历史记录行为,并创建独立的后退前进按钮事件,使用HTML5历史API(它不能在IE 9中使用)。这很hacky,但如果您想要拦截后退和前进按钮事件并以任何方式处理它们,则效果很好。这在许多场景下都非常有用,例如,如果您正在显示远程桌面窗口并需要在远程计算机上复制后退和前进按钮点击。

以下代码将覆盖后退和前进按钮行为:

var myHistoryOverride = new HistoryButtonOverride(function()
{
    console.log("Back Button Pressed");
    return true;
},
function()
{
    console.log("Forward Button Pressed");
    return true;
});

如果在这两个回调函数中返回了false,那么你将允许浏览器执行正常的后退/前进操作并离开你的页面。
以下是所需的完整脚本:
function addEvent(el, eventType, handler) {
    if (el.addEventListener) { // DOM Level 2 browsers
       el.addEventListener(eventType, handler, false);
    } else if (el.attachEvent) { // IE <= 8
        el.attachEvent('on' + eventType, handler);
    } else { // ancient browsers
        el['on' + eventType] = handler;
    }
}
function HistoryButtonOverride(BackButtonPressed, ForwardButtonPressed)
{
    var Reset = function ()
    {
        if (history.state == null)
            return;
        if (history.state.customHistoryStage == 1)
            history.forward();
        else if (history.state.customHistoryStage == 3)
            history.back();
    }
    var BuildURLWithHash = function ()
    {
        // The URLs of our 3 history states must have hash strings in them so that back and forward events never cause a page reload.
        return location.origin + location.pathname + location.search + (location.hash && location.hash.length > 1 ? location.hash : "#");
    }
    if (history.state == null)
    {
        // This is the first page load. Inject new history states to help identify back/forward button presses.
        var initialHistoryLength = history.length;
        history.replaceState({ customHistoryStage: 1, initialHistoryLength: initialHistoryLength }, "", BuildURLWithHash());
        history.pushState({ customHistoryStage: 2, initialHistoryLength: initialHistoryLength }, "", BuildURLWithHash());
        history.pushState({ customHistoryStage: 3, initialHistoryLength: initialHistoryLength }, "", BuildURLWithHash());
        history.back();
    }
    else if (history.state.customHistoryStage == 1)
        history.forward();
    else if (history.state.customHistoryStage == 3)
        history.back();

    addEvent(window,"popstate",function ()
    {
        // Called when history navigation occurs.
        if (history.state == null)
            return;
        if (history.state.customHistoryStage == 1)
        {
            if (typeof BackButtonPressed == "function" && BackButtonPressed())
            {
                Reset();
                return;
            }
            if (history.state.initialHistoryLength > 1)
                history.back(); // There is back-history to go to.
            else
                history.forward(); // No back-history to go to, so undo the back operation.
        }
        else if (history.state.customHistoryStage == 3)
        {
            if (typeof ForwardButtonPressed == "function" && ForwardButtonPressed())
            {
                Reset();
                return;
            }
            if (history.length > history.state.initialHistoryLength + 2)
                history.forward(); // There is forward-history to go to.
            else
                history.back(); // No forward-history to go to, so undo the forward operation.
        }
    });
};

这是一个简单的工作原理。当我们的页面加载时,我们创建了三个不同的历史状态(编号为1、2和3),并将浏览器导航到状态2。因为状态2位于中间,下一个历史记录导航事件将把我们置于状态1或3中,从而确定用户按下的方向。就这样,我们拦截了后退或前进按钮事件。然后我们可以根据需要处理它,并返回到状态2,以便我们可以捕获下一个历史记录导航事件。
显然,在使用HistoryButtonOverride脚本时,您需要避免使用history.replaceState和history.pushState方法,否则会破坏它。

4

由于隐私问题,不可能禁用后退按钮或检查用户历史记录,但可以在不更改页面的情况下创建新条目。每当您的AJAX应用程序更改状态时,请使用新的URI片段更新top.location

top.location = "#new-application-state";

这将在浏览器的历史记录栈中创建一个新条目。许多AJAX库已经处理了所有繁琐的细节,如Really Simple History

除了设置top.location之外,我还需要注意哪些无聊的细节? - Zack Burt
由于用户点击返回时不会触发“onload”事件,因此需要某些东西来触发事件。 不同的浏览器也有一些差异。一个好的 AJAX 库将封装所有这些细节,包括 darkporter 示例中的 setTimeout() - Matthew

3

我做类似这样的事情。我使用一个包含先前应用程序状态的数组。

初始化为:

var backstack = [];

然后我监听位置哈希的变化,当它发生变化时,我执行以下操作:

if (backstack.length > 1 && backstack[backstack.length - 2] == newHash) {
    // Back button was probably pressed
    backstack.pop();
} else
    backstack.push(newHash);

这样我就有了一种相对简单的方法来跟踪用户历史记录。现在,如果我想在应用程序中实现一个返回按钮(而不是浏览器的返回按钮),我只需要让它执行以下操作:
window.location.hash = backstack.pop();

2
听起来你正在复制后退按钮的功能。那很有趣,但可能不是 OP 问题的答案。 - Luke

2

在我的情况下,我希望防止后退按钮将用户发送到上一页,而是采取不同的操作。使用hashchange事件,我想出了一个适合我的解决方案。这个脚本假设你正在使用它的页面没有使用哈希,就像我这种情况:

var hashChangedCount = -2;
$(window).on("hashchange", function () {
    hashChangedCount++;
    if (top.location.hash == "#main" && hashChangedCount > 0) {
        console.log("Ah ah ah! You didn't say the magic word!");
    }
    top.location.hash = '#main';
});
top.location.hash = "#";

我遇到的问题是其他答案中的hashchange事件无法触发。根据您的情况,这种方法也可能适用于您。

1
在浏览器中,返回按钮通常会尝试返回到上一个地址。在浏览器JavaScript中,没有本机设备返回按钮按下事件,但是您可以通过更改位置和哈希来欺骗它以改变应用程序状态。
想象一下具有两个视图的简单应用程序:
- 默认视图带有获取按钮 - 结果视图
要实现它,请按照以下示例代码操作:
function goToView (viewName) {
  // before you want to change the view, you have to change window.location.hash.
  // it allows you to go back to the previous view with the back button.
  // so use this function to change your view instead of directly do the job.
  // page parameter is your key to understand what view must be load after this.

  window.location.hash = page
}

function loadView (viewName) {
    // change dom here based on viewName, for example:

    switch (viewName) {
      case 'index':
        document.getElementById('result').style.display = 'none'
        document.getElementById('index').style.display = 'block'
        break
      case 'result':
        document.getElementById('index').style.display = 'none'
        document.getElementById('result').style.display = 'block'
        break
      default:
        document.write('404')
    }
}

window.addEventListener('hashchange', (event) => {
  // oh, the hash is changed! it means we should do our job.
  const viewName = window.location.hash.replace('#', '') || 'index'

  // load that view
  loadView(viewName)
})


// load requested view at start.
if (!window.location.hash) {
  // go to default view if there is no request.
  goToView('index')
} else {
  // load requested view.
  loadView(window.location.hash.replace('#', ''))
}

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