web-dev-qa-db-fra.com

Pourquoi AngularJS avec ui-router continue à déclencher l'événement $ stateChangeStart?

J'essaie de bloquer tous les changements d'état du routeur ui jusqu'à ce que j'aie authentifié l'utilisateur:

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

Je rejette toutes les modifications d'état jusqu'à ce que l'utilisateur soit authentifié, mais si j'utilise une URL non valide utilisant la configuration otherwise(), la boucle est infinie avec le message suivant:

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"]]

Ci-dessous se trouve mon SSCCE . Servez-le avec python -m SimpleHTTPServer 7070 et allez à localhost:7070/test.html#/bar pour le voir exploser dans votre visage. Considérant que naviguer directement vers le seul emplacement angularjs valide n’explose pas 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>

Existe-t-il une autre méthode que je devrais utiliser pour accomplir ce blocage d'authentification? Je me rends bien compte que ce blocage d'authentification est uniquement du côté client. Je ne montre pas le côté serveur des choses dans cet exemple.

24
Ross Rogers

Fake-out Il s'agit d'un problème d'interaction entre $urlRouterProvider et $stateProvider. Je ne devrais pas utiliser $urlRouterProvider pour ma otherwise. Je devrais utiliser quelque chose comme:

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

Ou même une redirection transparente:

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

Au total maintenant:

<!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>
14
Ross Rogers

Cela ressemble à un bogue avec ui-router lorsque vous utilisez la combinaison $ urlRouterProvider.otherwise ("/ foo) avec $ stateChangeStart.

Numéro - https://github.com/angular-ui/ui-router/issues/600

Frank Wallis fournit une solution de contournement de Nice, utilisez la forme plus longue de la méthode sinon qui prend une fonction en tant qu'argument:

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

Beau travail Frank!

48
George S

J'ai aussi eu ce problème. Vous trouverez ci-dessous le code de la solution de contournement, inspiré par angular-permission project. 

Le concept principal consiste à ajouter manuellement un indicateur ($$finishAuthorize) dans l'état et à rompre la boucle infinie par cet indicateur. Un autre point à prendre en compte est l’option {notify: false} de $state.go et la diffusion manuelle de l’événement "$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)
    }
);
2
Jessie

J'ai aussi eu ce problème. Il s’avère que c’est le code qu’ils ont suggéré pour rendre une barre oblique optionnelle à la fin de la page https://github.com/angular-ui/ui-router/wiki/Frequent-Asked-Questions#how-to-make-a- trailing-slash-optional-for-all-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 + '/';
});

changé cela en

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

ne pas retourner le nouveau chemin et juste le remplacer ne déclenche pas un StateChangeStart

1
Tom Wu

J'ai essayé les solutions ci-dessus avec plus ou moins de succès (je construis une application Ionic Cordova). À un moment donné, j'ai réussi à ne pas avoir de boucles infinies et l'état a changé, mais je suis resté avec une vue vierge. J'ai ajouté { reload:true } et cela semble aider. J'ai essayé avec { notify:false } et { notify: true } et cela n'a pas aidé. 

J'ai fini par utiliser la majeure partie de la réponse de: 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
});
0
Eamonn Gahan

Essayez de changer votre bloc d'exécution à ceci:

    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);
    }
  ])
0
Neeraj Kumar Singh