延迟AngularJS路线更改,直到加载模型以防止闪烁


321

我想知道AngularJS是否有一种方法(类似于Gmail)将显示新路线的时间延迟到使用各自服务获取每个模型及其数据之后

例如,如果有一个ProjectsController列出所有项目project_index.html的模板,并且是显示这些项目的模板,则Project.query()在显示新页面之前将完全获取该模板。

在此之前,旧页面仍将继续显示(例如,如果我正在浏览另一个页面,然后决定查看此Project索引)。

Answers:


374

$ routeProvider resolve属性允许延迟路由更改,直到加载数据为止。

首先用这样的resolve属性定义一条路线。

angular.module('phonecat', ['phonecatFilters', 'phonecatServices', 'phonecatDirectives']).
  config(['$routeProvider', function($routeProvider) {
    $routeProvider.
      when('/phones', {
        templateUrl: 'partials/phone-list.html', 
        controller: PhoneListCtrl, 
        resolve: PhoneListCtrl.resolve}).
      when('/phones/:phoneId', {
        templateUrl: 'partials/phone-detail.html', 
        controller: PhoneDetailCtrl, 
        resolve: PhoneDetailCtrl.resolve}).
      otherwise({redirectTo: '/phones'});
}]);

请注意,该resolve属性是在路线上定义的。

function PhoneListCtrl($scope, phones) {
  $scope.phones = phones;
  $scope.orderProp = 'age';
}

PhoneListCtrl.resolve = {
  phones: function(Phone, $q) {
    // see: https://groups.google.com/forum/?fromgroups=#!topic/angular/DGf7yyD4Oc4
    var deferred = $q.defer();
    Phone.query(function(successData) {
            deferred.resolve(successData); 
    }, function(errorData) {
            deferred.reject(); // you could optionally pass error data here
    });
    return deferred.promise;
  },
  delay: function($q, $defer) {
    var delay = $q.defer();
    $defer(delay.resolve, 1000);
    return delay.promise;
  }
}

注意,控制器定义包含一个resolve对象,该对象声明控制器构造函数应使用的东西。此处将phones注入到控制器中,并在resolve属性中对其进行了定义。

resolve.phones功能负责返回承诺。收集了所有的承诺,并且路线更改被延迟到所有的承诺都解决之后。

工作演示:http : //mhevery.github.com/angular-phonecat/app/#/phones 来源:https : //github.com/mhevery/angular-phonecat/commit/ba33d3ec2d01b70eb5d3d531d


10
@MiskoHevery-如果您的控制器在模块内并且被定义为字符串而不是函数,该怎么办?如何像您一样设置resolve属性?
aar0n 2012年

53
angular.controller()类型控制器定义中如何使用它?在这些$routeProvider东西中,我认为您必须使用控制器的字符串名称。
Ben Lesh 2012年

6
使用angular.controller()和最新版本的AngularJS的示例吗?
洛朗

22
@blesh,当您使用时angular.controller(),可以将此函数的结果分配给变量(var MyCtrl = angular.controller(...)),然后进一步处理该变量(MyCtrl.loadData = function(){..})。看看egghead的视频,代码会立即显示在这里:egghead.io/video/0uvAseNXDr0
petrkotek

17
我仍然想要一种不错的方法,而不必将控制器放在全局位置。我不想到处乱扔垃圾。您可以使用常量来执行此操作,但是能够将resolve函数放置在控制器上/之中而不是其他位置会很好。
Erik Honn 2013年

51

这是一个适用于Angular 1.0.2的最小工作示例

模板:

<script type="text/ng-template" id="/editor-tpl.html">
    Editor Template {{datasets}}
</script>

<div ng-view>

</div>

JavaScript:

function MyCtrl($scope, datasets) {    
    $scope.datasets = datasets;
}

