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

我想知道是否有一种方式(类似于Gmail)的AngularJS 延迟显示一个新的路线,直到每个模型及其数据已被提取使用其各自的服务。

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

在此之前,旧的页面仍然会继续显示(例如,如果我正在浏览另一个页面,然后决定看到这个项目索引)。

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

请注意,控制器定义包含一个parsing对象,该对象声明了应该可用于控制器构造函数的东西。 这里的phones注入到控制器中,并在resolve属性中定义。

resolve.phones函数负责返回一个promise。 所有的承诺都被收集起来,路由的变更被延迟了,直到所有的承诺都解决了。

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

这是一个适用于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),我们实际上不需要创build自己的。 所以我们可以简化MyCtrl。 解决:

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

$ http()的结果包含数据状态头文件configuration对象,所以我们需要将MyCtrl的主体改为:

 $scope.datasets = datasets.data; 

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

我看到一些人问如何使用angular.controller方法与最小化友好的dependency injection。 因为我刚刚做这个工作,我觉得有必要回来帮忙。 这是我的解决scheme(从原来的问题和米斯科的回答中采用):

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

由于这个代码是从问题/最受欢迎的答案派生出来的,所以它是没有经过testing的,但是如果你已经知道如何制作小型化的angular码,它应该给你正确的方向。 我自己的代码并不需要的一个部分是将“电话”注入到“电话”的parsing函数中,我也没有使用任何“延迟”对象。

我也推荐这个youtubevideohttp://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 .... ] 

该提交是版本1.1.5及以上版本的一部分,公开$resource$promise对象。 包含此提交的ngResource版本允许像这样parsing资源:

$ routeProvider

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

调节器

 app.controller('ResourceCtrl', ['$scope', 'data', function($scope, data) { $scope.data = data; }]); 

这段代码是dependency injection友好的(我甚至使用它结合ngminuglify ),这是一个更优雅的域驱动的解决scheme。

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

注意:如果你正在使用ngmin (如果你不是,你应该),你只需要用DI数组约定来编写parsing函数。

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

下一部分是在模块处于configuration状态时将路由数据注入并将其应用到$ 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: '/' }); }); 

使用此设置testing路由configuration也非常简单:

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

你可以在我最近的(即将到来的)实验中看到它的光彩。 虽然这个方法对我来说工作的很好,但是我真的好奇为什么当$ inject注入的东西是一个承诺对象的时候,为什么不会延迟构造任何 东西 ; 它会使事情soooOOOOOOOOOOOO容易得多。

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

延迟显示路由肯定会导致asynchronous纠结……为什么不简单地跟踪主实体的加载状态并在视图中使用它。 例如,在你的控制器中,你可能在ngResource上同时使用成功和错误callback:

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

我从Misko的代码开始工作,这就是我所做的。 由于$defer已被更改为$timeout所以这是一个更新的解决scheme。 然而,代替$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; } } 

使用AngularJS 1.1.5

使用AngularJS 1.1.5语法在Justen的答案中更新'phones'函数。

原版的:

 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团队和贡献者。 🙂

这也是马克西米利安·霍夫曼的答案。 显然这个提交到1.1.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.. }); 

我喜欢darkporter的想法,因为AngularJS的新开发团队很容易理解和直接工作。

我创build了这个适配,使用2个div,一个用于加载器栏,另一个用于加载数据后显示的实际内容。 error handling将在别处完成。

给$ scope添加一个'ready'标志:

 $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, eg 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进度条文档

我喜欢上面的答案,并从他们身上学到了很多东西,但是在上面的大部分答案中都缺less一些东西。

我被困在一个类似的情况下,我解决了在服务器的第一个请求中获取的一些数据的URL。 如果承诺被rejected我面临的问题是什么。

我正在使用一个自定义的提供程序,用于返回一个Promise ,它是在configuration阶段由$routeProviderresolve的。

我想在这里强调的是什么when这样做的概念。

它看到url栏中的url,然后在被调用的控制器和视图中的块分别被引用到目前为止很好。

可以说我有以下configuration阶段代码。

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

否则在浏览器第一个块的根url上被调用otherwise被调用。

让我们来想象一下我在地址栏中打到rootUrl的场景AuthServicePrivider.auth()函数被调用。

让我们说承诺返回在拒绝状态然后呢?

没有得到任何呈现。

Otherwise block不会被执行,因为它对于没有在configuration块中定义的任何URL,并且对于angularJs config阶段是未知的。

当这个承诺没有解决时,我们将不得不处理被解雇的事件。 失败$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); }); 

国际海事组织这是一个好主意,把事件跟踪代码在运行应用程序块。 这段代码在应用程序的configuration阶段之后运行。

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

这样我们可以在configuration阶段处理诺言失败。

我有一个复杂的多级滑动面板界面,禁用屏幕层。 在禁用屏幕上创build指令,创build点击事件来执行状态

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

正在产生轻弹效果。 history.back(),而不是它工作正常,但它并不总是在我的情况历史。 所以我发现如果我只是在我的禁用屏幕上创build属性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; }); }); 

一个可能的解决scheme可能是在我们使用模型的元素中使用ng-cloak指令

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

我觉得这个花费最less。