Несколько полезных приемов в AngularJS для начинающих

При создании приложений на AngularJS у начинающего разработчика возникает много вопросов, особенно в том случае, если до определенного времени он использовал библиотеки сходные с jQuery или Prototype. В данном посте хотелось бы поделиться некоторыми приемами, которые будут полезны начинающим AngularJS разработчикам.

Загрузка приложения и менеджер состояний


До момента загрузки самого фрэймворка, его зависимостей и отработки всех запросов в бэкграунде приложение может выглядеть весьма удручающе. И даже ng-cloak не сможет помочь в большинстве случаев. Обычно для таких целей используют div имеющий больший z-index чем основной контент сайта и перекрывающий его до загрузки всех компонентов и состояний. Он может выглядеть так:

<div class="loader" ng-show="loader"><div class="loader-content">Loading...</div></div>


а его стили вот так:

    .loader {
        position: fixed;
        width: 100%;
        height: 100%;
        z-index: 9;
    }

    .loader-content {
        width: 128px;
        height: 128px;
        overflow: auto;
        margin: auto;
        position: absolute;
        z-index: 10;
        top: 0; left: 0; bottom: 0; right: 0;
    }

В AngularJS мы будем использовать переменную loader, которая будет находится в $rootScope, и в зависимости от ее значения наш div будет скрываться или отображаться в окне браузера. Постоянно изменять значение переменной в $rootScope довольно неудобно, при этом у нас может быть несколько операций, которые будут осуществляться в одно и тоже время, причем в тот момент когда одна из них отработает, вторая может не закончить свою работу. В этом случае загрузчик скроется.

Во избежание таких случаев можно создать сервис, который будет отвечать за хранение всех состояний, и в зависимости от того, есть ли какие-либо операции в работе, управлять значением $rootScope.loader. В самом простом виде его можно описать таким образом:

'use strict';

app.factory('StateManager', function StatemManager($rootScope, $log) {

	var stateContainer = [];
	
	return {
        add: function (service) {
            stateContainer.push(service);
            $rootScope.globalLoader = true;
			$log.log('Add service: ' + service);
        },

        remove: function (service) {
            stateContainer = _.without(stateContainer, service);
            $log.log('Remove service: ' + service);

            if (stateContainer.length === 0) {
                $rootScope.globalLoader = false;
                $log.log('StateContainer is empty.');
            }

        },

        getByName: function (service) {
            return _.include(stateContainer, service)
        },

        clear: function () {
            stateContainer.length = 0;
            $log.log('StateContainer clear.');
            return true; 
        }
	}

});

В данном примере используется библиотека underscore. Проще говоря, данный сервис записывает переданное имя процесса в массив и удаляет его по запросу. В случае если массив пустой, $rootScope.loader устанавливается в false. И пример использования:

StateManager.add('Load_json_data');
var request = $http.get('/data.josn');
request.success(function(response) {
	console.log(response);
	StateManager.remove('Load_json_data');
});


Конечно это не идеальный вариант, но для понимания принципа работы его достаточно.

Получение событий


Бывают ситуации, когда нам требуется получить событие или элемент из функции контроллера по, предположим, ng-click. Конечно, для этих целей лучше использовать директивы, но случается всякое. К счастью в нашем распоряжении имеется $event:

<ul>
	<li ng-repeat="name in names" ng-bind="name" ng-click="setActive($event);"></li>
</ul>


$scope.names = ['John', 'Peter', 'Joe'];
$scope.setActive = function (evt) {
	angular.element(evt.target).addClass('active');
}

В данном случае при клике по элементу из списка он получит класс active. По сути, angular.element является jqLite и позволяет использовать те самые методы к которым так привыкли любители jQuery.

AngularJS и PHP


Многие новички в AngularJS удивляются тому, что php не может обработать посланный ему POST-запрос. Он попросту не видит его. Все объясняется очень просто. Как говорят сами разработчики, AngularJS заточен под Ruby on Rails, поэтому сериализация данных происходит в формате JSON, непонятному для php. Есть прекрасная статья, описывающая способ избавления от данной проблемы. В случае, если вы хотите добиться работы $http сервиса в стиле jQuery.ajax вам потребуется включить следующий код в свое AngularJS приложение:

Javascript код
angular.module('MyModule', [], function($httpProvider)
{
  // Use x-www-form-urlencoded Content-Type
  $httpProvider.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded;charset=utf-8';
 
  // Override $http service's default transformRequest
  $httpProvider.defaults.transformRequest = [function(data)
  {
    /**
     * The workhorse; converts an object to x-www-form-urlencoded serialization.
     * @param {Object} obj
     * @return {String}
     */ 
    var param = function(obj)
    {
      var query = '';
      var name, value, fullSubName, subName, subValue, innerObj, i;
      
      for(name in obj)
      {
        value = obj[name];
        
        if(value instanceof Array)
        {
          for(i=0; i<value.length; ++i)
          {
            subValue = value[i];
            fullSubName = name + '[' + i + ']';
            innerObj = {};
            innerObj[fullSubName] = subValue;
            query += param(innerObj) + '&';
          }
        }
        else if(value instanceof Object)
        {
          for(subName in value)
          {
            subValue = value[subName];
            fullSubName = name + '[' + subName + ']';
            innerObj = {};
            innerObj[fullSubName] = subValue;
            query += param(innerObj) + '&';
          }
        }
        else if(value !== undefined && value !== null)
        {
          query += encodeURIComponent(name) + '=' + encodeURIComponent(value) + '&';
        }
      }
      
      return query.length ? query.substr(0, query.length - 1) : query;
    };
    
    return angular.isObject(data) && String(data) !== '[object File]' ? param(data) : data;
  }];
});


Кэширование ($cacheFactory)


Даже отнюдь не новички пренебрегают этим сервисом. Предположим, вы имеете контроллер, который при переходе на определенный роут делает POST-запрос и получает данные об альбомах исполнителя:

var request = $http.get('/albums.json');
request.success(function (response) {
	$scope.albums = response;
});

Дело в том, что если пользователь перейдет по другой ссылке, а после вернется обратно, наш код выполнится снова, и запрос на получение альбомов уйдет повторно. Довольно часто нет нужды использовать повторные запросы на сервер после загрузки всего приложения. Ситуаций может быть множество. Для этого и существует $cacheFactory. Попробуем создать простейший кэш-сервис:

app.factory('DataCache', function ($cacheFactory) {
	return $cacheFactory('dataCache', {});
});

и использовать его в нашем контроллере:

app.controller('AlbumsCtrl', function (DataCache) {

	var $scope.albums = DataCache.get('albums');

	if (!$scope.albums) {
		var request = $http.get('/albums.json');
		request.success(function (response) {
			DataCache.put('albums', response);
			$scope.albums = response;
		});
	}
});


Теперь, при наличии данных в кэше, запрос на получение альбомов не будет выполнен.

Заключение


Данные приемы не претендуют на истину в первой инстанции. Буду рад увидеть в комментариях ваши способы, отличные от предложенных мной.
Поделиться публикацией
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама

Комментарии 32

    +6
    По поводу использования с PHP: просто AngularJS сериализует все данные в json и передает этот json в теле POST запроса, таким образом в PHP мы его можем достать следующим образом:
    PHP код
    $data = json_decode(file_get_contents('php://input'), true);
    



    По поводу контроллеров (и фабрик тоже): если в проекте будет использоваться минификация все скриптов, то описанный вами способ объявления контроллера не переживет эту минификацию, лучше так:
    app.controller('SomeController', ['$scope', 'DataCache', function ($scope, DataCache) {
      ...
    }]);
    

    В этом случае зависимости будут внедрены правильно вне зависимости от того, как обзываются параметры конструктора.

    Все остальное же, ну не знаю, перевод документации?
      +1
      Ну и еще. для array-нотации можно применить утилиту ngmin, которая обернет все за вас.
        0
        есть же ngmin.
        +2
        Вот только начал осваивать Angular и замечаю одну и ту же вещь — редко в каких примерах можно вот так просто взять кусок кода скопировать к себе и будет работать.
        Какие-то везде хитрые связи, что приходится хоть чуть-чуть, но что-то исправлять.
        Я раньше с подобной технологией не сталкивался, поэтому понимание Angular приходит далеко не сразу.
        А в целом, конечно, Angular — классный фреймворк.

        Может кто подскажет как лучше отлаживать сайт на Angular? А то мне Firefox Firebug выдает ошибки в абсолютно непонятном виде.
          +1
          google chrome + надстройка для дебагера. Хотя и консоли фаербага хватить должно.
            +1
            На самом деле, ошибок, которые стоит отлаживать, не так уж много.
            Если на сайте дополнительно используется jQuery(вполне рабочая ситуация при использовании различных плагинов), то можно, например, в хроме сделать такой финт ушами:
            1. Выделить элемент.
            2. В консоли написать магическую строчку $($1).scope()
            3. В консоль напишется значение scope, которая привязана к текущему элементу.

            Почему не стоит использовать Batarang (ИМХО) — сайт начинает глючить, тормозить, и насколько мне известно, еще месяц-два назад были проблемы с утечкой памяти.

            А вообще — Firebug должно хватать. Все ошибки достаточно легко локализуются обычным выключением новых директив с поиском конфликтующей (потому что проблемы возникают с их взаимодействием обыкновенно).

            Из финтов ушами для получения значения внутри scope можно, пожалуй, еще вспомнить такую вещь:
            <pre ng-bind="object_in_scope | 'json'"></pre>
            
              0
              Не совсем правильно выразился. Я имел в виде не отладку, а диагностику ошибок.
              То есть что-то написал, страницу обновляю и в консоли Firebug красный текст с ошибкой на символов 200 из которого ничего не понятно.
              Вот и приходится методом подбора изменять код.
                0
                Ну тут все просто. Основные типы ошибок, которые навскидку вспоминаются:
                1. Не подключен модуль
                2. В параметр директивы с вычислением ('=', '&') передается какая-то фигня.
                3. Stack overflow при вызове вложенных scope.$digest
                Все из этих проблем достаточно легко локализуются и исправляются. Ну хорошо, насчет легко исправляются я все-таки пошутил. =)
                Ошибки легко гуглятся, а если Вам непонятно, из-за чего они возникли — опишите в комментарии. Быть может, я сталкивался с такой проблемой.
                  0
                  Возможно, я не правильно понял, но вы вручную вызываете $digest? Если да, то это плохая практика, если нужно провести какую-то манипуляцию со $scope, в месте, где AngularJS сам за этими манипуляциями не сможет уследить (к примеру $.animate, или еще что-нибудь асинхронное), то стоит использовать $scope.$apply все же.

                  Просто я сам никогда с ошибкой из п.3 никогда не сталкивался, часто бывает Error $scope.$digest already in progress, но не более того :)
                    0
                    Нет, вручную $digest я, слава богу, не вызываю. Пару раз были косяки с вложенными scope.$apply, над которыми я долго думал(как обычно, функция, внутри функции внутри функции). Каюсь, я тоже страдаю криворукостью иногда)
                    Ваша ошибка тоже иногда случается, да)
                    +2
                    Ну вот к примеру,
                    image
                    Как я могу понять какая тут ошибка или я может в принципе что-то не так делаю?
                      +1
                      Инжектор навернулся. Посмотрите, что у Вас с функцией stateProvider, какие проблемы в синтаксисе.
                        0
                        У меня вопрос не по этой конкретной ошибке, а в целом. Почему я не могу получить нормальную диагностику ошибок?
                        Я не сильно хорошо знаю JavaScript и Angular, может это в порядке вещей, тогда буду принимать это как данность. А может я что-то делаю не так.
                        Но почему бы не указать нормально хотя бы строку и файл, где имеются проблемы?
                        Это Angular получается рушиться в процессе выполнения?
                          0
                          Да, в процессе выполнения, потому-то все так грустно. Обычно у него есть перехватчик ошибок, который, впрочем, тоже ловит нечто невразумительное)
                          Как советовали в комментариях ниже — быть может, просто стоит взять 1.2.0 вместо 1.0.8.
                        0
                        А что говорит call stack?
                          0
                          Ну вот почти все, что выдало в консоль и есть ссылка, просто хромовский DevTools делает это нормальной ссылкой, кликабельной :)
                            0
                            Мне очень нравится как обрабатывает ошибки KnockoutJS
                              –1
                              У меня зачастую такой текст указывает на то, что допустил глупую ошибку. Скорее всего где-то опечатка в синтаксисе
                            0
                            А можно узнать какую вы версию AngularJS используете? Т.к. в текущей стабильной версии 1.0.8 да, с этим проблемно, но в, скажем, 1.2.0-rc.* уже все значительно проще, если случается какая-то внутренняя ошибка, то в консоли окажется ссылочка, которая ведет на человеческое описание проблемы :)
                              0
                              Я пользуюсь последней версией 1.2.0. Спасибо всем за разъяснения, я понял, что можно получить более полную информацию по ошибкам, но с чем-то приходится мириться. Пока напрягает не очень сильно и думаю, что с увеличением практических навыков подобные проблемы разрешаться.
                        +1
                        По поводу получения событий. Опишите пожалуйста, рабочую ситуацию, когда это может пригодиться.
                        Приведенный Вами пример включает в себя две неверные вещи при работе с ангуляром:
                        1. Использование angular.element в контроллере, в то время как лучше его использовать в директивах, а лучше вообще не использовать.
                        2. Зачем городить свой ng-class?
                          0
                          Когда делаю директиву-адаптер к jquery плагину, иногда приходится прокидывать $event, чтобы после отправить его в плагин, например.
                          0
                          По поводу StateManager, честно, так и не понял зачем он нужен, почему бы просто один раз не написать Interceptor, а все данные передавать в контроллеры через resolve.
                            0
                            В вашем примере StateManager не очень удобно то, что нужно не забывать удалять процессы из него.
                            Я написал свой велосипед, который пока руки не дошли причесать и выложить на github.

                            В шаблоне пишем:

                            <div ng-loaded="loaded">
                               view content
                            </div>
                            


                            В контроллере (я использую Restangular и ui-router) пишем:

                                var users = Restangular.all('user').getList().then(function(data) {
                                    // do something with response
                                });
                                var products = Restangular.all('product').getList().then(function(data) {
                                    // do something with response
                                });
                                $scope.loaded = loadingService.add(users, products).wait();
                            


                            Код
                            angular.module('loadingService', [])
                            .factory('loadingService', ['$q', '$rootScope', function($q, $rootScope) {
                                var waited = [];
                                $rootScope.$on('$stateChangeStart', function(event, toState, toParams, fromState, fromParams) {
                                    waited = []; // empty array when transition to new state
                                });
                                return {
                                    add: function() {
                                        for(var i in arguments) {
                                            waited.push(arguments[i]);
                                        }
                                        return this;
                                    },
                                    wait: function() {
                                        var ret = $q.defer();
                                        $q.all(waited)['finally'](function() {
                                            waited = [];
                                            ret.resolve(1);
                                        });
                                        return ret.promise;
                                    }
                                };
                            }])
                            .directive('ngLoaded', ['$compile', function($compile) {
                                return {
                                    link: function(scope, element, attrs) {
                                        var indicator = angular.element('<div><img src="assets/loader.gif" /> Loading...</div>');
                                        indicator = $compile(indicator)(scope);
                                        element.after(indicator);
                                        scope.$watch(attrs.ngLoaded, function(newValue, oldValue) {
                                            if(newValue) {
                                                element.css({display: 'block'});
                                                indicator.css({display: 'none'});
                                            } else {
                                                element.css({display: 'none'});
                                                indicator.css({display: 'block'});
                                            }
                                        });
                                    }
                                };
                            }])
                            ;
                            

                              0
                              Мы в m.mamba.ru делаем так:

                              factory('LoadRendererService', ['$rootScope', function LoadRendererService($rootScope) {
                              	function setFlag(isLoaded)
                              	{
                              		$rootScope.controllerDataLoaded = !! isLoaded;
                              	}
                              	function setFlagLoadError(isError)
                              	{
                              		$rootScope.controllerDataLoadError = !! isError;
                              	}
                              
                              	return {
                              		setLoaded: function setLoaded()
                              		{
                              			setFlag(true);
                              		},
                              		setNotLoaded: function setLoaded()
                              		{
                              			setFlag(false);
                              		},
                              		setLoadError: function setLoadError(isError)
                              		{
                              			setFlagLoadError(isError);
                              		}
                              	};
                              }])
                              

                              шаблон:
                              <div class="b-layout" ng-class="{'b-loader': ! controllerDataLoaded && ! controllerDataLoadError}">
                              </div>
                              


                              использование в контроллере:
                              controller('RegisterConfirmCtrl', ['$scope', 'LoadRendererService', 'RegisterConfirmService', function RegisterConfirmCtrl($scope, LoadRendererService, RegisterConfirmService) {
                              	LoadRendererService.setNotLoaded();
                              	RegisterConfirmService.get(function (response) {
                              		LoadRendererService.setLoaded();
                              		onLoad(response);
                              	});
                              }])
                              
                                0
                                Спасибо за статью, а пару месяцев назад, так вообще, цены бы ей не было.
                                Пользуясь случаем, хотелось бы посоветоваться с более опытными коллегами. Насколько приемлемо переносить логику с контроллера в шаблон.? Часто бывает случаи, когда достаточно сделать что-то типа
                                data-ng-click="foo.myParam = !foo.myparam"
                                и вроде бы хорошо, но проект растет и подобных вещей становится как-то слишком много, где-то тернарные операции, где-то присваивание. Может быть лучше даже простые операции оставлять в контроллере, ради чистоты шаблона.? Но и контроллер с кучей функций как-то не айс :)
                                  0
                                  Ну обычно эта логика относится непосредственно к представлению, так что это наоборот нормально. А бизнес логика должна выноситься в сервисы.
                                    0
                                    Если эта логика относится только к шаблону, то лучше бы её завернуть в директиву, там же не просто присвоение, есть какое-то поведение у DOM-а. Заодно можно будет один раз протестировать и потом повторно использовать. Если присвоение относится к бизнес-логике — то переместить в контроллер.
                                  0
                                  По поводу PHP. Зря переводил что ли habrahabr.ru/post/181009/ :-)
                                    0
                                    Опять же, передавать с запросами content-type: application/json, и на стороне сервера все такие запросы предварительно десериализовать.

                                    p.s. каюсь, прочел только первый абзац статьи на момент комментария.
                                    +1
                                    ng-cloak можно использовать как css класс и если для него иметь определение в своих стилях, то сходу не очень понятно, какие там проблемы в «большинстве» случаев.

                                    Менеджер состояний выглядит неоднозначно. Для простейших случаев есть pendingRequests. В остальных случаях это должно архитектурно решаться, а не ручным добавлением, удалением.

                                    Получение событий — тот случай, когда лучше бы «всякое» не случалось.

                                    $cacheFactory — может быть им пренебрегают, потому что простейший кэш — это все же не бином Ньютона. А вот то, что $cacheFactory в AngularJS — это LRU кэш (вытеснение давно неиспользуемых) из документации можно сходу и не понять.
                                      +1
                                      Как то раз мне внезапно захотелось, что бы сайт после загрузки появлялся плавно (с использованием css transition). С использованием ng-cloak этого не получилось, поэтому сделал вот так:

                                      <div class="wrapper" ng-class="{smoothOpacity:true}">
                                          ...
                                      </div>
                                      


                                      И прописал стили:
                                      .wrapper {
                                          opacity: 0;
                                          -moz-transition: opacity 0.6s ease;
                                          -o-transition: opacity 0.6s ease;
                                          -webkit-transition: opacity 0.6s ease;
                                          transition: opacity 0.6s ease;
                                      }
                                      
                                      .smoothOpacity {
                                          opacity: 1;
                                      }
                                      


                                      Что получилось? ng-class начинает работать только после того как Angular полностью загрузился, контент появляется плавно и довольно симпатично, все довольны. За это решение спасибо StackOverFlow.

                                      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                      Самое читаемое