MyCtrl.resolve = {
    datasets : function($q, $http) {
        var deferred = $q.defer();

        $http({method: 'GET', url: '/someUrl'})
            .success(function(data) {
                deferred.resolve(data)
            })
            .error(function(data){
                //actually you'd want deffered.reject(data) here
                //but to show what would happen on success..
                deferred.resolve("error value");
            });

        return deferred.promise;
    }
};

var myApp = angular.module('myApp', [], function($routeProvider) {
    $routeProvider.when('/', {
        templateUrl: '/editor-tpl.html',
        controller: MyCtrl,
        resolve: MyCtrl.resolve
    });
});​

http://jsfiddle.net/dTJ9N/3/

精简版:

由于$ http()已经返回了一个Promise(也称为deferred),所以我们实际上不需要创建自己的Promise。因此,我们可以简化MyCtrl。解决:

MyCtrl.resolve = {
    datasets : function($http) {
        return $http({
            method: 'GET', 
            url: 'http://fiddle.jshell.net/'
        });
    }
};

$ http()的结果包含datastatus标头config对象,因此我们需要将MyCtrl的主体更改为:

$scope.datasets = datasets.data;

http://jsfiddle.net/dTJ9N/5/


我正在尝试做这样的事情,但由于未定义注入“数据集”而遇到麻烦。有什么想法吗?
Rob Bygrave

嘿MB21,我想你也许能够帮助我这个问题:stackoverflow.com/questions/14271713/...
winduptoy

有人可以帮助我将此答案转换为app.controller('MyCtrl')格式吗? jsfiddle.net/5usya/1对我不起作用。
user1071182

我得到一个错误:Unknown provider: datasetsProvider <- datasets
chovy

您可以通过以下方式替换数据集来简化答案:function($http) { return $http({method: 'GET', url: '/someUrl'}) .then( function(data){ return data;}, function(reason){return 'error value';} ); }
Morteza Tourani

32

我看到有人问如何使用带有最小化依赖项注入的angular.controller方法来执行此操作。由于我刚开始这项工作,所以我有义务回来并提供帮助。这是我的解决方案(从原始问题和Misko的答案中采用):

angular.module('phonecat', ['phonecatFilters', 'phonecatServices', 'phonecatDirectives']).
  config(['$routeProvider', function($routeProvider) {
    $routeProvider.
      when('/phones', {
        templateUrl: 'partials/phone-list.html', 
        controller: PhoneListCtrl, 
        resolve: { 
            phones: ["Phone", "$q", function(Phone, $q) {
                var deferred = $q.defer();
                Phone.query(function(successData) {
                  deferred.resolve(successData); 
                }, function(errorData) {
                  deferred.reject(); // you could optionally pass error data here
                });
                return deferred.promise;
             ]
            },
            delay: ["$q","$defer", function($q, $defer) {
               var delay = $q.defer();
               $defer(delay.resolve, 1000);
               return delay.promise;
              }
            ]
        },

        }).
      when('/phones/:phoneId', {
        templateUrl: 'partials/phone-detail.html', 
        controller: PhoneDetailCtrl, 
        resolve: PhoneDetailCtrl.resolve}).
      otherwise({redirectTo: '/phones'});
}]);

angular.controller("PhoneListCtrl", [ "$scope", "phones", ($scope, phones) {
  $scope.phones = phones;
  $scope.orderProp = 'age';
}]);

由于该代码是从问题/最受欢迎的答案中衍生而来的,因此未经测试,但是如果您已经了解如何制作缩小友好的角度代码,它应该向正确的方向发送信息。我自己的代码不需要的一部分是将“ Phone”注入“ phones”的resolve函数中,我也根本不使用任何“ delay”对象。

我也推荐这个youtube视频http://www.youtube.com/watch?v=P6KITGRQujQ&list=UUKW92i7iQFuNILqQOUOCrFw&index=4&feature=plcp,这对我有很大帮助

如果您感兴趣,我决定也粘贴我自己的代码(写在coffeescript中),以便您了解我的工作方式。

仅供参考,我预先使用了通用控制器,该控制器可帮助我在几种模型上进行CRUD:

