Показать загрузочную анимацию для медленного скрипта, используя 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)
) должен быть небольшим, но достаточным, чтобы дать потоку пользовательского интерфейса некоторое время для реагирования - реальная логика фильтра, которая, вероятно, должна быть обратным вызовом в реальных сценариях.
Это только доказательство концепции; Я бы предложил рефакторинг (возможно, директивы) для реального использования.
Вот шаги:
- Во-первых, вы должны использовать CSS-анимацию. Никакие анимации и GIF, управляемые JS, не должны использоваться в тяжелых процессах, потому что. из одного потока предел. Анимация остановится. 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'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'])