AngularJS: показать анимацию загрузки для медленного script, например. при фильтрации

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

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

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

Ответ 1

Проблема в том, что до тех пор, пока выполняется 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)) должна быть небольшой, но достаточно, чтобы дать потоку пользовательского интерфейса некоторое время для реагирования.
  • фактическая логика фильтра, которая, вероятно, должна быть обратным вызовом в реальных сценариях жизни.

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

Ответ 2

Вот шаги:

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

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

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

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

Ответ 3

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

Ответ 4

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

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

$scope.READY = false;

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

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

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

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

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

Ответ 5

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

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

ОБНОВЛЕНИЕ 23.04.14

Я видел значительное улучшение производительности в крупномасштабном проекте, используя scalyr и bindonce

Ответ 6

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

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>

Ответ 7

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

//app Factory для хранения модели данных

app.factory('postFactory', function ($ 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;

});

Ответ 8

Вы можете использовать этот код, взятый из этого 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'])