Разработка

AngularJS: сервисы, фабрики, провайдеры

Чтоже это за странные service, factory и provider. Казалось бы — выполняют почти одну и туже функцию, подключаются через DI. Зачем их так много?

На абстрактный пример можно глянуть тут (к слову, автор привел очень хороший пример).

Но никто не рассказывает, зачем оно в реальной практике нужно.

Сервис

Он чаще всего используется для создания разделяемого ресурса.

var constructor = function constructor() {
    this.foo = 'foo'
    this.bar = function bar() {
        
    }
}

.service('someService', constructor)

Когда сервис создается первый раз, то выполняется

var someServiceImpl = new constructor()

И в дальнейшем при подключении зависимостей через DI в объекты будет всегда передаваться someServiceImpl.

Фабрика
это более продвинутая конструкция. Она не является разделяемым ресурсом сама по себе. При каждом использовании фабрики через DI ее значение — это результат вызова фабричной функции.

var factoryFn = function () {
    var someClass = function() {}
    
    return someClass;
}

.factory('someFactory', factoryFn)

При первом инстанцировании фабрики будет вызвана функция factoryFn и ее результат — это уже и есть разделяемое между всеми участниками системы значение.

Свое применения фабрика находит в первую очередь для реализации моделей. Как в примере выше. Создается и описывается класс модели someClass и фабрика при инстанцировании возвращает ссылку на него. В последующем можно использовать этот класс как конструктор объектов.

.controller('someController', ['someFactory', function(someFactory) {
    // Тут мы успешно инстанцируем новый объект someClass
    var someClassImpl = new someFactory();
}])

Провайдер
Это тоже фабрика. Но более продвинутаця. Так же точно при первом инстанцировании провайдера выполняется некая функция и ее результат становится разделяемым значением.

Но вся мощь провайдеров в том, что их можно конфигурировать (в отличие от фабрик или сервисов). Т.е. на этапе инициализации вашего приложения можно передать провайдеру некоторые опции, которые нельзя указать в дальнейшем.

Как пример — библиотека для oauth-авторизации. на этапе конфигурирования в нее передаются токены.

var providerFn = function() {
    var someModel = function() {
        // это модель
    }
    
    this.configure = function() {
        // делаем какую-то конфигурацию
    }
    
    this.$get = function() {
        return new someModel()
    }
}

.provider('someProvider', providerFn)

И теперь самое интересное: при подключении провайдера через DI в качестве разделяемого объекта будет выступать то, что вернула функция $get.

В то же время на этапе конфигурирования нам доступен сам инстанс providerFn и мы можем использовать его функционал по конфигурированияю

.config(['someProvider', function(someProvider) {
    //...
    someProvider.configure(options)
}])
Разработка

Google JS API для AngularJS

Написал небольшой враппер к гугловому апи для использования совместно с ангуляром. Взять можно на гитхабе.

Или в bower

 $ bower install angularjs-gapi

Поключение

angular.module('app', ['gapi']).config(['gapiProvider', '$routeProvider', function(gapiProvider, $routeProvider) {
   gapiProvider.apiKey(YOU_API_KEY) // апи-ключ можно создать в консоли разработчика
        .clientId(YOU_APP_CLIENT_ID) // берем в консоли разработчика
        .apiScope(SCOPES_FOR_APP); // скоупы, которые нужны для работы приложения
}])

Если вам не требуется работать с пользовательскими данными и не требуется разрешение пользователя, то достаточно использования только YOU_API_KEY.

Авторизация

angular.module('app').controller('tstController', ['$scope', 'gapi', function($scope, gapi) {
  gapi.login().then(function() {
    $scope.login = 'success';
  }, function() {
    $scope.login = 'fail';
  });
}])

Выполнение запросов не требующих авторизации

angular.module('app').controller('tstController', ['$scope', 'gapi', function($scope, gapi) {
  // we can't make requests while api is not ready
  if (gapi.isApiReady()) {
    gapi.call("youtube", "v3", "search", "list", {
      query: "search term"
      part: "snippet"
      type: "video"
    }).then(function(response) {
      // work with response
    })
  }
}]);

Выполнение запросов требующих авторизации

angular.module('app').controller('tstController', ['$scope', 'gapi', function($scope, gapi) {
  // we can't make requests while api is not ready and user is not logged in
  if (gapi.isApiReady() && gapi.isLoggedIn()) {
      gapi.call("youtube", "v3", "playlists", "list", {
      part: "snippet",
      type: "video";
    }).then(function(response) {
      // work with response
    })
  }
}]);

