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

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

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

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

157324 次浏览

routeProvider美元 解决属性允许延迟路由更改,直到数据加载。

首先定义一个具有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属性是在route上定义的。

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函数负责返回一个promise。收集所有的承诺,并延迟路由更改,直到所有的承诺都被解决。

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

延迟显示路由肯定会导致异步纠缠……为什么不简单地跟踪主实体的加载状态并在视图中使用它呢?例如,在你的控制器中,你可能同时使用ngResource上的success和error回调:

$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。然而,替换$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;
}
}

下面是一个适用于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()已经返回了一个承诺(又名deferred),我们实际上不需要创建自己的承诺。我们可以简化MyCtrl。解决:

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

$http()的结果包含数据状态配置对象,因此我们需要将MyCtrl的主体更改为:

$scope.datasets = datasets.data;

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

我喜欢darkporter的想法,因为对于刚接触AngularJS的开发团队来说,理解和工作起来很容易。

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

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

参见:引导进度条文档

我看到一些人问如何使用angular来做到这一点。带有简化友好依赖项注入的控制器方法。因为我刚开始工作,我觉得有必要回来帮忙。以下是我的解决方案(采用了最初的问题和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,它对我帮助很大

如果你感兴趣,我也决定粘贴我自己的代码(写在coffescript),所以你可以看到我是如何得到它的工作。

仅供参考,在此之前,我使用了一个通用控制器,帮助我在几个模型上做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版本允许像这样解析资源:

routeProvider美元

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

控制器

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


$scope.data = data;


}]);

使用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团队和贡献者,这个版本变得更短了。:)

这也是马克西米利安·霍夫曼的答案。显然,这个commit被放到了1.1.5。

这个代码段是依赖注入友好的(我甚至在ngmin糟蹋的组合中使用它),它是一个更优雅的基于领域驱动的解决方案。

下面的例子注册了一个电话 资源和一个常数 phoneRoutes,其中包含了该(电话)域的所有路由信息。在提供的答案中,我不喜欢解决逻辑的位置——主要模块不应该知道任何东西,也不应该被提供给控制器的资源参数的方式所困扰。这样逻辑就保持在同一个域中。

注意:如果你使用ngmin(如果你不是:你应该),你只需要写DI数组约定的解析函数。

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);
});
});
});
});
你可以在我最新(即将)的实验中看到它的全部荣耀。 虽然这个方法对我来说工作得很好,但我真的想知道为什么$injector在检测到承诺对象的任何东西注入时不延迟任何东西的构造;它会让事情变得非常非常非常简单。

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

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

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

我觉得这个最省力。

您可以使用美元routeProvider解决属性来延迟路由更改,直到数据被加载。

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属性是在route上定义的。

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

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

我遇到过类似的情况,我在解析url时使用了从服务器的第一个请求中获取的一些数据。我面临的问题是,如果承诺是rejected

我使用的是一个自定义提供程序,它曾经返回一个Promise,该Promise在配置阶段由$routeProviderresolve解析。

这里我想强调的是when的概念,它是这样做的。

它在url栏中看到url,然后在调用的控制器和视图中看到各自的when块,到目前为止,很好。

假设我有以下配置阶段的代码。

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

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

让我们想象一个场景,我在地址栏AuthServicePrivider.auth()函数被调用时点击rootUrl。

假设返回的Promise处于拒绝状态

什么都没有渲染。

Otherwise块将不会被执行,因为它对于任何没有在配置块中定义的url和angularJs的配置阶段未知的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);
});

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

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

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

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

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

产生了弹跳的效果。history.back()而不是它工作得很好,但它并不总是回到历史在我的情况下。所以我发现,如果我只是在我的禁用屏幕上创建属性href而不是状态。去吧,效果很好。

<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.run(function($rootScope, $state) {


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


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




});
});