appModule.config ['$routeProvider', ($routeProvider) ->
  genericControllers = ["boards","teachers","classrooms","students"]
  for controllerName in genericControllers
    $routeProvider
      .when "/#{controllerName}/",
        action: 'confirmLogin'
        controller: 'GenericController'
        controllerName: controllerName
        templateUrl: "/static/templates/#{controllerName}.html"
        resolve:
          items : ["$q", "$route", "$http", ($q, $route, $http) ->
             deferred = $q.defer()
             controllerName = $route.current.controllerName
             $http(
               method: "GET"
               url: "/api/#{controllerName}/"
             )
             .success (response) ->
               deferred.resolve(response.payload)
             .error (response) ->
               deferred.reject(response.message)

             return deferred.promise
          ]

  $routeProvider
    .otherwise
      redirectTo: '/'
      action: 'checkStatus'
]

appModule.controller "GenericController", ["$scope", "$route", "$http", "$cookies", "items", ($scope, $route, $http, $cookies, items) ->

  $scope.items = items
      #etc ....
    ]

我可以从您的示例和我的失败尝试中正确推断出,现在无法resolve在Angular的最新版本中引用控制器中的函数吗?所以必须在配置中正确声明它吗?
XML

@XMLilley我很确定是这种情况。我相信,这个示例是从我撰写本文时的1.1.2开始的。我没有看到有关将
解析器

2
太好了,谢谢。有很多关于SO的示例(例如此处的前两个),但是它们都是2012年和2013年初的。这是一种优雅的方法,但似乎已被弃用。现在,最干净的替代方法似乎是编写作为promise对象的单个服务。
XML

谢谢,这对我有用。对于在未定义$defer服务方面遇到错误的其他人,请注意,在AngularJS 1.5.7版中,您想使用它$timeout
racl101 '16

18

此提交是1.1.5及更高版本的一部分,它公开的$promise对象$resource。包含此提交的ngResource版本允许解析如下资源:

$ routeProvider

resolve: {
    data: function(Resource) {
        return Resource.get().$promise;
    }
}

控制者

app.controller('ResourceCtrl', ['$scope', 'data', function($scope, data) {

    $scope.data = data;

}]);

请问哪个版本包含该提交?
XML

最新的不稳定版本(1.1.5)包含此提交。 ajax.googleapis.com/ajax/libs/angularjs/1.1.5/angular.min.js
Maximilian Hoffmann

我喜欢这种不太冗长的方法。从实际的数据对象创建一个promise并直接传递给它,这将是很好的选择,但是它的代码太少了,效果很好。
Sam Barnum

1
资源将如何访问$ routeParams?例如:在GET '/api/1/apps/:appId'->中,App.get({id: $routeParams.appId}).$promise();我不能这样使用
zeronone

2
@zeronone您可以注入$route您的决心和使用$route.current.params。注意,$routeParams仍指向旧路线。
Brice Stacey

16

这个片段是依赖注入友好(我甚至用它在组合ngmin丑化),这是一个更优雅的领域驱动基础的解决方案。

下面的示例注册一个Phone 资源和一个常量 phoneRoutes,其中包含该(电话)域的所有路由信息。在提供的答案中,我不喜欢的是解析逻辑的位置- 模块不应该知道任何信息,也不应该对将资源参数提供给控制器的方式感到困扰。这样,逻辑保持在同一域中。

注意:如果您使用的是ngmin(如果不是,则应使用),则只需要使用DI数组约定编写resolve函数。