Вроде все.

Разработка

Несколько советов по оптимизации приложений на Angular JS

Что нам потребуется.

AngularJS

Версия < 1.3

В версии 1.3 появились существенные отличия. Включая однонаправленный биндинг — :: (два двоеточия), но она несовместима с bindonce.

Хотя по скорости ничем не уступает оптимизированному варианту. Если вы используете >= 1.3, то применяйте готовый однонаправленный биндинг вместо bo-*

Bindonce

url: https://github.com/Pasvaz/bindonce

Эта библиотека позволяет делать однонаправленный биндинг данных в шаблоны.
Если вам не требуется, чтобы какая-то переменная после своего отображения меняла значение (т.е. она не будет его менять в процессе работы), то это однозначно для вас.

Например у нас есть список книг, который приходит с сервера и обновляется целиком (опять же с сервера).
Что мы делаем в коде

<div ng-repeat="book in books"><span>{{book.name}}</span><div>

Имя книги не меняется и мы можем записать так

<div ng-repeat="book in books"><span bo-text="book.name"></span><div>

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

Однако, если вы используете sly-repeat (ниже), то его нельзя сочетать с bindonce из-за агрессивного кеширования дерева дом этим самым слайрепитом.

Scalyr

url: https://github.com/scalyr/angular

post: http://blog.scalyr.com/2013/10/31/angularjs-1200ms-to-35ms/

Эта библиотека позволит вам минимизировать количество одновременно работающих вотчеров путем отключения ненужных. А так же реализовать умный ng-repeat с кешированием созданных dom-элементов (который, увы, не сильно дружит с bindonce. т.е. не дружит вовсе).

Оптимизации

Много скрытых элементов на странице

Это одна из ключевых оптимизаций для проекта. Так как нам часто приходится скрывать/показывать разные элементы.

Представим, что у нас есть какой-то код типа нижеследующего.

<div ng-show="condition"><!-- а тут много разных строчек с данными, которые выводятся на базе данных из ангуляра --></div>

Если не предпринять дополнительных действий, то все то, что написано внутри скрытого дива будет выполняться (!), а это лишние такты процессора и нервные подергивания пользователя.

Что же мы можем сделать?

На помощь нам приходит sly-show+sly-prevent-evaluation-when-hidden. Эта убойная комбинация не будет запускать интерпретацию скрытых элементов.

<div sly-show="condition" sly-prevent-evaluation-when-hidden><!-- а тут много разных строчек с данными, которые выводятся на базе данных из ангуляра --></div>

Переписав код таким образом мы получим тот самый профит: снижение нагрузки на клиент-сайд.

Но стоит заметить одну особенность директивы: она в своем решении интерпретировать или нет вложенный код опирается на наличие класса ng-hide. При инициализации приложения или перерисовке элементов этого класса нет. Поэтому мы можем увидеть что-то вроде этого:

Trace angular application

Причина проста: у элементов при инициализации еще нет класса ng-hide. Он появится только после того, как отработают нужны контроллеры, а значит, что отрисовка элемента с нуля всегда будет отрисовывать и интерпретировать все скрываемые элементы (даже если они действительно скрыты).

Решение проблемы: принудительно добавить класс ng-hide всем элементам, которые находятся под связкой sly-show+sly-prevent-evaluation-when-hidden.

<div class="ng-hide"><!-- а тут много разных строчек с данными, которые выводятся на базе данных из ангуляра --></div>

На этом оптимизация неиспользуемого кода закончена.

Оптимизация биндингов

Под этим подразумевается однократное добавление в шаблон переменной.

Как в примере bindonce с книгами (выше) нам может не требоваться функционал двунаправленного отображения.

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

{{var}} -> <div bo-text=”var”></div>

Путь оптимизации: вместо элемента создается контейнер, на который выполняется отображение текста элемента. Так как в биндванс не существует шаблонной конструкции для биндинга. Естественно, что там может быть любой тег и не обязательно div. Самое главное — наличие контейнера.

ng-href="text{{var}}" -> bo-href="’text’ + var"

А так мы оптимизировали ссылки.
Важно заметить, что ng-* и bo-* работают с биндингами различным образом: если первый работает по внутриангуляровским правилам интепретации, то второй просто выполняет .eval() на строку. Поэтому в bo-* мы работаем с обычными строками и переменными.

Аналогичным образом работаем и с другими атрибутами. Если есть аналог биндинга.

Если аналога нет, то используем конструкцию bo-attr-<имя кастомного атрибута>

