延迟 AngularJS 路由更改直到模型加载以防止闪烁
Posted
技术标签:
【中文标题】延迟 AngularJS 路由更改直到模型加载以防止闪烁【英文标题】:Delaying AngularJS route change until model loaded to prevent flicker 【发布时间】:2012-08-11 21:49:00 【问题描述】:我想知道 AngularJS 是否有办法(类似于 Gmail)延迟显示新路线,直到使用其各自的服务获取每个模型及其数据之后。
例如,如果有一个列出所有项目的ProjectsController
和显示这些项目的模板project_index.html
,则将在显示新页面之前完全获取Project.query()
。
在此之前,旧页面仍将继续显示(例如,如果我正在浏览另一个页面,然后决定查看此项目索引)。
【问题讨论】:
【参考方案1】:$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;
请注意,控制器定义包含一个解析对象,该对象声明了控制器构造函数应该可用的东西。这里phones
被注入到控制器中,并在resolve
属性中定义。
resolve.phones
函数负责返回一个承诺。收集所有的 Promise 并延迟路由更改,直到所有的 Promise 都解决后。
工作演示:http://mhevery.github.com/angular-phonecat/app/#/phones 来源:https://github.com/mhevery/angular-phonecat/commit/ba33d3ec2d01b70eb5d3d531619bf90153496831
【讨论】:
@MiskoHevery - 如果您的控制器位于模块内部并且被定义为字符串而不是函数,该怎么办。你怎么能像你一样设置 resolve 属性? 如何在angular.controller()
类型控制器定义中使用它?在$routeProvider
的东西中,我认为您必须使用控制器的字符串名称。
任何使用 angular.controller() 和最新版本 AngularJS 的示例?
@blesh,当您使用angular.controller()
时,您可以将此函数的结果分配给一个变量(var MyCtrl = angular.controller(...)
),然后进一步使用它(MyCtrl.loadData = function()..
)。查看 egghead 的视频,代码直接显示在那里:egghead.io/video/0uvAseNXDr0
我仍然想要一个很好的方法,而不需要将你的控制器放在全局中。我不想到处乱扔全球垃圾。你可以用一个常量来做,但是如果能够将解析函数放在控制器上/控制器中,而不是其他地方,那就太好了。【参考方案2】:
这是一个适用于 Angular 1.0.2 的最小工作示例
模板:
<script type="text/ng-template" id="/editor-tpl.html">
Editor Template datasets
</script>
<div ng-view>
</div>
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),我们实际上不需要创建我们自己的。所以我们可以简化MyCtrl。解决:
MyCtrl.resolve =
datasets : function($http)
return $http(
method: 'GET',
url: 'http://fiddle.jshell.net/'
);
;
$http() 的结果包含 data、status、headers 和 config 对象,所以我们需要将 MyCtrl 的正文改为:
$scope.datasets = datasets.data;
http://jsfiddle.net/dTJ9N/5/
【讨论】:
我正在尝试做这样的事情,但在注入“数据集”时遇到了麻烦,因为它没有定义。有什么想法吗? 嘿 mb21,我想你可以帮我解决这个问题:***.com/questions/14271713/… 有人可以帮我将此答案转换为 app.controller('MyCtrl') 格式吗? jsfiddle.net/5usya/1 对我不起作用。 我收到一个错误:Unknown provider: datasetsProvider <- datasets
您可以通过将数据集替换为以下内容来简化答案:function($http) return $http(method: 'GET', url: '/someUrl') .then( function(data) return data;, function(reason)return 'error value'; );
【参考方案3】:
我看到有人问如何使用 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';
]);
由于此代码源自问题/最受欢迎的答案,因此未经测试,但如果您已经了解如何制作缩小友好的角度代码,它应该会向您发送正确的方向。我自己的代码不需要的一部分是将“电话”注入“电话”的解析函数中,我也根本没有使用任何“延迟”对象。
我也推荐这个 youtube 视频 http://www.youtube.com/watch?v=P6KITGRQujQ&list=UUKW92i7iQFuNILqQOUOCrFw&index=4&feature=plcp,它对我帮助很大
如果你感兴趣,我决定也粘贴我自己的代码(用咖啡脚本编写),这样你就可以看到它是如何工作的。
仅供参考,我提前使用了一个通用控制器来帮助我在几个模型上进行 CRUD:
appModule.config ['$routeProvider', ($routeProvider) ->
genericControllers = ["boards","teachers","clas-s-rooms","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 ....
]
【讨论】:
我是否从您的示例和我的失败尝试中正确推断出,在最新版本的 Angular 中,现在无法在控制器中引用resolve
函数?所以它必须像这里一样在配置中声明?
@XMLilley 我很确定是这样。我相信这个例子是我写的时候从 1.1.2 开始的。我没有看到任何关于将解析放入控制器的文档
酷,谢谢。在 SO 上有很多这样做的例子(比如这里的前两个),但它们都来自 2012 年和 2013 年初。这是一种优雅的方法,但似乎已被弃用。现在最干净的选择似乎是编写作为承诺对象的单个服务。
感谢这对我有用。对于遇到关于 undefined $defer
服务错误的任何其他人,请注意在 AngularJS 的 1.5.7 版本中,您想改用 $timeout
。【参考方案4】:
This commit 是 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;
]);
【讨论】:
请问哪些版本包含该提交? 最新的不稳定版本(1.1.5)包含这个提交。 ajax.googleapis.com/ajax/libs/angularjs/1.1.5/angular.min.js 我喜欢这种不那么冗长的方法。从实际的数据对象创建一个 Promise 并直接传递它会很好,但代码太少了,它工作得很好。 资源如何访问 $routeParams?例如:在GET '/api/1/apps/:appId'
--> App.get(id: $routeParams.appId).$promise();
我不能这样使用
@zeronone 你注入$route
到你的解决方案并使用$route.current.params
。小心,$routeParams
仍然指向旧路线。【参考方案5】:
这个 sn-p 对 依赖注入 友好(我什至将它结合使用 ngmin 和 uglify),而且它更优雅领域驱动 基于解决方案。
下面的示例注册了一个 Phone resource 和一个 constant phoneRoutes,其中包含您的所有路由信息那个(电话)域。在提供的答案中我不喜欢的是 resolve 逻辑的位置 - main 模块不应该知道 任何事情或被打扰关于资源参数提供给控制器的方式。这样逻辑就保持在同一个域中。
注意:如果您使用 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;
]
);
下一部分是在模块处于配置状态时注入路由数据并将其应用到 $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);
);
);
);
);
您可以在my latest (upcoming) experiment 中看到它的全部荣耀。 虽然这种方法对我来说很好,但我真的很想知道为什么 $injector 在检测到 anything 的注入是 promiseanything 的构造/强>对象;它会让事情变得更简单。
编辑:使用 Angular v1.2(rc2)
【讨论】:
这个出色的答案似乎更符合“Angular”哲学(封装等)。我们都应该有意识地努力阻止逻辑像葛一样在代码库中蔓延。I really wonder why the $injector isn't delaying construction of anything when it detects injection of anything that is a promise object
我猜他们省略了这个功能,因为它可能会鼓励对应用程序的响应能力产生负面影响的设计模式。他们心目中的理想应用是真正异步的应用,因此解析应该是一个边缘案例。【参考方案6】:
延迟显示路线肯定会导致异步纠缠......为什么不简单地跟踪主实体的加载状态并在视图中使用它。例如,在您的控制器中,您可能会在 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>
【讨论】:
也许将 HTTP 状态暴露给视图是不对的,就像处理 CSS 类和 DOM 元素属于控制器一样。我可能会使用相同的想法,但在 isValid() 和 isLoaded() 中抽象状态。 这确实不是最好的关注点分离,而且如果你有依赖于特定对象的嵌套控制器,它会崩溃。 这很聪明.. 至于将状态代码暴露给视图,您可以将 http 逻辑粘贴到控制器的范围属性中,然后绑定到它们。此外,如果您在后台进行多个 ajax 调用,无论如何您都希望这样做。 如果问题只是延迟查看问题,那就没问题了。但是如果您需要延迟控制器的实例化——而不仅仅是视图,最好使用 resolve。 (例如:如果您需要确保您的 JSON 已加载,因为您的控制器在连接之前将其传递给指令。)来自文档:“路由器将等待它们全部被解析或一个被拒绝 before控制器被实例化".【参考方案7】:我在上面的 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;
【讨论】:
【参考方案8】:使用 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 团队和贡献者的帮助,时间缩短了很多。 :)
这也是马克西米利安霍夫曼的答案。显然,该提交已进入 1.1.5。
【讨论】:
我在the docs 中似乎找不到任何关于$promise
的信息。从 v2.0+ 起,它可能已被删除。
仅在 1.2 中可用【参考方案9】:
您可以使用$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
属性是在路由上定义的。
EntitiesCtrlResolve
和 EntityCtrlResolve
是与 EntitiesCtrl
和 EntityCtrl
控制器在同一文件中定义的 constant 对象。
// 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..
);
【讨论】:
【参考方案10】:我喜欢 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 progress bar docs
【讨论】:
如果您要加载多条数据,则有点崩溃。你怎么知道所有东西都加载了? 自从我在 2 月添加此答案以来,事情已经发生了变化,此页面上有更多活动。看起来 Angular 现在有比这里建议的更好的支持来解决这个问题。干杯, 我来的有点晚,但处理多条数据并不是什么大问题。您只需为每个请求使用单独的变量(布尔值:isReadyData1、isReadyData2 等),并设置 $scope.ready = isReadyData1 && isReadyData2 ...;很适合我。【参考方案11】:我喜欢上面的答案,并从中学到了很多,但上面的大多数答案都缺少一些东西。
我被困在一个类似的场景中,我正在使用从服务器的第一个请求中获取的一些数据来解析 url。 我面临的问题是,如果承诺是 rejected
。
我使用了一个自定义提供程序,该提供程序用于返回一个 Promise
,在配置阶段由 $routeProvider
的 resolve
解析。
我想在这里强调的是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
。
让我们想象一个场景,我在地址栏中点击了 rootUrl AuthServicePrivider.auth()
函数被调用。
假设返回的 Promise 处于 reject 状态然后呢???
根本没有渲染任何东西。
Otherwise
块不会被执行,因为它对于任何未在配置块中定义且 angularJs 配置阶段未知的 url。
当这个promise没有解决时,我们将不得不处理被触发的事件。失败时$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);
);
]);
这样我们可以在配置阶段处理promise失败。
【讨论】:
【参考方案12】:我有一个复杂的多级滑动面板界面,屏幕层被禁用。在禁用屏幕层上创建指令,该指令将创建单击事件以执行状态,如
$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;
);
);
【讨论】:
【参考方案13】:一种可能的解决方案可能是将 ng-cloak 指令与我们使用模型的元素一起使用,例如
<div ng-cloak="">
Value in myModel is: myModel
</div>
我认为这个最省力。
【讨论】:
以上是关于延迟 AngularJS 路由更改直到模型加载以防止闪烁的主要内容,如果未能解决你的问题,请参考以下文章
AngularJS:在不完全重新加载控制器的情况下更改哈希和路由
AngularJS UI 路由器 - 在不重新加载状态的情况下更改 url