Показать загрузочную анимацию для медленного скрипта, используя AngularJS?

В angularjs 1.2 такие операции, как фильтрация ng-repeat со многими строками (>2000 строк) может стать довольно медленным (>1 сек).
Я знаю, что могу оптимизировать время выполнения, используя limitTo, нумерация страниц, пользовательские фильтры и т. д., но мне все еще интересно узнать, возможно ли показать анимацию загрузки, пока браузер занят выполнением длинных сценариев.

В случае углового я думаю, что это может быть вызвано всякий раз, когда $digest работает, потому что это, кажется, основная функция, которая занимает больше всего времени и может вызываться несколько раз.

В связанном вопросе не было дано никаких полезных ответов. Любая помощь с благодарностью!

8 ответов

Проблема в том, что, пока выполняется Javascript, пользовательский интерфейс не имеет возможности обновляться. Даже если вы представите счетчик перед фильтрацией, он покажется "замороженным", пока Angular занят.

Способ преодолеть это - отфильтровать порции и, если доступно больше данных, отфильтровать снова после небольшого $timeout, Тайм-аут дает возможность потоку пользовательского интерфейса запускать и отображать изменения и анимацию.

Скрипка, демонстрирующая принцип, здесь.

Он не использует фильтры Angular (они синхронные). Вместо этого он фильтрует data массив со следующей функцией:

function filter() {
    var i=0, filtered = [];
    innerFilter();

    function innerFilter() {
        var counter;
        for( counter=0; i < $scope.data.length && counter < 5; counter++, i++ ) {
            /////////////////////////////////////////////////////////
            // REAL FILTER LOGIC; BETTER SPLIT TO ANOTHER FUNCTION //
            if( $scope.data[i].indexOf($scope.filter) >= 0 ) {
                filtered.push($scope.data[i]);
            }
            /////////////////////////////////////////////////////////
        }
        if( i === $scope.data.length ) {
            $scope.filteredData = filtered;
            $scope.filtering = false;
        }
        else {
            $timeout(innerFilter, 10);
        }
    }
}

Требуются 2 вспомогательные переменные: $scope.filtering является true когда фильтр активен, это дает нам возможность отобразить счетчик и отключить вход; $scope.filteredData получает результат фильтра.

Есть 3 скрытых параметра:

  • размер куска (counter < 5) мал, чтобы продемонстрировать эффект
  • задержка тайм-аута ($timeout(innerFilter, 10)) должен быть небольшим, но достаточным, чтобы дать потоку пользовательского интерфейса некоторое время для реагирования
  • реальная логика фильтра, которая, вероятно, должна быть обратным вызовом в реальных сценариях.

Это только доказательство концепции; Я бы предложил рефакторинг (возможно, директивы) для реального использования.

Вот шаги:

  1. Во-первых, вы должны использовать CSS-анимацию. Никакие анимации и GIF, управляемые JS, не должны использоваться в тяжелых процессах, потому что. из одного потока предел. Анимация остановится. CSS-анимации отделены от потока пользовательского интерфейса и поддерживаются в IE 10+ и во всех основных браузерах.
  2. Напишите директиву и разместите ее вне вашей ng-view сfixed позиционирование.
  3. Привязать его к контроллеру приложения с помощью специального флага.
  4. Переключите видимость этой директивы до и после длительных / тяжелых процессов. (Вы можете даже привязать текстовое сообщение к директиве, чтобы отобразить некоторую полезную информацию для пользователя). - Взаимодействие с этим или чем-либо еще непосредственно в цикле тяжелого процесса займет больше времени, чтобы закончить. Это плохо для пользователя!

Директива Шаблон:

<div class="activity-box" ng-show="!!message">
    <img src="img/load.png" width="40" height="40" />
    <span>{{ message }}</span>
</div>

activity Директива:

Простая директива с одним атрибутом message, Обратите внимание ng-show Директива в шаблоне выше. message используется как для переключения индикатора активности, так и для установки информационного текста для пользователя.

app.directive('activity', [
    function () {
        return {
            restrict: 'EA',
            templateUrl: '/templates/activity.html',
            replace: true,
            scope: {
                message: '@'
            },
            link: function (scope, element, attrs) {}
        };
    }
]);

SPA HTML:

<body ng-controller="appController">
    <div ng-view id="content-view">...</div>
    <div activity message="{{ activityMessage }}"></div>
</body>

Обратите внимание, что activity директива размещена за пределами ng-view, Он будет доступен в каждом разделе вашего одностраничного приложения.

Контроллер APP:

