加载中/错误子状态 编辑页面
英文原文: http://emberjs.com/guides/routing/loading-and-error-substates/
除了异步路由指南中描述的技术外,Ember路由还提供了通过使用error
和loading
子状态,来实现自定义路由间的异步过渡。
loading
子状态
考虑下面的情形:
1 2 3 4 5 |
App.Router.map(function() { this.resource('articles', function() { // -> ArticlesRoute this.route('overview'); // -> ArticlesOverviewRoute }); }); |
如果导航到articles/overview
,并且在ArticlesRoute#model
中,返回了一个AJAX查询承诺,来加载需要花费较长时间才能完成加载的文章集合。在这其间,UI并不会有任何关于在做什么的实际性反馈;如果通过页面刷新进入到该页面,UI将会一直是空的,因为这时还没有完成进入路由,也没有显示任何模板;如果是从其他的路由进入articles/overview
,那么会一直停留在之前路由渲染的模板,直到所有文章加载完成,这个时候articles/overview
的所有模板才会被渲染,才可见。
那么,在这个过渡的过程中该如何添加一些反馈信息呢?
loading
事件
在深入讨论加载中子状态之前,理解loading
事件的行为非常重要。
Ember路由允许通过beforeModel
/model
/afterModel
各种钩子在一个过渡的过程中返回承诺(详细介绍)。这些承诺在其被履行前会将过渡暂停。如果在其中一个钩子中返回了承诺,并且这个承诺没有理解解决,那么loading
事件就会在该路由被触发,并且一直向上冒泡到ApplicationRoute
。例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
App.Router.map(function() { this.resource('foo', function() { // -> FooRoute this.route('slowModel'); // -> FooSlowModelRoute }); }); App.FooSlowModelRoute = Ember.Route.extend({ model: function() { return somePromiseThatTakesAWhileToResolve(); }, actions: { loading: function(transition, originRoute) { // displayLoadingSpinner(); // Return true to bubble this event to `FooRoute` // or `ApplicationRoute`. return true; } } }); |
如果FooSlowModelRoute
返回了一个较慢的承诺,那么loading
事件就会在FooRoute
上被触发(而不是FooSlowModelRoute
上)。
loading
事件的缺省实现
之前已经介绍了可以使用一种层次化的结构来配置loading
行为。此外,Ember还提供了一种实现了如下所示的缺省的loading
处理器。
1 2 3 4 5 6 7 |
App.Router.map(function() { this.resource('foo', function() { // -> FooRoute this.resource('for.bar', function() { // -> FooBarRoute this.route('baz'); // -> FooBarBazRoute }); }); }); |
如果foo.bar.baz
路由返回了一个不会立即履行的承诺,Ember会在foo.bar.baz
的层次结构中查找可以过渡的loading
路由,查找路径为:
foo.bar.loading
foo.loading
loading
Ember会在上述的loading
路由地址找一个路由,a) 该路由可能是如下所示定义的一个路由的子类:
App.FooBarLoadingRoute
App.FooLoadingRoute
App.LoadingRoute
b) 又或者是一个按照一定规则进行命名的loading
模板:
foo/bar/loading
foo/loading
loading
在一个较慢的异步过渡过程中,如果存在loading
子状态/路由,Ember会先过渡到第一个找到的。这个到loading
子状态的中间状态的过渡会立即发生(同步的),URL也不会发生改变。与其他在当另一个异步过渡活动时发生的过渡不同,该情况下的活动异步过渡不会被取消。
当进入一个loading
子状态过渡后,对应该子状态的模板如果存在的话,会被渲染到父路由的主插口(outlet),例如foo.bar.loading
模板会被渲染到foo.bar
的插口中。(loading
路由并不是特例,所有路由都是按照这种方式工作的。)
当foo.bar.baz
主异步过渡完成时,会退出loading
子状态,渲染的模板也会被移除,并进入foo.bar.baz
,渲染其模板。
渴望型 VS 延迟型异步过渡
loading
子状态都是可选的,如果提供了loading
子状态,那么就表示强调了希望异步过渡为“渴望型”的。在缺少目标路由的loading
子状态时,路由将依然停留在之前的过渡路由,知道所有目标路由的承诺得到履行,知会在过渡完全完成是一次性过渡到目标路由(渲染模板等)。但是如果提供了一个目标路由的loading
子状态,那么就选择了“渴望型”过渡,这就表明与默认的“延迟型”不同,会首先退出当前路由(清除其模板等),并过渡到loading
子状态。除非过渡被取消或者在同一运行循环中被重定向,否则URL都会理解更新。
这里也暗含了一个错误处理,例如,当过渡到一个路由失败时,延迟过渡(默认)会依然停留在原路由,而一个渴望过渡早就离开了之前的状态路由进入到loading
子状态中了。
error
子状态
Ember为过渡提供了一种与loading
事件/子状态类似的错误处理方法。
1 2 3 4 5 |
App.Router.map(function() { this.resource('articles', function() { // -> ArticlesRoute this.route('overview'); // -> ArticlesOverviewRoute }); }); |
如果ArticlesOverviewRoute
返回一个被拒绝的承诺(可能因为服务器端返回一个错误,又或者时用户没有登录等等),一个error
事件将在ArticlesOverviewRoute
被触发,并向上冒泡。可以使用这个error
事件来处理并显示一个错误消息,例如重定向到登录页面等。与loading
事件处理器实现类似,缺省的error
事件处理器也会进入一个子状态来完成处理。
例如,在ArticlesOverviewRoute#model
(或beforeModel
、afterModel
)中抛出了一个异常或者返回了一个被拒绝的承诺,那么会按照以下方式进行查找错误处理:
ArticlesErrorRoute
路由或者articles/error
模板ErrorRoute
路由或者error
模板
如果之上任意一个被找到,路由将立即过渡到该子状态(不更新URL),错误的“原因”(例如抛出的异常或拒绝的承诺)会作为model
传递给error
子状态。
如果没有找到可以访问的error
子状态,那么一条错误消息会在控制台中输出。
loading
/error
子状态处理的唯一区别是,error
从过渡的中心路由开始向上冒泡。
带动态段的error
子状态
带动态段的路由通常映射到一个模型的两个不同的层面。例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
App.Router.map(function() { this.resource('foo', {path: '/foo/:id'}, function() { this.route('baz'); }); }); App.FooRoute = Ember.Route.extend({ model: function(params) { return new Ember.RSVP.Promise(function(resolve, reject) { reject("Error"); }); } }); |
在URL层次中,访问/foo/12
将会导致将foo
模板渲染到application
模板的outlet
处。当尝试加载一个foo
路由发生一个错误事件,会将顶层的error
模板渲染到application
模板的outlet
。这感觉就好像foo
路由就从未正确进入过一般。为了创建一个foo
范围的错误信息,并渲染foo/error
到foo
的outlet
中,那么需要将动态段分离:
1 2 3 4 5 6 7 |
App.Router.map(function() { this.resource('foo', {path: '/foo'}, function() { this.resource('elem', {path: ':id'}, function() { this.route('baz'); }); }); }); |
遗留的LoadingRoute
之前的Ember版本(有些不慎)支持通过定义一个全局的LoadingRoute
,该路由将在过渡遇到一个较慢的承诺或者完全退出一个过渡时被激活。因为loading
模板作为顶层的视图来渲染,并没有放入到一个插口中,那么在这里处理可以显示一个加载中的指示器外几乎不能做其他的事情。与此相比较,loading
事件/子状态提供了更强的控制力,如果希望模拟与遗留的LoadingRoute
类似的行为,可以按照如下的例子来实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
App.LoadingView = Ember.View.extend({ templateName: 'global-loading', elementId: 'global-loading' }); App.ApplicationRoute = Ember.Route.extend({ actions: { loading: function() { var view = this.container.lookup('view:loading').append(); this.router.one('didTransition', view, 'destroy'); } } }); |
上例实现了一个与LoadingRoute
类型的行为,当路由进入一个loading
状态时,在顶层添加了一个视图,并在完成过渡时删除加入的视图。