angular.module('myApp').factory('Phone',function ($resource) {
  return $resource('/api/phone/:id', {id: '@id'});
}).constant('phoneRoutes', {
    '/phone': {
      templateUrl: 'app/phone/index.tmpl.html',
      controller: 'PhoneIndexController'
    },
    '/phone/create': {
      templateUrl: 'app/phone/edit.tmpl.html',
      controller: 'PhoneEditController',
      resolve: {
        phone: ['$route', 'Phone', function ($route, Phone) {
          return new Phone();
        }]
      }
    },
    '/phone/edit/:id': {
      templateUrl: 'app/phone/edit.tmpl.html',
      controller: 'PhoneEditController',
      resolve: {
        form: ['$route', 'Phone', function ($route, Phone) {
          return Phone.get({ id: $route.current.params.id }).$promise;
        }]
      }
    }
  });

下一部分是在模块处于configure状态时注入路由数据,并将其应用于$ routeProvider

angular.module('myApp').config(function ($routeProvider, 
                                         phoneRoutes, 
                                         /* ... otherRoutes ... */) {

  $routeProvider.when('/', { templateUrl: 'app/main/index.tmpl.html' });

  // Loop through all paths provided by the injected route data.

  angular.forEach(phoneRoutes, function(routeData, path) {
    $routeProvider.when(path, routeData);
  });

  $routeProvider.otherwise({ redirectTo: '/' });

});

使用此设置测试路由配置也非常简单:

describe('phoneRoutes', function() {

  it('should match route configuration', function() {

    module('myApp');

    // Mock the Phone resource
    function PhoneMock() {}
    PhoneMock.get = function() { return {}; };

    module(function($provide) {
      $provide.value('Phone', FormMock);
    });

    inject(function($route, $location, $rootScope, phoneRoutes) {
      angular.forEach(phoneRoutes, function (routeData, path) {

        $location.path(path);
        $rootScope.$digest();

        expect($route.current.templateUrl).toBe(routeData.templateUrl);
        expect($route.current.controller).toBe(routeData.controller);
      });
    });
  });
});

您可以在我最新的(即将进行的)实验中全面了解它。尽管这种方法能正常工作对我来说,我真的不知道为什么$喷油器不误事的建设什么当它检测到的注射什么这是一个承诺的对象; 它将使事情变得容易得多。

编辑:使用过的Angular v1.2(rc2)


2
这个极好的答案似乎更符合“ Angular”哲学(封装等)。我们都应该做出有意识的努力,以防止逻辑像kudzu那样在代码库中蔓延。
zakdances

I really wonder why the $injector isn't delaying construction of anything when it detects injection of anything that is a promise object我猜他们忽略了此功能,因为它可能会鼓励对应用程序的响应性产生负面影响的设计模式。他们认为理想的应用程序是真正异步的应用程序,因此解决方案应该是一个边缘案例。
zakdances

11

延迟显示路线肯定会导致异步纠结...为什么不简单地跟踪主实体的加载状态并在视图中使用它。例如,在您的控制器中,您可以在ngResource上同时使用成功和错误回调:

$scope.httpStatus = 0; // in progress
$scope.projects = $resource.query('/projects', function() {
    $scope.httpStatus = 200;
  }, function(response) {
    $scope.httpStatus = response.status;
  });

然后,在视图中您可以执行任何操作:

<div ng-show="httpStatus == 0">
    Loading
</div>
<div ng-show="httpStatus == 200">
    Real stuff
    <div ng-repeat="project in projects">
         ...
    </div>
</div>
<div ng-show="httpStatus >= 400">
    Error, not found, etc. Could distinguish 4xx not found from 
    5xx server error even.
</div>

6
也许向视图公开HTTP状态是不正确的,不仅仅是处理CSS类和DOM元素属于控制器。我可能会使用相同的想法,但是在isValid()和isLoaded()中使用抽象状态。
jpsimons 2012年

1
这确实不是最好的关注点分离,而且如果您有依赖于特定对象的嵌套控制器,它将崩溃。

就将状态代码公开给视图而言,这非常聪明。您可以将http逻辑粘贴到控制器的作用域属性中,然后绑定到它们。同样,如果您在后台进行多个ajax调用,您还是要这样做。
KingOfHypocrites