app.controller('appController',
    function ($scope, $timeout) {
        // You would better place these two methods below, inside a 
        // service or factory; so you can inject that service anywhere
        // within the app and toggle the activity indicator on/off where needed
        $scope.showActivity = function (msg) {
            $timeout(function () {
                $scope.activityMessage = msg;
            });
        };
        $scope.hideActivity = function () {
            $timeout(function () {
                $scope.activityMessage = '';
            }, 1000); // message will be visible at least 1 sec
        };
        // So here is how we do it:
        // "Before" the process, we set the message and the activity indicator is shown
        $scope.showActivity('Loading items...');
        var i;
        for (i = 0; i < 10000; i += 1) {
            // here goes some heavy process
        }
        // "After" the process completes, we hide the activity indicator.
        $scope.hideActivity();
    });

Конечно, вы можете использовать это и в других местах. например, вы можете позвонить $scope.hideActivity(); когда обещание разрешается. Или переключение активности на request а также response из httpInterceptor это тоже хорошая идея.

Пример CSS:

.activity-box {
    display: block;
    position: fixed; /* fixed position so it doesn't scroll */
    z-index: 9999; /* on top of everything else */
    width: 250px;
    margin-left: -125px; /* horizontally centered */
    left: 50%;
    top: 10px; /* displayed on the top of the page, just like Gmail's yellow info-box */
    height: 40px;
    padding: 10px;
    background-color: #f3e9b5;
    border-radius: 4px;
}

/* styles for the activity text */
.activity-box span {
    display: block;
    position: relative;
    margin-left: 60px;
    margin-top: 10px;
    font-family: arial;
    font-size: 15px;
}

/* animating a static image */
.activity-box img {
    display: block;
    position: absolute;
    width: 40px;
    height: 40px;
    /* Below is the key for the rotating animation */
    -webkit-animation: spin 1s infinite linear;
    -moz-animation: spin 1s infinite linear;
    -o-animation: spin 1s infinite linear;
    animation: spin 1s infinite linear;
}

/* keyframe animation defined for various browsers */
@-moz-keyframes spin {
    0% { -moz-transform: rotate(0deg); }
    100% { -moz-transform: rotate(359deg); }
}
@-webkit-keyframes spin {
    0% { -webkit-transform: rotate(0deg); }
    100% { -webkit-transform: rotate(359deg); }
}
@-o-keyframes spin {
    0% { -o-transform: rotate(0deg); }
    100% { -o-transform: rotate(359deg); }
}
@-ms-keyframes spin {
    0% { -ms-transform: rotate(0deg); }
    100% { -ms-transform: rotate(359deg); }
}
@keyframes spin {
    0% { transform: rotate(0deg); }
    100% { transform: rotate(359deg); }
}

Надеюсь это поможет.

Используйте spin.js и сайт http://fgnass.github.com/spin.js/ показывает шаг, который довольно прост. анимация загрузки выполняется в CSS, который отделен от потока пользовательского интерфейса и поэтому загружается плавно.

Что вы можете сделать, это обнаружить конец ngRepeat как говорится в этом посте.

Я сделаю что-то вроде, в контроллере:

$scope.READY = false;

И в директиве, как сказано выше, я сделаю что-то вроде:

if (scope.$last) {
    $scope.READY = true;
}

И вы можете иметь загрузчик / спиннер на основе CSS с

<div class="loader" ng-show="!READY">
</div>

Конечно, вы также можете иметь анимации на основе CSS, которые не зависят от выполнения JS.

Вы можете запустить фильтр в другом потоке, используя WebWorkers, чтобы ваша страница angularjs не блокировалась.

Если вы не используете веб-работников, браузер может получить тайм-аут выполнения javascript и полностью остановить ваше угловое приложение, и даже если вы не получите тайм-аут, ваше приложение зависнет до завершения расчета.

ОБНОВЛЕНИЕ 23.04.14

Я видел значительное улучшение производительности в крупномасштабном проекте с использованием Scalyr и Bindonce

Вот рабочий пример:

angular
  .module("APP", [])
  .controller("myCtrl", function ($scope, $timeout) {
    var mc = this
    
    mc.loading = true
    mc.listRendered = []
    mc.listByCategory = []
    mc.categories = ["law", "science", "chemistry", "physics"]
    
    
    mc.filterByCategory = function (category) {
      mc.loading = true
      // This timeout will start on the next event loop so 
      // filterByCategory function will exit just triggering 
      // the show of Loading... status

      // When the function inside timeout is called, it will
      // filter and set the model values and when finished call 
      // an inbuilt $digest at the end.
      $timeout(function () {
        mc.listByCategory = mc.listFetched.filter(function (ele) {
          return ele.category === category          
        })
        mc.listRendered = mc.listByCategory
        $scope.$emit("loaded")
      }, 50)
    }
    
    // This timeout is replicating the data fetch from a server
    $timeout(function () {
      mc.listFetched = makeList()
      mc.listRendered = mc.listFetched
      mc.loading = false    
    }, 50)
    
    $scope.$on("loaded", function () { mc.loading = false })
  })
  
  function makeList() {
    var list = [
      {name: "book1", category: "law"},
      {name: "book2", category: "science"},
      {name: "book1", category: "chemistry"},
      {name: "book1", category: "physics"}      
    ]
    var bigList = []
    for (var i = 0; i <= 5000; i++) {
      bigList = bigList.concat(list)
    }
    return bigList
  }
