【问题标题】:Why does AngularJS with ui-router keep firing the $stateChangeStart event?为什么带有 ui-router 的 AngularJS 不断触发 $stateChangeStart 事件?
【发布时间】:2014-09-23 19:11:13
【问题描述】:

我正在尝试阻止所有 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>

我应该使用其他方法来完成此身份验证阻止吗?我确实意识到这种身份验证阻止只是客户端。在这个例子中我没有展示服务器端的东西。

【问题讨论】:

  • 这不是您问题的直接解决方案,但您应该查看 this article 了解 Angular 中的不同身份验证技术。其中一节展示了如何使用 uiRouter 和 $stateProvider 实现。
  • 谢谢特里。我实际上正在使用该文章的混蛋版本进行身份验证,这可能是我的问题。我只是将我的代码简化为上述代码,以便将其发布在 StackOverflow 上。

标签: angularjs angular-ui-router


【解决方案1】:

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

我最终使用了来自https://stackoverflow.com/a/26800804/409864的大部分答案

$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
});

【讨论】:

    【解决方案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)
        }
    );
    

    【讨论】:

      【解决方案3】:

      我也有这个问题。原来这是他们建议在 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

      【讨论】:

        【解决方案4】:

        当您使用 $urlRouterProvider.otherwise("/foo) 与 $stateChangeStart 的组合时,这似乎是 ui-router 的一个错误。

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

        Frank Wallis 提供了一个很好的解决方法,使用 else 方法的较长形式,该方法将函数作为参数:

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

        弗兰克干得好!

        【讨论】:

        • 我希望早一天找到这篇文章!我试过罗斯的解决方案,但你的解决方案成功了,非常感谢!
        • 我相信这是基于documentation的正确修复。
        • 完美运行。正是我需要的解决方案。谢谢
        【解决方案5】:

        假冒。这是$urlRouterProvider$stateProvider 之间的交互问题。我不应该将$urlRouterProvider 用于我的otherwise。我应该使用类似的东西:

        $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>
        

        【讨论】:

        • 我认为你在这种被称为“否则”的状态下救了我的命 =)
        • 我已经有一个其他的了。对我来说,解决方法是创建一个名为 default 的状态,其 url 为 '',在控制器中设置 $state.go('home')。
        【解决方案6】:

        尝试将您的运行块更改为:

            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(),因为在函数中什么都不做应该会导致状态转换。
        • 代码块在 cmets 中的格式不正确。想把它放在你原来的答案中吗?只需点击上方的edit
        • @Ross Rogers :不要在 stageChange 事件处理程序中调用 $state.go。这就是它进入循环的原因。
        • Neeraj,你在上下文中尝试过吗?在引发错误之前,我的原始代码甚至没有调用 $state.go() 函数。如果我完全注释掉$state.go(),它仍然会失败。
        • 看看这个:plnkr.co/edit/Jm5JG9Ydm5qS3GSW2jWR。告诉我它是否能解决您的问题。
        猜你喜欢
        • 1970-01-01
        • 2016-04-25
        • 2018-06-02
        • 2016-06-05
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2021-07-16
        相关资源
        最近更新 更多