为什么带有ui-router的AngularJS会继续触发$ stateChangeStart事件?

时间:2022-02-21 12:16:12

I'm trying to block all ui-router state changes until I've authenticated the user:

我试图阻止所有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)
  }
})

I reject all state changes until the user has been authenticated, but if I go to an invalid URL that uses the otherwise() configuration, I get an infinite loop with a message:

我拒绝所有状态更改,直到用户通过身份验证,但如果我转到使用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"]]

Below is my SSCCE. Serve it up with python -m SimpleHTTPServer 7070 and go to localhost:7070/test.html#/bar to see it explode in your face. Whereas directly navigating to the only valid angularjs location does not blow up localhost:7070/test.html#/foo:

以下是我的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>

Is there an alternative method I should use to accomplish this authentication blocking? I do realize this authentication blocking is client side only. I'm not showing the server side of things in this example.

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

6 个解决方案

#1


48  

Looks like this is a bug with ui-router when you use the combination of $urlRouterProvider.otherwise("/foo) with the $stateChangeStart.

当你使用$ urlRouterProvider.otherwise(“/ foo)和$ stateChangeStart的组合时,看起来这是ui-router的错误。

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

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

Frank Wallis provides a nice workaround, use the longer form of the otherwise method that takes a function as an argument:

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

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

Nice work Frank!

干得好弗兰克!

#2


14  

Fakeout. This is an interaction issue between $urlRouterProvider and $stateProvider. I shouldn't be using $urlRouterProvider for my otherwise. I should be using something like:

欺骗。这是$ urlRouterProvider和$ stateProvider之间的交互问题。我本不应该使用$ urlRouterProvider。我应该使用类似的东西:

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

Or even a transparent'ish redirect:

甚至是透明的重定向:

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

Altogether now:

现在总共:

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

#3


2  

I also had this issue. Below is the code to workaround, which inspired by angular-permission project.

我也有这个问题。以下是解决方法的代码,其灵感来自angular-permission项目。

The main concept is to add a flag($$finishAuthorize) into state manually, and break the infinite loop by this flag. Another point we need to be aware is the {notify: false} option of $state.go, and broadcast "$stateChangeSuccess" event manually.

主要概念是手动将标志($$ 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)
    }
);

#4


1  

I also had this issue. Turns out it was the code that they suggested to make a trailing slash optional at https://github.com/angular-ui/ui-router/wiki/Frequently-Asked-Questions#how-to-make-a-trailing-slash-optional-for-all-routes

我也有这个问题。事实证明,他们建议在https://github.com/angular-ui/ui-router/wiki/Frequently-Asked-Questions#how-to-make-a-trailing-上设置一个尾随斜杠的代码。砍伐可选换全路由

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

changed this to

改成这个

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

not returning the new path and just replacing it doesn't trigger a StateChangeStart

不返回新路径只是替换它不会触发StateChangeStart

#5


0  

Try changing your run block to this:

尝试将运行块更改为:

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

#6


0  

I tried the above solutions, with varying degrees of success (am building an Ionic cordova application). At one point I managed to not get infinite loops and the state would change but I was left with a blank view. I added { reload:true } and it seems to help. I tried with { notify:false } and { notify: true } and that did not help.

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

I ended up using most of the answer from: https://*.com/a/26800804/409864

我最终使用了大部分答案:https://*.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
});

#1


48  

Looks like this is a bug with ui-router when you use the combination of $urlRouterProvider.otherwise("/foo) with the $stateChangeStart.

当你使用$ urlRouterProvider.otherwise(“/ foo)和$ stateChangeStart的组合时,看起来这是ui-router的错误。

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

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

Frank Wallis provides a nice workaround, use the longer form of the otherwise method that takes a function as an argument:

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

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

Nice work Frank!

干得好弗兰克!

#2


14  

Fakeout. This is an interaction issue between $urlRouterProvider and $stateProvider. I shouldn't be using $urlRouterProvider for my otherwise. I should be using something like:

欺骗。这是$ urlRouterProvider和$ stateProvider之间的交互问题。我本不应该使用$ urlRouterProvider。我应该使用类似的东西:

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

Or even a transparent'ish redirect:

甚至是透明的重定向:

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

Altogether now:

现在总共:

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

#3


2  

I also had this issue. Below is the code to workaround, which inspired by angular-permission project.

我也有这个问题。以下是解决方法的代码,其灵感来自angular-permission项目。

The main concept is to add a flag($$finishAuthorize) into state manually, and break the infinite loop by this flag. Another point we need to be aware is the {notify: false} option of $state.go, and broadcast "$stateChangeSuccess" event manually.

主要概念是手动将标志($$ 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)
    }
);

#4


1  

I also had this issue. Turns out it was the code that they suggested to make a trailing slash optional at https://github.com/angular-ui/ui-router/wiki/Frequently-Asked-Questions#how-to-make-a-trailing-slash-optional-for-all-routes

我也有这个问题。事实证明,他们建议在https://github.com/angular-ui/ui-router/wiki/Frequently-Asked-Questions#how-to-make-a-trailing-上设置一个尾随斜杠的代码。砍伐可选换全路由

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

changed this to

改成这个

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

not returning the new path and just replacing it doesn't trigger a StateChangeStart

不返回新路径只是替换它不会触发StateChangeStart

#5


0  

Try changing your run block to this:

尝试将运行块更改为:

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

#6


0  

I tried the above solutions, with varying degrees of success (am building an Ionic cordova application). At one point I managed to not get infinite loops and the state would change but I was left with a blank view. I added { reload:true } and it seems to help. I tried with { notify:false } and { notify: true } and that did not help.

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

I ended up using most of the answer from: https://*.com/a/26800804/409864

我最终使用了大部分答案:https://*.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
});