button {
  display: inline-block;      
}
<html>
  <head>
    <title>This is an Angular Js Filter Workaround!!</title>
  </head>
   
  <body ng-app="APP">
    
    <div ng-controller="myCtrl as mc">
      <div class = "buttons">
        <label>Select Category:- </label>
        <button ng-repeat="category in mc.categories" ng-click="mc.filterByCategory(category)">{{category}}</button>
      </div>
      
      <h1 ng-if="mc.loading">Loading...</h1>
      
      <ul ng-if="!mc.loading">
        <li ng-repeat="ele in mc.listRendered track by $index">{{ele.name}} - {{ele.category}}</li>
      </ul>
    </div>
    
    <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.5/angular.min.js"></script>
    
  </body>
  
<html>

Promise/deferred можно использовать в этом случае, вы можете позвонить в notify, чтобы посмотреть прогресс вашего кода, документацию от angular.js: https://code.angularjs.org/1.2.16/docs/api/ng/service/$ q Вот учебник по тяжелой обработке JS, в котором используется ng-Promise: http://liamkaufman.com/blog/2013/09/09/using-angularjs-promises/, надеюсь, это полезно.

// Фабрика приложений для хранения модели данных

app.factory('postFactory', функция ($ http, $ q, $ timeout) {

var  factory = {

    posts : false,

    getPosts : function(){

        var deferred = $q.defer();

        //avoiding the http.get request each time 
        //we call the getPosts function

        if (factory.posts !== false){
            deferred.resolve(factory.posts);

        }else{

            $http.get('posts.json')
            .success(function(data, status){
                factory.posts = data

                //to show the loading !
                $timeout(function(){
                    deferred.resolve(factory.posts)
                }, 2000);

            })
            .error(function(data, status){
                deferred.error('Cant get the posts !')
            })

        };  

        return deferred.promise;
    },

    getPost : function(id){

        //promise
        var deferred = $q.defer();

        var post = {};
        var posts = factory.getPosts().then(function(posts){
            post = factory.posts[id];

            //send the post if promise kept
            deferred.resolve(post);

        }, function(msg){
            deferred.reject(msg);
        })

        return deferred.promise;
    },

};
return factory;

});

Вы можете использовать этот код, взятый из этого URL: http://www.directiv.es/angular-loading-bar

там вы также можете найти демо-версию.

Вот их код:

    <!DOCTYPE html>
<html ng-app="APP">
<head>
    <meta charset="UTF-8">
    <title>angular-loading-bar example</title>
    <link rel="stylesheet" type="text/css" href="/application/html/js/chieffancypants/angular-loading-bar/loading-bar.min.css"/>
    <style>
        body{
            padding:25px;
        }
    </style>
</head>
<body ng-controller="ExampleController">
    <button id="reloader" ng-click="getUsers()">Reload</button>
    <ul ng-repeat="person in data">
        <li>{{person.lname}}, {{person.fname}}</li> 
    </ul>
    <script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.2.0-rc.2/angular.min.js"></script>
    <script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.2.0-rc.2/angular-animate.min.js"></script>
    <script src="/application/html/js/chieffancypants/angular-loading-bar/loading-bar.min.js"></script>
    <script>
    angular.module('APP', ['chieffancypants.loadingBar', 'ngAnimate']).
    controller("ExampleController",['$scope','$http',function($scope,$http){
        $scope.getUsers = function(){
            $scope.data=[];
            var url = "http://www.filltext.com/?rows=10&fname={firstName}&lname={lastName}&delay=3&callback=JSON_CALLBACK"
            $http.jsonp(url).success(function(data){
                $scope.data=data;
            })
        }
        $scope.getUsers()

    }])
    </script>
</body>
</html>

Как мне это использовать?

Установите его через npm или bower

$ npm install angular-loading-bar
$ bower install angular-loading-bar

Чтобы использовать, просто включите его в качестве зависимости в вашем приложении, и все готово!

angular.module('myApp', ['angular-loading-bar'])
Другие вопросы по тегам