如何使用Angular检测浏览器的返回按钮点击事件?

79

有没有可能检测到用户是通过浏览器的后退按钮进入页面的?最好能够使用angular.js来检测这个操作。

我不想使用angular路由。如果用户提交表单并成功将其重定向到服务器,然后通过浏览器的后退按钮返回到表单,也应该能够检测到。

8个回答

121

下面是一个使用Angular的解决方案。

myApp.run(function($rootScope, $route, $location){
   //Bind the `$locationChangeSuccess` event on the rootScope, so that we dont need to 
   //bind in induvidual controllers.

   $rootScope.$on('$locationChangeSuccess', function() {
        $rootScope.actualLocation = $location.path();
    });        

   $rootScope.$watch(function () {return $location.path()}, function (newLocation, oldLocation) {
        if($rootScope.actualLocation === newLocation) {
            alert('Why did you use history back?');
        }
    });
});

我使用一个运行块来启动这个过程。首先我将实际位置存储在$rootScope.actualLocation中,然后我监听$locationChangeSuccess事件,并在发生时使用新值更新actualLocation。
在$rootScope中,我观察位置路径的变化,如果新位置等于previousLocation,那么是因为$locationChangeSuccess没有被触发,意味着用户使用了历史记录返回。

1
谢谢!看起来很不错。明天需要仔细检查一下。特别是我需要检查一下,如果位置是通过“普通”超链接而非使用Angular路由更改的,$rootScope.actualLocation是否也会更新。您认为使用普通超链接也能实现吗? - Thomas Kremmel
4
啊,是的。我以前从未使用过Angular路由,所以看了你的例子后似乎没有普通和非普通(Angular路由类型)的超链接。所以请忘记我的评论。 - Thomas Kremmel
2
$locationProvider.html5Mode(true)仅用于使jsFiddle中的链接工作,您在项目中不需要它。只需记住,链接需要以#/开头,这样angular才能捕获它们(在fiddle中没有)。如果您使用“普通”链接,例如/book,angular将无法捕获它,并且页面将重定向到该URL,这将重置您的应用程序,因为所有脚本都将重新加载,这就是为什么它不起作用的原因。 - Bertrand
19
对于像我一样不理解其工作原理的人:当 URL 以普通方式改变时(而非使用“后退/前进”按钮),$locationChangeSuccess 事件会在 $watch 回调之后触发。但是当 URL 使用“后退/前进”按钮或 history.back() API 改变时,$locationChangeSuccess 事件会在 $watch 回调之前触发。因此,在 $watch 回调触发时,actualLocation 已经等于 newLocation。这只是 Angular 内部工作的一种方式,而这个解决方案只是利用了这种内部行为。 - Ruslan Stelmachenko
6
以上代码无法检测URL查询字符串的更改,例如从 /#/news/?page=2 变为 /#/news/?page=3。为了捕获这些更改,您可以使用 $location.absUrl() 替换 $location.path() (在上面使用它的两个地方)。 - Michael
显示剩余7条评论

12

如果您需要更精确的解决方案(检测向前和向后),我对Bertrand提供的解决方案进行了扩展:

$rootScope.$on('$locationChangeSuccess', function() {
    $rootScope.actualLocation = $location.path();
});


$rootScope.$watch(function () {return $location.path()}, function (newLocation, oldLocation) {

    //true only for onPopState
    if($rootScope.actualLocation === newLocation) {

        var back,
            historyState = $window.history.state;

        back = !!(historyState && historyState.position <= $rootScope.stackPosition);

        if (back) {
            //back button
            $rootScope.stackPosition--;
        } else {
            //forward button
            $rootScope.stackPosition++;
        }

    } else {
        //normal-way change of page (via link click)

        if ($route.current) {

            $window.history.replaceState({
                position: $rootScope.stackPosition
            });

            $rootScope.stackPosition++;

        }

    }

 });

7
对于ui-routing,我正在使用以下代码。
在App.run()中,使用:
 $rootScope.$on("$stateChangeStart", function (event, toState, toParams, fromState, fromParams) 
 {

    if (toState.name === $rootScope.previousState )
       { 
          // u can any 1 or all of below 3 statements
         event.preventDefault();  // to Disable the event
         $state.go('someDefaultState'); //As some popular banking sites are using 
         alert("Back button Clicked");

       }
 else
      $rootScope.previousState= fromState.name;
   });

如果你希望,你也可以在'$stateChangeSuccess'事件中获取'previousState'的值,方法如下:

    $rootScope.$on("$stateChangeSuccess", function (event, toState, toParams, fromState, fromParams) 
   {

        $rootScope.previousStatus = fromState.name;
    });

1
这不会检测到后退按钮的点击 - 它只是检测用户是否在两个状态名称之间切换 - 无论是通过后退按钮还是通过在两个页面之间单击。此外,它不会查看状态的参数。 - Amy B

1

我知道这是一个老问题。不管怎样 :)

Bema的答案看起来很棒。然而,如果你想让它适用于“正常”的URL(没有哈希),你可以比较绝对路径,而不是$location.path()返回的路径:

myApp.run(function($rootScope, $route, $location){
//Bind the `$locationChangeSuccess` event on the rootScope, so that we dont need to 
//bind in induvidual controllers.

    $rootScope.$on('$locationChangeSuccess', function() {
        $rootScope.actualLocation = $location.absUrl();
    });      

    $rootScope.$watch(function () {return $location.absUrl()}, function (newLocation, oldLocation) {
        if($rootScope.actualLocation === newLocation) {
            alert('Why did you use history back?');
        }
    });
});

0

所以,我已经将@Bema的答案转录成TypeScript,它看起来像这样:

namespace MyAwesomeApp {
    // ReSharper disable once Class
    function detectBackButton(
        $rootScope: ng.IRootScopeService,
        $location: ng.ILocationService
    ) {
        let actualLocation: string = '';

        $rootScope.$on('$locationChangeSuccess',
            () => {
                actualLocation = $location.path();
            });

        $rootScope.$watch(() => $location.path(),
            (newLocation: string, oldLocation: string) => {
                if (actualLocation === newLocation) {
                    //$rootScope.$broadcast('onBackButtonPressed', null);
                }
            });
    }

    detectBackButton.$inject = [
        '$rootScope',
        '$location'
    ];

    angular
        .module('app')
        .run(detectBackButton);
}

我们不必在$rootScope服务上创建属性,我们可以让我们的“on location change success”和“on location changed”代码都关闭本地actualLocation变量。从那里开始,您可以像原始代码一样做任何您喜欢的事情。就我而言,我会考虑广播一个事件,以便各个控制器可以执行必要的操作,但如果必须的话,您也可以包括全局操作。

感谢您提供如此好的答案,我希望这能帮助其他TypeScript用户。


0

感谢您的评论。由于我更喜欢使用Angular的方式,我将尝试Bertrand的答案。 - Thomas Kremmel

-2

我对这个问题的解决方案是

app.run(function($rootScope, $location) {
    $rootScope.$on('$locationChangeSuccess', function() {
        if($rootScope.previousLocation == $location.path()) {
            console.log("Back Button Pressed");
        }
        $rootScope.previousLocation = $rootScope.actualLocation;
        $rootScope.actualLocation = $location.path();
    });
});

-7

当按下返回按钮时,Angular 只触发 $routeChangeStart 事件,$locationChangeStart 不会被触发。


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