如果问题仅是延迟视图,那将是很好的。但是,如果需要延迟控制器的实例化(而不仅仅是视图),则最好使用resolve 。(例如:如果您需要确保已加载JSON,因为控制器在连线之前将其传递给了指令。)在文档中:“路由器将在控制器被解析之前等待它们全部解析或拒绝。实例化 ”。

8

我使用上面Misko的代码进行工作,这就是我所做的。由于$defer已更改为,因此这是最新的解决方案$timeout$timeout但是,替换将等待超时时间(以Misko的代码为单位,为1秒),然后返回数据,希望它能及时得到解决。这样,它会尽快返回。

function PhoneListCtrl($scope, phones) {
  $scope.phones = phones;
  $scope.orderProp = 'age';
}

PhoneListCtrl.resolve = {

  phones: function($q, Phone) {
    var deferred = $q.defer();

    Phone.query(function(phones) {
        deferred.resolve(phones);
    });

    return deferred.promise;
  }
}

7

使用AngularJS 1.1.5

使用AngularJS 1.1.5语法更新Justen答案中的“电话”功能。

原版的:

phones: function($q, Phone) {
    var deferred = $q.defer();

    Phone.query(function(phones) {
        deferred.resolve(phones);
    });

    return deferred.promise;
}

更新:

phones: function(Phone) {
    return Phone.query().$promise;
}

归功于Angular团队和贡献者,这要短得多。:)

这也是马克西米利安·霍夫曼(Maximilian Hoffmann)的答案。显然,该提交使其成为1.1.5。


1
我似乎$promise文档中找不到任何内容。从v2.0 +起,它可能已被删除。
zakdances

它是只在1.2
托马斯

5

您可以使用$ routeProvider resolve属性来延迟路由更改,直到加载数据为止。

angular.module('app', ['ngRoute']).
  config(['$routeProvider', function($routeProvider, EntitiesCtrlResolve, EntityCtrlResolve) {
    $routeProvider.
      when('/entities', {
        templateUrl: 'entities.html', 
        controller: 'EntitiesCtrl', 
        resolve: EntitiesCtrlResolve
      }).
      when('/entity/:entityId', {
        templateUrl: 'entity.html', 
        controller: 'EntityCtrl', 
        resolve: EntityCtrlResolve
      }).
      otherwise({redirectTo: '/entities'});
}]);

请注意,该resolve属性是在路线上定义的。

EntitiesCtrlResolveEntityCtrlResolve恒定在同一文件中定义为对象EntitiesCtrlEntityCtrl控制器。

// EntitiesCtrl.js

angular.module('app').constant('EntitiesCtrlResolve', {
  Entities: function(EntitiesService) {
    return EntitiesService.getAll();
  }
});

angular.module('app').controller('EntitiesCtrl', function(Entities) {
  $scope.entities = Entities;

  // some code..
});

// EntityCtrl.js

angular.module('app').constant('EntityCtrlResolve', {
  Entity: function($route, EntitiesService) {
    return EntitiesService.getById($route.current.params.projectId);
  }
});

angular.module('app').controller('EntityCtrl', function(Entity) {
  $scope.entity = Entity;

  // some code..
});

3

我喜欢darkporter的想法,因为对于AngularJS来说,新开发团队很容易理解并立即工作。

我创建了此改编,它使用2个div,一个用于加载器栏,另一个用于加载数据后显示的实际内容。错误处理将在其他地方进行。

向$ scope添加一个“就绪”标志:

$http({method: 'GET', url: '...'}).
    success(function(data, status, headers, config) {
        $scope.dataForView = data;      
        $scope.ready = true;  // <-- set true after loaded
    })
});

在html视图中:

<div ng-show="!ready">

    <!-- Show loading graphic, e.g. Twitter Boostrap progress bar -->
    <div class="progress progress-striped active">
        <div class="bar" style="width: 100%;"></div>
    </div>

</div>

<div ng-show="ready">

    <!-- Real content goes here and will appear after loading -->

</div>

