Resolve not called the second time

2019-01-29 05:20发布

问题:

I have some routes defined like this :

$stateProvider
            .state('app', {
                url: '/',
                abstract: true,
                views: {
                    'menuContent': {
                        templateUrl: 'templates/home.html'
                    }
                }
            })
            .state('app.restricted', {
                url: '/restricted',
                views: {
                    'content': {
                        templateUrl: 'templates/restricted/restricted-dashboard.html',
                        controller: 'RestrictedController as vmRestricted'
                    }
                },
                resolve: {
                    isGranted: 'isGranted'
                }
            })
            .state('app.restricted.pending', {
                url: '/pending',
                views: {
                    'tabsView': {
                        templateUrl: 'templates/restricted/restricted-manage-pending.html',
                        controller: 'RestrictedPendingController as vm'
                    }
                },
                resolve: {
                    isGranted: 'isGranted'
                }
            })
            .state('app.restricted.devices', {
                url: '/devices',
                views: {
                    'tabsView': {
                        templateUrl: 'templates/trusted/restricted-manage-devices.html',
                        controller: 'RestrictedDevicesController as vm'
                    }
                },
                resolve: {
                    isGranted: 'isGranted'
                }
            })
            .state('app.grant', {
                url: '/grant-access',
                views: {
                    'content': {
                        templateUrl: 'templates/grant-access.html',
                        controller: 'GrantAccessController as vm'
                    }
                }
            })
        ;

In these routes I have a restricted area and a grant access page to grant access to the restricted area. When the isGranted resolve provider is rejected I redirect to the app.grant route.

This is the code doing this :

$rootScope.$on(AngularEvents.STATE_CHANGE_ERROR, _onStateChangeError);

function _onStateChangeError(event, toState, toParams, fromState, fromParams, error){

            switch (error) {

                case 'accessRejected':
                    $state.go('app.grant');
                    break;

            }

        }

Here is the code of my isGranted provider :

(function() {

    'use strict';

    angular.module('app')
        .provider('isGranted', isGrantedProvider);

    isGrantedProvider.$inject = [];

    function isGrantedProvider() {

        this.$get = isGranted;

        isGranted.$inject = ['$q', '$log', 'grantService'];

        function isGranted($q, $log, grantService){
            $log.log('isGrantedProvider');

            if (grantService.isGranted()) {
                return $q.when(true);
            } else {
                return $q.reject('accessRejected');
            }
        }

    }

})();

(grantService.isGranted() just returns a boolean value)

The first time I go to the app.restricted route with $state.go('app.restricted') the provider is executed. The route is rejected because the access is not granted and we are redirected to the app.grant route.

In this page, the user can log in and have access to the restricted area. Once the user is logged in we redirect him to the app.restricted.pending route but the resolve is not called and the route is rejected and we are redirected to the app.grant route again, whereas the access was granted.

Why is the resolve not called? Is there a way to force it?

EDIT

I have new information after some testing.

I saw that the resolve is not called the second time only when it is a service:

This resolve is always executed when we enter the state:

state('app.restricted', {
                url: '/restricted',
                views: {
                    'content': {
                        templateUrl: 'templates/restricted/restricted-dashboard.html',
                        controller: 'RestrictedController as vmRestricted'
                    }
                },
                resolve: {
                    isGranted: ['$log', function($log) {
                        $log.log('RESOLVE');
                    }]
                }
            })

But this resolve is only executed once even when I enter again to the state:

state('app.restricted', {
                    url: '/restricted',
                    views: {
                        'content': {
                            templateUrl: 'templates/restricted/restricted-dashboard.html',
                            controller: 'RestrictedController as vmRestricted'
                        }
                    },
                    resolve: {
                        isGranted: 'isGranted'
                    }
                })

angular.module('app')
        .provider('isGranted', isGrantedP);

    isGrantedP.$inject = [];

    function isGrantedP() {

        this.$get = isGranted;

        isGranted.$inject = ['$q', '$log'];

        function isGranted($q, $log){
            $log.log('RESOLVE');
        }

    }

Why isn't this service called each time? Is it because a service is a singleton? How should I proceed?

回答1:

After a lot of investigations and testing I found the solution!

First, let's see why it is not working

As mentioned in the docs (http://angular-ui.github.io/ui-router/site/#/api/ui.router.state.$stateProvider), if the resolve is a string, then it corresponds to a service

factory - {string|function}: If string then it is alias for service. Otherwise if function, it is injected and return value it treated as dependency. If result is a promise, it is resolved before its value is injected into controller.

And as mentioned in the angularjs docs (https://docs.angularjs.org/guide/providers), all services are singletons, meaning that it will be instantiated only once

Note: All services in Angular are singletons. That means that the injector uses each recipe at most once to create the object. The injector then caches the reference for all future needs.

Why is it important?

Because resolves do not call a function inside our service. They just use the return value of the instantiated service. BUT because our service will be instantiated only once, the return value will always be the same! (because our service initialization is only called once)

What can we do?

From my tests I could see that a resolve defined like this:

resolve: {
                    myResolve: ['$log', function($log) {
                        $log.log('My Resolve!');
                    }]
                }

is always executed, so we can write them this way to make it work correctly.

But how can I do if I want to use my service?

The best working solution I found to be able to use my service and have a syntax that looks similar to this one: myResolve: 'myResolveService' is to declare my resolve like this:

resolve: {
                        myResolve: ['myResolveService', function(MyResolveService) {
                            myResolveService.log();
                        }]
                    }

And my service like this:

angular.module('app')
        .factory('myResolve', myResolve);

    myResolve.$inject = ['$log'];

    function myResolve($log) {

        function service(){
            this.log = log;

            function log() {
                $log.log('My resolve!');
            }
        }

        return new service();

    }

This code can also be adapted for resolves that return a promise:

Resolve:

resolve: {
                            myResolve: ['myResolveService', function(MyResolveService) {
                                return myResolveService.check();
                            }]
                        }

Service:

angular.module('app')
        .factory('myResolve', myResolve);

    myResolve.$inject = ['$q', 'myService'];

    function myResolve($q, myService) {

        function service(){

            this.check = check;

            function check() {
                var defer = $q.defer();

                if (myService.check()) {
                    defer.resolve(true);
                } else {
                    defer.reject('rejected');
                }

                return defer.promise;
            }
        }

        return new service();

    }