为什么AngularJS与ui-router会不断触发$stateChangeStart事件?

24

我正在尝试阻止所有UI-Router状态更改,直到我验证了用户:

$rootScope.$on('$stateChangeStart', function (event, next, toParams) {
  if (!authenticated) {
    event.preventDefault()
    //following $timeout is emulating a backend $http.get('/auth/') request
    $timeout(function() {
      authenticated = true
      $state.go(next,toParams)
    },1000)
  }
})

在用户进行身份验证之前,我拒绝所有状态更改,但是如果我进入使用otherwise()配置的无效URL,就会出现一个带有消息的无限循环:

Error: [$rootScope:infdig] 10 $digest() iterations reached. Aborting!
Watchers fired in the last 5 iterations: [["fn: $locationWatch; newVal: 7; oldVal: 6"],["fn: $locationWatch; newVal: 8; oldVal: 7"],["fn: $locationWatch; newVal: 9; oldVal: 8"],["fn: $locationWatch; newVal: 10; oldVal: 9"],["fn: $locationWatch; newVal: 11; oldVal: 10"]]

以下是我的SSCCE。用python -m SimpleHTTPServer 7070启动它,然后转到localhost:7070/test.html#/bar,看它在你脸上爆炸。而直接导航至唯一有效的angularjs位置不会爆炸localhost:7070/test.html#/foo

<!doctype html>
  <head>
    <script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.0/jquery.js"></script>
    <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.15/angular.js"></script>
    <script src="//cdnjs.cloudflare.com/ajax/libs/angular-ui-router/0.2.10/angular-ui-router.min.js"></script>
  </head>
  <body ng-app="clientApp">
    <div ui-view="" ></div>

    <script>
      var app = angular.module('clientApp', ['ui.router'])

      var myRouteProvider = [
                '$stateProvider', '$urlRouterProvider',
        function($stateProvider,   $urlRouterProvider) { 
          $urlRouterProvider.otherwise('/foo');
          $stateProvider.state('/foo', {
            url: '/foo',
            template: '<div>In Foo now</div>',
            reloadOnSearch: false
          })
        }]
      app.config(myRouteProvider)

      var authenticated = false
      app.run([
                 '$rootScope', '$log','$state','$timeout',
        function ($rootScope,   $log,  $state,  $timeout) {
          $rootScope.$on('$stateChangeStart', function (event, next, toParams) {
            if (!authenticated) {
              event.preventDefault()
              //following $timeout is emulating a backend $http.get('/auth/') request
              $timeout(function() {
                authenticated = true
                $state.go(next,toParams)
              },1000)
            }
          })
        }
      ])
    </script>
  </body>
</html>

有没有其他方法可以用来实现此身份验证阻止?我确实意识到这个身份验证阻止只是客户端的。在这个例子中,我没有展示服务器端的情况。


这不是你问题的直接解决方案,但你应该查看这篇文章,了解Angular中不同的身份验证技术。其中一个部分展示了如何使用uiRouter和$stateProvider实现。 - Terry
谢谢Terry。实际上,我正在使用那篇文章的一个改编版本进行身份验证,这可能是我的问题所在。我只是为了在StackOverflow上发布它而将我的代码简化到以上内容。 - Ross Rogers
6个回答

48

看起来当你在使用 $urlRouterProvider.otherwise("/foo) 和 $stateChangeStart 的组合时,这是 ui-router 的一个 bug。

问题 - https://github.com/angular-ui/ui-router/issues/600

Frank Wallis 提供了一个不错的解决方法,使用 longer form 的 otherwise 方法并将一个函数作为参数传入:

$urlRouterProvider.otherwise( function($injector, $location) {
            var $state = $injector.get("$state");
            $state.go("app.home");
        });

干得好,弗兰克!


真希望我早一天看到这篇文章!我尝试了Ross的解决方案,但是你的方法起作用了,非常感谢! - cl3m
我相信这是正确的修复方法,基于文档 - Josh McKearin
完美地工作了。正是我所需要的解决方案。谢谢。 - Kxng Kombian

14

误导。这是$urlRouterProvider$stateProvider之间的交互问题。我不应该在otherwise中使用$urlRouterProvider。我应该使用类似于以下内容的东西:

$stateProvider.state("otherwise", {
    url: "*path",
    template: "Invalid Location",
    controller: [
              '$timeout','$state',
      function($timeout,  $state ) {
        $timeout(function() {
          $state.go('/foo')
        },2000)
      }]
});

甚至可以是透明的重定向:

$stateProvider.state("otherwise", {
    url: "*path",
    template: "",
    controller: [
              '$state',
      function($state) {
        $state.go('/foo')
      }]
});

现在大家一起来:

<!doctype html>
  <head>
    <script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.0/jquery.js"></script>
    <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.15/angular.js"></script>
    <script src="//cdnjs.cloudflare.com/ajax/libs/angular-ui-router/0.2.10/angular-ui-router.min.js"></script>
  </head>
  <body ng-app="clientApp">
    <div ui-view="" ></div>

    <script>
      var app = angular.module('clientApp', ['ui.router'])

      var myRouteProvider = [
                '$stateProvider',
        function($stateProvider) { 

          $stateProvider.state('/foo', {
            url: '/foo',
            template: '<div>In Foo now</div>',
            reloadOnSearch: false
          })

          $stateProvider.state("otherwise", {
              url: "*path",
              template: "",
              controller: [
                        '$state',
                function($state) {
                  $state.go('/foo')
                }]
          });
        }]
      app.config(myRouteProvider)

      var authenticated = false
      app.run([
                 '$rootScope', '$log','$state','$timeout',
        function ($rootScope,   $log,  $state,  $timeout) {
          $rootScope.$on('$stateChangeStart', function (event, next, toParams) {
            if (!authenticated) {
              event.preventDefault()
              //following $timeout is emulating a backend $http.get('/auth/') request
              $timeout(function() {
                authenticated = true
                $state.go(next,toParams)
              },1000)
            }
          })
        }
      ])
    </script>
  </body>
</html>

1
我认为你用这个叫做“否则”的状态救了我的命 =) - Sandro Simas
我已经有了一个其他的解决方案。对我来说,修复方法是创建一个名为default的状态,它具有url '',并在控制器中设置$state.go('home')。 - Owen