<div bo-attr bo-attr-isbn="’{{book.isbn}}’"></div>

Здесь кроется еще одна важная особенность обработки атрибутов библиотекой: они рассматриваются как строки. И если в атрибуте текст, что его надо представить в виде текста (то же характерно для любого юиндига bo-*). Мы привыкли чаще всего работать с числами, но не стоит забывать, что есть еще буквы.

Поэтому если не уверены в содержимом переменной — пишите строковое предствление: ‘{{var}}’, а не var.

Вложенность элементов

На картинке профайлера (выше) вы можете увидеть гребенку из кучи вложенных вызовов функций. Если мы посмотрим на эти вывовы под микроскопом отладчиком, то увидим, что это рекурсивный обход дерева dom.

Чем меньше у вас уровней вложенности, тем лучше. Особенно это касается кода, который расположен в циклах.

Так что старайтесь сократить вложенности.

Дополнительная оптимизация при обработке скрытых элементов

У sly-prevent-evaluation-when-hidden есть одна большая проблема: она опирается только на наличие класса ng-hide, что не совсем уместно если видимость элемента контролируется каким-то другим классом.

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

/**
* Extended version of slyPreventEvaluationWhenHidden from scalyr.
* This version prevent evaluation not only when element has class ng-hide.
* If element is hidden with "display: none" evaluation is prevented.
*/
angular.module('sly')
 .directive('slyPreventEvaluationWhenHidden', function () {
   return {
     restrict: 'A',
     // We create a new scope just because it helps segment the gated watchers
     // from the parent scope.  Unclear if this is that important for perf.
     scope: true,
     compile: function compile(tElement, tAttrs) {
       return {
         // We need a separate pre-link function because we want to modify the scope before any of the
         // children are passed it.
         pre: function preLink(scope, element, attrs) {
           scope.$addWatcherGate(function hiddenChecker() {
             // Should only return true if the element is not hidden.
             return !element.hasClass('ng-hide') && element.is(":visible") ;
           }, function hiddenDecider(watchExpression, listener, equality, directiveName) {
             // Make an exception for slyShow.. do not gate its watcher.
             if (isDefined(directiveName) && (directiveName == 'slyShow'))
               return false;
             return true;
           });
         },
       };
     },
   };
 });
Разработка, HowTo

AngularJS: простой прогресс-бар

AngularJS: progressbarАнгуляр — это очень удобная штука. Но еще удобнее, когда она нормально скрещивается с другими фреймворками вроде jQueryUI.
В AngularUI я что-то прогресс-бара не нашел (знаю, искал плохо). Накропал сам.

Фидл с примером работы и исходниками.

JFF

AngularJS: забавная особенность bindonce

Для AngularJS существует модуль bindonce, который позволяет сократить количество вотчеров и тем самым ускорить страинцу.

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

Однако, у этой директивы есть забавное поведение, которое связано с особенностями интерпретации.

$scope.title = 'some text with $peci@l chars'
$scope.title_ref = 'title'
$scope.title_title_ref = 'title_ref'
<a bo-attr="" bo-attr-title="title">anchor1</a>
<a bo-attr="" bo-attr-title="{{title_ref}}">anchor2</a>
<a bo-attr="" bo-attr-title="'{{title}}'">anchor3</a>
<a bo-attr="" bo-attr-title="{{title_title_ref}}">anchor4</a>

Как думаете, что выведется в каждом случае? 🙂

Фидл с примером.

Разработка

AngularJS: реагируем на изменение состояния объекта

Допустим У нас есть часть шаблона, которая может быть либо отображена, либо скрыта. Нам нужно правильно реагировать на это и рассылать оповещения в скоуп (надо так).

Тогда мы просто берем и начинаем следить за состоянием нужного элемента.

angular.module('app')directive('watchState', function($rootScope) {
    return {
        restrict: 'A',
        controller: function($scope, $element) {
            // show. that state was changed by outer source.
            // prevent action when no changes in $digest cycle
            var toggled = false

            $scope.$watch(function() {
                if ($element.hasClass('ng-hide')) {
                    if (!toggled) {
                        toggled = true
                        $rootScope.$broadcast('log', 'text is hidden')
                    }
                } else {
                    if (toggled) {
                        toggled = false
                        $rootScope.$broadcast('log', 'text is visible')
                    }
                }
            })
        }
    }
})

Теперь мы можем следить за состоянием отображения любого элемента.

<p ng-hide="hidden" watch-state>some text</p>

Пример на jsFiddle