另请参阅:Boostrap进度栏文档


如果要加载多个数据,会分崩离析。你怎么知道一切都装好了?
toxaq

自2月份我添加此答案以来,事情一直在发展,此页面上还有更多活动。看起来Angular现在比这里建议的有更好的支持来解决此问题。干杯,
reggoodwin

我来晚了一点,但是处理多个数据并不重要。您只需为每个请求使用单独的变量(布尔值:isReadyData1,isReadyData2等),然后设置$ scope.ready = isReadyData1 && isReadyData2 ...; 对我来说效果很好。
GuillaumeA

1

我喜欢上面的答案,并从中学到了很多,但是上面的大多数答案中都缺少某些东西。

我陷入了类似的情况,其中我使用从服务器的第一个请求中获取的一些数据来解析url。我面临的问题是如果诺言兑现的话rejected

我使用的是自定义提供程序,该提供程序用于返回,在配置阶段Promise由解析。resolve$routeProvider

我想在这里强调的是when它的概念。

when到目前为止,它在网址栏中看到了该网址,然后在被调用的控制器和视图中引用了各自的块。

可以说我有以下配置阶段代码。

App.when('/', {
   templateUrl: '/assets/campaigns/index.html',
   controller: 'CampaignListCtr',
   resolve : {
      Auth : function(){
         return AuthServiceProvider.auth('campaign');
      }
   }
})
// Default route
.otherwise({
   redirectTo: '/segments'
});

在浏览器的根URL上,首先运行run块,否则otherwise将被调用。

假设我在地址栏AuthServicePrivider.auth()函数中击中rootUrl的情况被调用。

可以说,返回的Promise处于拒绝状态 吗???

什么都不会渲染。

Otherwise block不会像在config块中未定义且urlJs config阶段不知道的任何url一样执行。

当这个承诺没有解决时,我们将不得不处理被触发的事件。一旦失败$routeChangeErorr就被解雇$rootScope

可以捕获它,如下面的代码所示。

$rootScope.$on('$routeChangeError', function(event, current, previous, rejection){
    // Use params in redirection logic.
    // event is the routeChangeEvent
    // current is the current url
    // previous is the previous url
    $location.path($rootScope.rootPath);
});

IMO通常将事件跟踪代码放在应用程序的运行块中是个好主意。此代码仅在应用程序的配置阶段之后运行。

App.run(['$routeParams', '$rootScope', '$location', function($routeParams, $rootScope, $location){
   $rootScope.rootPath = "my custom path";
   // Event to listen to all the routeChangeErrors raised
   // by the resolve in config part of application
   $rootScope.$on('$routeChangeError', function(event, current, previous, rejection){
       // I am redirecting to rootPath I have set above.
       $location.path($rootScope.rootPath);
   });
}]);

这样,我们可以在配置阶段处理承诺失败。


0

我有一个复杂的多级滑动面板界面,并具有禁用的屏幕层。在禁用屏幕层上创建指令,该指令将创建click事件以执行如下状态

$state.go('account.stream.social.view');

产生轻弹效果。history.back()不能正常工作,但是就我而言,它并不总是恢复历史。因此,我发现,如果我只是在禁用屏幕上创建属性href而不是state.go,那么它就像一种魅力。

<a class="disable-screen" back></a>

指令“后退”

app.directive('back', [ '$rootScope', function($rootScope) {

    return {
        restrict : 'A',
        link : function(scope, element, attrs) {
            element.attr('href', $rootScope.previousState.replace(/\./gi, '/'));
        }
    };

} ]);

app.js我只是保存以前的状态

app.run(function($rootScope, $state) {      

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

        $rootScope.previousState = fromState.name;
        $rootScope.currentState = toState.name;


    });
});

-2

一种可能的解决方案可能是将ng-cloak指令与我们正在使用模型的元素一起使用,例如

<div ng-cloak="">
  Value in  myModel is: {{myModel}}
</div>

我认为这一工作最省力。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.