2
我也遇到过这个问题。以下是解决方法的代码,灵感来自于angular-permission项目。
主要思路是手动在状态中添加一个标志($$finishAuthorize),并通过此标志打破无限循环。另一个需要注意的地方是$state.go{notify: false}选项,并手动广播"$stateChangeSuccess"事件。
$rootScope.$on('$stateChangeStart', function (event, toState, toParams, fromState, fromParams) {
    if (toState.$$finishAuthorize) {
        return;
    }
    if (!authenticated) {
        event.preventDefault();
        toState = angular.extend({'$$finishAuthorize': true}, toState);

        // following $timeout is emulating a backend $http.get('/auth/') request
        $timeout(function() {
            authenticated = true;
            $state.go(toState.name, toParams, {notify: false}).then(function() {
                $rootScope.$broadcast('$stateChangeSuccess', toState, toParams, fromState, fromParams);
            });
        },1000)
    }
);

1
我也遇到了这个问题。结果发现是他们建议的代码在https://github.com/angular-ui/ui-router/wiki/Frequently-Asked-Questions#how-to-make-a-trailing-slash-optional-for-all-routes上使一个尾随斜杠变成了可选项。
$urlRouterProvider.rule(function ($injector, $location) {
  var path = $location.url();

  console.log(path);
  // check to see if the path already has a slash where it should be
  if (path[path.length - 1] === '/' || path.indexOf('/?') > -1) {
    return;
  }

  if (path.indexOf('?') > -1) {
    return path.replace('?', '/?');
  }

  return path + '/';
});

将此更改为:

改为这个

$urlRouterProvider.rule(function ($injector, $location) {
  var path = $location.url();
  // check to see if the path already has a slash where it should be
  if (path[path.length - 1] === '/' || path.indexOf('/?') > -1) {
    return;
  }
  if (path.indexOf('?') > -1) {
    $location.replace().path(path.replace('?', '/?'));
  }
  $location.replace().path(path + '/');
});

不返回新路径而只是替换它不会触发StateChangeStart事件。

0
尝试将您的运行块更改为以下内容:
    app.run([
             '$rootScope', '$log','$state','$interval',
    function ($rootScope,   $log,  $state,  $interval) {
      var authenticated = false;
      $rootScope.$on('$stateChangeStart', function (event, next, toParams) {
        if (!authenticated) {
          event.preventDefault()
          //following $timeout is emulating a backend $http.get('/auth/') request
        }
      })


      var intervalCanceller = $interval(function() {
        //backend call
        if(call succeeds & user authenticated) {
          authenticated = true;
          $interval.cancel(intervalCanceller);
          $state.go(next, toParams);
        }
      }, 3000);
    }
  ])

你试过了吗?当我按照你的建议去做时,它仍然失败了。原始的$timeout甚至在错误出现之前都没有触发,所以我不太确定$interval多次触发函数会有什么帮助。此外,如果$stateChangeStart回调函数立即终止,状态将会改变。我不需要调用$state.go(),因为在函数中什么也不做应该会导致状态转换。 - Ross Rogers
代码块在评论中的格式不好。想把它放在你的原始答案中吗?只需点击上面的“编辑”即可。 - Ross Rogers
@Ross Rogers:不要在stageChange事件处理程序内调用$state.go。这就是它陷入循环的原因。 - Neeraj Kumar Singh
Neeraj,你在上下文中尝试过了吗?我的原始代码甚至在抛出错误之前都没有调用$state.go()函数。如果我完全注释掉$state.go(),它仍然会失败。 - Ross Rogers
看一下这个:http://plnkr.co/edit/Jm5JG9Ydm5qS3GSW2jWR。告诉我它是否解决了你的问题。 - Neeraj Kumar Singh
不幸的是,这个问题的本质与$location/window.location操作有关,因此将其嵌入到plnkr中并不能展示出该问题。如果我将您的解决方案复制粘贴到一个文件中,然后在Web服务器上提供服务(例如Python的SimpleHTTPServer),那么您的代码仍然会失败。 - Ross Rogers

0

我尝试了上述解决方案,但成功的程度不同(正在构建一个Ionic Cordova应用程序)。有一次我设法避免无限循环并且状态会改变,但是我留下了一个空白视图。我添加了{reload:true},它似乎有所帮助。我尝试了{notify:false}{notify:true},但没有帮助。

最终,我使用了大部分来自https://dev59.com/El8d5IYBdhLWcg3wcyBV#26800804的答案。

$rootScope.$on('$stateChangeStart', function (event, toState, toParams, fromState, fromParams) {

  // Do not redirect if going to an error page
  if (toState.name === 'app.error') {
    return;
  }

  // Do not redirect if going to the login page
  if (toState.name === 'app.login') {
    return;
  }

  // Do not redirect if there is a token present in localstorage
  var authData = localstorage.getItem('auth');
  if (authData.token) {
    return;
  }

  // We do not have a token, are not going to the login or error pages, time to redirect!
  event.preventDefault();
  console.debug('No auth credentials in localstorage, redirecting to login page');
  $state.go('engineerApp.home', {}, {reload: true}); // Empty object is params
});

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