在ui-router的解析期间添加加载动画

36

$routeProviderresolve属性允许在相应视图呈现之前执行一些任务。

如果我想要在执行这些任务时显示一个旋转图标以增强用户体验怎么办?
确实,否则用户会感觉应用程序已被阻塞,因为一些毫秒内没有显示任何视图元素,例如大于600毫秒。

当然,有一种方法是定义一个全局的div元素在当前视图之外,以便在使用$scope.$rootChangeStart函数显示旋转图标。
但我不想仅仅通过将中间的旋转图标隐藏来隐藏整个页面。
我希望我的Web应用程序的某些页面在加载方式方面有所不同。

我看到了这篇有趣的帖子,其中包含了我上述描述的确切问题:

这种方法会导致可怕的UI体验。用户点击一个按钮以刷新列表或其他内容,因为库无法仅针对受状态更改影响的视图显示旋转图标,所以整个屏幕都被覆盖了一般的旋转图标。拒绝。

无论如何,在我提交此问题之后,我意识到"resolve"特性是一种反模式。它等待所有承诺都解决,然后动画化状态更改。这是完全错误的-您希望状态之间的转换动画与数据加载并行运行,以便后者可以被前者覆盖。

例如,假设您有一个项目列表,并且单击其中一个项目将隐藏该列表并在不同的视图中显示该项目的详细信息。如果我们为项目细节进行异步加载,平均需要400毫秒,那么我们可以通过在列表视图上设置300毫秒的"离开"动画和在项目详细信息视图上设置300毫秒的"进入"动画来几乎完全覆盖加载。这样我们就提供了更流畅的感觉。

在大多数情况下,通过将异步加载逻辑放在控制器中来避免使用resolve可以避免在UI上显示旋转图标。

然而,这需要我们同时启动异步加载和状态更改动画。如果使用 "resolve",则整个异步动画会在动画开始之前发生。用户点击后会看到旋转图标,然后才会看到过渡动画。整个状态更改需要花费约1000毫秒,太慢了。
如果"Resolve"有不等待Promise的选项,它可能是缓存不同视图之间依赖关系的有用方法,但当前行为总是在状态更改开始之前解析它们,几乎没用,在我看来应该避免任何涉及异步加载的依赖关系。

所以,你是否真的应该停止使用resolve来加载一些数据,而是直接在相应的控制器中加载它们?这样可以使得任务执行的时候及时更新视图,并且可以在视图中指定位置进行更新,而不是全局更新。

1
我们已经花了两周时间开发一个类似于这个的旋转器。我们尝试了所有方法。我们使用了“resolve”函数,在加载时在控制器中打开旋转器,使用“spinner-directive”。但是没有一种方法可以干净地解决问题。最终,我们采用了混合解决方案,使用根作用域中变量的观察来激活/停用模态旋转器视图。它仍然有一些小问题,但总体解决方案是可接受的。如果您需要代码片段,我可以提供给您。 - Th0rndike
@Th0rndike 如果我们直接在控制器中加载数据会怎样呢?我们将绕过resolve属性。代码设计可能不如应该的那么干净,但我想不出这会是一个坏方法。事实上,在任务加载时,我们将拥有完整的视图可供操作。 - Mik378
我们遇到的大部分问题是在转换期间显示旋转器。在实例化和填充控制器时加载旋转器并不能解决视图转换时的“等待旋转器”问题。如果这不是您的要求,那么您可以使用它。 - Th0rndike
假设有两个页面:A导向B。如果您希望A显示“离开”动画,然后让B接替其位置,那么我认为目前没有简单的解决方案。但是,如果一旦要求B,A就直接离开,并且B通过其控制器自己显示自己的旋转器,则使用B的控制器将变得容易和好用。对于用户来说,无论是在视图A中显示控制器的A控制器还是在视图B中显示控制器的B控制器,都不会有任何区别。 - Mik378
离开视图时会有一些延迟,这意味着在打开目标视图之前清除控制器元素(销毁 $scope 等)的过程需要超过 300 毫秒?! - Mik378
显示剩余7条评论
6个回答

51
你可以使用一个指令来监听$routeChangeStart事件,例如在触发时显示元素:
app.directive('showDuringResolve', function($rootScope) {

  return {
    link: function(scope, element) {

      element.addClass('ng-hide');

      var unregister = $rootScope.$on('$routeChangeStart', function() {
        element.removeClass('ng-hide');
      });

      scope.$on('$destroy', unregister);
    }
  };
});

然后您将其放置在特定视图的加载器上,例如:

视图1:

<div show-during-resolve class="alert alert-info">
  <strong>Loading.</strong>
  Please hold.
</div>

视图2:

<span show-during-resolve class="glyphicon glyphicon-refresh"></span>

这个解决方案(以及许多其他解决方案)的问题在于,如果您从外部网站浏览到其中一条路由,则不会加载先前的 ng-view 模板,因此在解析期间您的页面可能会是空白的。
通过创建一个指令来充当后备加载程序,可以解决这个问题。它将侦听 $routeChangeStart 事件,并仅在没有先前路由时显示加载程序。
一个基本示例:
app.directive('resolveLoader', function($rootScope, $timeout) {

  return {
    restrict: 'E',
    replace: true,
    template: '<div class="alert alert-success ng-hide"><strong>Welcome!</strong> Content is loading, please hold.</div>',
    link: function(scope, element) {

      $rootScope.$on('$routeChangeStart', function(event, currentRoute, previousRoute) {
        if (previousRoute) return;

        $timeout(function() {
          element.removeClass('ng-hide');
        });
      });

      $rootScope.$on('$routeChangeSuccess', function() {
        element.addClass('ng-hide');
      });
    }
  };
});

回退加载器应放置在具有 ng-view 的元素外面:
<body>
  <resolve-loader></resolve-loader>
  <div ng-view class="fadein"></div>
</body>

演示所有功能:http://plnkr.co/edit/7clxvUtuDBKfNmUJdbL3?p=preview

很好的方式,非常感谢 :) 我相信它并不适用于所有情况。我会研究一下并告诉你我的感受。 - Mik378
@tasseKATT resolveLoader指令非常好用!直到你决定使用templateUrl加载模板,然后它就再也没有显示出来了。我不知道为什么。http://plnkr.co/edit/8LRrZNTeswY0wVCqAtp7?p=preview - Sbu
4
这是一个与链接函数相关的时间问题。如果使用templateUrl,您只需要这些:http://plnkr.co/edit/31qu9xcZCCEzSfHn5YyN?p=preview。等我有时间时,我会仔细查看并更新我的答案。 - tasseKATT
3
更新 $routeChangeStart$stateChangeStart,以适配新版本的 ui-router。具体操作请参考 https://github.com/angular-ui/ui-router/wiki#state-change-events。 - SimplGy
可能你可以使用scope.$on代替$rootScope.$on,因为事件是广播的,所以应该传递到所有作用域(不确定100%)。 https://docs.angularjs.org/api/ngRoute/service/$route - DarthVanger
@DarthVanger 正确。 - tasseKATT

29

我认为这相当不错

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

  $rootScope.$on('$stateChangeStart',function(){
      $rootScope.stateIsLoading = true;
 });


  $rootScope.$on('$stateChangeSuccess',function(){
      $rootScope.stateIsLoading = false;
 });

}]);

然后在视图上查看

<div ng-show='stateIsLoading'>
  <strong>Loading.</strong>
</div>

@Pranay Dytta,你让它变得尽可能简单了。 - Mo.
1
简单但无法处理嵌套的ui-view。因为全局变量会影响所有部分和加载。即使在已经加载的部分上也会出现“正在加载”的消息。 - Kalyan

11
为了进一步解答Pranay的答案,这是我的做法。
JS:
app.run(['$rootScope',function($rootScope){

    $rootScope.stateIsLoading = false;
    $rootScope.$on('$routeChangeStart', function() {
        $rootScope.stateIsLoading = true;
    });
    $rootScope.$on('$routeChangeSuccess', function() {
        $rootScope.stateIsLoading = false;
    });
    $rootScope.$on('$routeChangeError', function() {
        //catch error
    });

}]);

HTML

->

HTML

<section ng-if="!stateIsLoading" ng-view></section>
<section ng-if="stateIsLoading">Loading...</section>

3
在较新版本的UI Router中,它是$stateChangeStart和$stateChangeSuccess。 - George Kagan

3

我晚了两年才来,虽然其他的解决方案也可以工作,但我发现在一个"run"块中处理所有这些问题更加容易,就像这样:

.run(['$rootScope','$ionicLoading', function ($rootScope,$ionicLoading){


  $rootScope.$on('loading:show', function () {
    $ionicLoading.show({
        template:'Please wait..'
    })
  });

  $rootScope.$on('loading:hide', function () {
    $ionicLoading.hide();
  });

  $rootScope.$on('$stateChangeStart', function () {
    console.log('please wait...');
    $rootScope.$broadcast('loading:show');
  });

  $rootScope.$on('$stateChangeSuccess', function () {
    console.log('done');
    $rootScope.$broadcast('loading:hide');
  });

}])

你不需要任何其他东西。相当容易,对吧。这里有一个示例


1

3
鼓励提供外部资源链接,但请在链接周围添加上下文,以便其他用户了解它是什么以及为什么存在。始终引用重要链接的最相关部分,以防目标站点无法访问或永久离线。 - Bugs

0

'$stateChangeStart'和'$stateChangeSuccess'的问题在于当你返回到上一个状态时,"$rootScope.stateIsLoading"不会被刷新。 有没有什么解决办法? 我也尝试了以下方法:

    $rootScope.$on('$viewContentLoading',
        function(event){....});

    $rootScope.$on('$viewContentLoaded',
        function(event){....});

但是仍然存在相同的问题。


这并没有提供问题的答案。如果您想对作者进行批评或请求澄清,请在他们的帖子下留言 - 您始终可以在自己的帖子上发表评论,并且一旦您拥有足够的声望,您将能够在任何帖子上发表评论。 - Dethariel

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接