Всем привет!

В нашей компании, помимо разработки собственной СУБД, также занимаются заказными разработками по самым разным направлениям, от суровых java-энтерпрайз приложений до небольших мобильных приложений. Наши команды стараются использовать передовые технологии и фреймворки. И как раз я являюсь представителем одной из таких команд. Сегодня хочу поделится опытом разработки на AngularJS и парой мыслей о проектировании веб приложения с использованием этого фреймворка.




За время, которое я занимаюсь разработкой, мне приходилось сталкиваться с различными подходами к написанию приложений. Кто-то оборачивает простые вещи в очень странные обертки, так что автору кода приходится в дальнейшем прибегать к «комплементарному декаплингу эксплицируемых компонент» (с). Есть люди, которые, наоборот, нисколько не заморачиваются с архитектурными изысками и пишут код «здесь и сейчас», не заботясь о дальнейшем сопровождении и психическом здоровье коллег. Мне кажется, что код всё же должен быть в меру наполнен абстракциями, и при этом легко делиться на логические модули и легко читаться. Знакомство с AngularJS пару лет назад принесло понимание, как это может неплохо выглядеть в javascript'е.

Требования к приложению


Можно много спорить о достоинствах и недостатках AngularJS, оставим эти споры за рамками заметки, остановимся только на главном вопросе — можно ли использовать AngularJS в серьезном приложении?

С одной стороны, фреймворк накладывает некоторые ограничения на структуру приложения и вводит свои собственные термины для ее описания. Другая же структура и подходы многих ставят в тупик, но это, на мой взгляд, спорный минус, поскольку любой фреймворк, решающий такие масштабные задачи, так или иначе подгоняет под себя архитектуру. Написать приложение на одном фреймворке, а потом с легкостью перенести на другой — это типичная “программистская утопия”. Поэтому аргументы по этому пункту многих коллег мне кажутся сильно надуманными.

С другой стороны находится производительность как краеугольный камень любого одностраничного веб-приложения. Во многом производительность AngularJS зависит от того, насколько глубоко вы в него погрузились и насколько правильно используете те или иные компоненты фреймворка. Это не скрывают и создатели AngularJS и честно рассказывают, как все устроено внутри и как можно избежать проблем со скоростью. Главная суть в том, что фреймворк — это инструмент, а каждому инструменту подходит какой-то определенный тип задач.

Итак, давайте посмотрим, какие задачи AngularJS не поможет нам решить:
  1. У вас приложение с большим количеством (тысячи) элементов, которые постоянно добавляются, удаляются и перемещаются на одной странице. Это может быть, например, игра или анимационное приложение.
  2. Ваше приложение оперирует на клиентской части большим количеством “сырых” данных — постоянно их преобразует, что вынуждает изменять модели и соответственно перерисовывать их отображение.
  3. У вас есть готовый код, написанный, например, при помощи JQuery и не отличающийся грамотностью, т.е. представляет собой попросту говоря “лапшу”. Приведение такого кода в соответствие с концепциями AngularJS может быть слишком трудоемко.

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

Проектирование веб-приложения.


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

Для начала необходимо разбить весь наш функционал на модули и разграничить ответственность компонентов. На практике это сводится к построению сервисов, директив и контроллеров, а также объединению их в модули. Давайте разберемся, что означает каждая из этих базовых сущностей.
  • Модуль — контейнер, который хранит в себе компоненты, решающие одну или несколько задач.
  • Сервис — компонент, который хранит в себе переиспользуемый код или объект и позволяет выделять общую логику для работы других компонентов. Это могут быть операции над объектом, хранилища данных, кэш и пр.
  • Директива — компонент, который представляет собой переиспользуемый виджет или специфичный код для работы с DOM-деревом браузера и стилями.
  • Контроллер — компонент, содержащий специфичную логику (в т.ч. и UI логику) для работы конкретной страницы или ее части.

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

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

Приведу несколько полученных опытным путем рекомендаций, как можно эффективнее строить взаимодействие наших компонентов.

  1. Постоянно следите за кодом и не бойтесь его рефакторить. Мне приходится часто сталкиваться с мнением “работает — не трогай”, но такой подход не ведет к развитию проекта и со временем в модуле начинает накапливаться код, который становится неуправляемым. Примером может послужить один сложный контроллер главной страницы, который рано или поздно разбухнет до стадии “чтобы исправить баг мне нужно очень много часов”.
  2. Если у вас сложная директива, которая требует постоянного взаимодействия с бизнес-логикой (например, интерактивная карта с метками и геолокацией), создайте к ней свой сервис. Этот сервис будет работать как интерфейс, который можно передать в другие сервисы или контроллеры, что значительно упростит код и инкапсулирует логику взаимодействия.
    Пример
    angular.module('googleMap', [])
        // наш сервис (интерфейс)
        .factory('GoogleMapService', function () {
            var mapInstance;
            return {
                currentLocation: {
                    lat: "",
                    lng: ""
                },
                createMapInstance: function (mapNode, options) {
                    mapInstance = {
                        map: new google.maps.Map(mapNode, options.mapOptions),
                        geocoder: angular.isDefined(options.geocoder) ? new google.maps.Geocoder() : undefined
                    };
                    return mapInstance.map;
                },
                setCenter: function (lat, lng) {
                    mapInstance.map.setCenter(lat, lng);
                },
                setZoom: function (value) {
                    mapInstance.map.setZoom(value);
                },
                setCurrentLocation: function (lat, lng) {
                    this.currentLocation.lat = lat;
                    this.currentLocation.lng = lng;
                    // ... логика установки
                },
                getCurrentLocation: function () {
                    return this.currentLocation.lat != "" && this.currentLocation.lng != "" ? new google.maps.LatLng(this.currentLocation.lat, this.currentLocation.lng) : undefined;
                },
                geocodeLocation: function (lat, lng, callbackSuccess, callbackError) {
                    if (angular.isDefined(mapInstance.geocoder)) {
                        mapInstance.geocoder.geocode({location: new google.maps.LatLng(lat, lng)}, function (results, status) {
                            if (status == google.maps.GeocoderStatus.OK) {
                                callbackSuccess(results[0] ? results[0].formatted_address : '');
                            } else {
                                if (status === 'OVER_QUERY_LIMIT') {
                                    callbackError('Exceeded the map usage limits per second');
                                }
                            }
                        })
                    }
                },
                destroy: function () {
                   // ... уничтожаем карту
                }
                // другие методы
            }
        })
        // наша директива
        .directive('googleMap', ['GoogleMapService', function (GoogleMapService) {
            return function (scope, element, attrs) {
                //map options
                var mapOptions = {
                    zoom: angular.isDefined(GoogleMapService.getCurrentLocation()) ? 10 : 4,
                    center: angular.isDefined(GoogleMapService.getCurrentLocation()) ? GoogleMapService.getCurrentLocation() : new google.maps.LatLng(39.164141, -102.304687),
                    mapTypeId: google.maps.MapTypeId.ROADMAP
    
                };
    
                //Google API map object
                var map = GoogleMapService.createMapInstance(element[0], {
                    mapOptions: mapOptions,
                    autocomplete: attrs.autocomplete,
                    geocoder: attrs.geocoder
                });
    
                scope.$on('$destroy', function () {
                    GoogleMapService.destroy();
                });
    
                // остальная визуальная логика по карте
            }
        }]);
    
    // пример использования
    angular.module('someModule', [])
        .controller('$scope', 'GoogleMapService',
        function ($scope, GoogleMapService) {
            GoogleMapService.setCurrentLocation($scope.lat, $scope.lng);
            GoogleMapService.geocodeLocation($scope.lat, $scope.lng, function (result) {
                $scope.address = result;
            }, function (errorMessage) {
                $scope.errorMessage = errorMessage;
            });
            GoogleMapService.setZoom(10);
        });
    
    ...
    
    <div id="map" class="map" google-map autocomplete="location" geocoder></div>
    <input id="location"  type="text" placeholder="Please input place name or click on the map">
                  


  3. Активно используйте внутренний контроллер директивы для сокрытия логики работы вашего виджета. Вместо внешнего управления директивой, попробуйте убрать все “потроха” внутрь.
  4. Избегайте управления DOM-деревом напрямую в контроллере. Иногда это кажется проще, чем написать отдельную директиву, однако для получения структурированного кода необходимо следовать этой рекомендации. К тому же, сам AngularJS “из коробки” предоставляет большое количество готовых мини-директив, помогающих в задачах вроде “показать элемент, если… ” и “добавить класс, если… ”
  5. С умом используйте двойное связывание, а там где это возможно, используйте одностороннее связывание. Переизбыток отслеживаемых переменных может привести к падению скорости. Если у вас не предполагается изменение переменной, используйте одностороннее связывание
  6. Если по какой-либо причине прямая связь между контроллером, сервисом и директивой невозможна, используйте паттерн “Наблюдатель” (википедия и шина событий в AngularJS). Тем не менее, важно этим не злоупотреблять, потому что зарегистрировать и получить событие можно в каждом контексте ($scope) любого контроллера. Избыток таких конструкций усложняет понимание и отладку кода. Для глобальных системных событий вместо использования $broadcast в конкретном контексте лучше подписывать события на $rootScope и иницииривать на нем же через $rootScope.$emit. (спасибо serf за дополнение)
  7. Избегайте использования корневого контекста ($rootScope), старайтесь изолировать логику внутри одного контроллера или связки контроллер-сервис. Корневой контекст работает на всё приложение целиком, поэтому, например, добавленные туда функции отслеживания ($watch) будут срабатывать при каждом цикле ($digest), когда изменяется переменная в разных местах приложения.
  8. Разберитесь (если еще не разобрались) и используйте механизм promise (“обещаний”). Это простой и наглядный способ избавиться от спагетти-кода при вызове асинхронных функций. Также, одним из интересных применений “обещаний” является сообщение дочерним контроллерам о выполнении асинхронных запросов (через механизм наследования контекста).
    Пример
    <div ng-controller="Controller1">
        <div ng-controller="Controller2"></div>
    </div>
    
    ...
    
    function Controller1($scope, DataLoaderService) {
        ...
        // данные загружаются асинхронно
        $scope.dataLoaded = DataLoaderService.get(...);
        ...
    }
    ...
    function Controller2($scope, DataLoaderService) {
         ...
        $scope.dataLoaded.then(function(result) {
          // вызов функции произойдет когда завершится запрос из Controller1
            ...
        });
        ...
    }
                   




Немного полезных инструментов.


Помимо рекомендаций, хотелось бы поделиться полезными инструментами для построения эффективного процесса отладки и разработки. В своей работе мы активно используем:
  1. WebStorm IDE. Думаю, IDE не нуждается в представлении, простая и очень удобная в использовании среда от ребят из JetBrains. Поддержка AngularJS из коробки, включая автоподстановку.
  2. JSDoc 3. Документация на проекте является важным фактором его успешности, поскольку хорошо документированный код проще поддерживать. Уже давно действует стандарт написания документации к javascript — JSDoc — и его можно использовать для документирования кода вашего AngularJS приложения. Для генерации красивых html страничек можно использовать специальный генератор, он прост и требует только Node.js.
  3. Jasmine. Код, написанный на javascript, можно и нужно тестировать. Unit тестирование возможно и в AngularJS, при помощи фреймфорка Jasmine и “запускатора” Karma. Опять же вам потребуется Node.js, а настройка всего окружения не должна отнять много времени и подробно описана у каждого инструмента.
  4. Closure Compiler. Для ускорения загрузки код можно минифицировать с помощью javascript компилятора, а в некоторых местах и оптимизировать. Для AngularJS отлично подходит Closure Compiler (к слову сам AngularJS собирается им же). Отличный гайд как собрать ваше приложение лежит тут. От себя лишь добавлю, что ваше приложение, увы, не заработает в режиме ADVANCED_OPTIMIZATIONS.
  5. ng-annotate. Дополнительный модуль для Node.js, который позволяет автоматически добавлять в код зависимости для инъекций. В результате, можно избавиться от лишнего кода. (спасибо anotherpit за дополнение)
    Пример
    // Обычный код, так объявляется контроллер в AngularJS
    angular.module("MyMod").controller("MyCtrl", ["$scope", "$timeout", function($scope, $timeout) {}]);
    
    // С помощью ng-annotate можно писать так. После прогона через дополнение, код станет таким же, как и выше.
    angular.module("MyMod").controller("MyCtrl", function($scope, $timeout) {});
    




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

Спасибо за внимание!

P.S. Заметка об Angular 1, я специально не стал упоминать Angular 2, поскольку он еще находится в глубокой альфе, и пока применять его в реальных приложениях не рекомендуют сами разработчики.

Комментарии (23)


  1. enleur
    15.07.2015 12:06
    +1

    Реально ли использовать Angular вместе с рендерингом на бекенде?


    1. aikixd
      15.07.2015 12:14
      +3

      На mvc я сделал общий view, в котором есть ng-include. Все запросы к любой странице выдают этот view, а он загружает html темплейты в этот ng-include в зависимости от адресной строки. В Angular $location позволяет работать с адресной строкой не перегружая страницу. Такая себе single page app.
      Если интересно могу показать код чуть позже, сейчас времени нет.


      1. aikixd
        15.07.2015 17:31

        Расценю плюс как интерес.
        Вобщем рецепт:
        Делаете общий view и добавляете в него подобный код

        <content ng-controller="ContentTemplate">
            <div ng-include="contentUrl"></div>
        </content>
        


        В контроллере добавляете вот этот код

        $scope.$watch(function () { return $location.path(); }, function (path) {
            var match = path.match(/^\/?([^/?]+)(?:\/([^/?]+))?/i);
        
            if (match == null) {
                $scope.contentUrl = "Views/Default/Index.html";
                return;
            }
        
            if (match[2] == null) {
                match[2] = "Index";
            }
        
            $scope.contentUrl = "/Views/{0}/{1}.html".format(match[1], match[2]);
        });
        


        Суть такова: при загрузке любой страницы, в контроллере сервера вы указываете всегда возвращать один и тот же view. Когда он загружается $watch запускается в первый раз, смотрит на адрес, делает простой роутинг и пишет в contentUrl новое значение, которое подхватывает ng-include и загружает новый контент.

        Что-бы перейти на другую страницу делаете так:

        $location.path("/Controller[/Action]");
        

        $location по умолчанию не перегружает страницу, а только меняет адресную строку. Если браузер не поддерживает такое поведение, то он пишет это как хеш ссылку (address.org#/controller/action) и все тоже работает.


    1. DigitalSmile Автор
      15.07.2015 12:18

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

      Если имелось в виду рендеринг для поисковиков, то тут есть два пути:

      • Использовать prerender.io
      • Генерить статические странички «на лету» или складировать их заранее в кеш.

      Мы пытались пойти по первому пути и долго бились, чтобы все заработало как надо. Мы использовали обертку на node.js, но, к сожалению, сам сервер постоянно падал после нескольких десятков обращений. Поэтому мы переключились на второй вариант, на котором и остановились.


      1. SeriousDron
        16.07.2015 09:01

        А чем второй путь отличается от первого? Чем вы рендерите во втором способе? Собственно вопрос-то в том как не держать второй комплект шаблонов и т.п. на серверной стороне.


        1. DigitalSmile Автор
          16.07.2015 09:14

          Под капотом у prerender'a находится PhantomJS. Он в real-time сходит на страницу и на выходите даст сформированный html с выполненным javascript'ом.

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

          Можно немного расширить второй путь и формировать html'ки заранее, складывая их в какой-нибудь кеш. Но тут все зависит от объема данных и насколько актуальными их надо держать. Нам это не подошло, данные могли меняться быстро, а объем позволял их формировать «на лету» без потери производительности.


  1. serf
    15.07.2015 17:29

    Тем не менее, важно этим не злоупотреблять, потому что зарегистрировать и получить событие можно в каждом контексте ($scope) любого контроллера.

    Добавлю что например для глобальных событий вместо использования $broadcast лучше подписывать события на $rootScope и иницииривать на нем же через $rootScope.$emit. И вообще как по мне оыбчно достаточно глобальны событий, иначе может возникнуть лишняя сложность.


    1. DigitalSmile Автор
      16.07.2015 09:03

      Спасибо, верное замечание, дополню рекомендацию.


  1. SVVer
    15.07.2015 19:26
    +1

    Интересно было бы узнать о применении AngularJs для разработки Web-приложений, работающих в браузере мобильных устройств. Как в этом случае трансформируются рекомендуемые ограничения по количеству watch-ей на странице? Было бы здорово еще и узнать о каких-нибудь примерах из реального опыта.


    1. DigitalSmile Автор
      16.07.2015 09:31
      +2

      Наше приложение работало также и в мобильных браузерах, и надо отметить это было нашей ошибкой. Вкратце:

      1. Производительность раза в 2-3 (субъективно) хуже, чем на десктопе. У нас использовалась masonry-плитки с картинками.
      2. Пришлось в одном приложении адаптировать пользовательский интерфейс одновременно и для мобильных устройств, и для десктопа. Это вылилось в большую кучу «костылей» и ветвлений в коде, «резиновая» верстка полностью не покрывала все случаи. Чудес, увы, не бывает.
      3. Было очень много багов, связанных с конкретными браузерами на устройствах. Поддерживать их все было очень сложно. Бывали даже случаи, когда один и тот же баг в одной и той же версии браузера повторялся на устройствах Samsung, но упорно не хотел на устройствах LG (Nexus)


      Сейчас, мы бы разделили эти приложения на два разных с точки зрения UI части, но объединили бы логическую часть (сервисы) через Ionic. Это было бы правильнее, на мой взгляд.


      1. SVVer
        16.07.2015 09:48

        Но ведь эти проблемы связаны не непосредственно с AngularJs, я правильно понимаю? Т.е. понятно, что резиновый интерфейс не всегда позволяет сделать интерфейс удобным для всех форм-факторов, а косяки с разметкой могут быть разные в разных браузерах. Но если все-таки этот путь выбран (хотя бы как временный), то как себя будет вести AngularJs со своим двусторонним связыванием в мобильном браузере? В аспекте производительности и в аспекте совместимости AngularJs с разными браузерами…


        1. DigitalSmile Автор
          16.07.2015 10:10
          +1

          Да, Вы правы, эти проблемы напрямую не относятся к AngularJS. Не могу сказать, что фреймворк накладывал какие-то ограничения на использование в мобильных браузерах конкретно у нас.
          Сложновато отделить проблемы с производительностью двойного связывания от наведенных проблем другими компонентами. У нас показывалось примерно до 200 «плиток» на странице (может быть и больше удавалось показывать за счет бесконечной прокрутки, сложно сказать) и на каждой порядка 15 элементов были с двойным связыванием. Хуже всех себя показывал нативный Android браузер. Chrome, Safari и Firefox были примерно на одном уровне. Были небольшие тормоза при прокрутке и «затыки» при добавлении новых элементов, вероятно связанные в большей степени из-за манипуляций с расчетом позиций этих «плиток».
          В целом, не могу сказать что производительность страдала конкретно из-за AngularJS. При разумном применении (не на синтетическом примере в 2000 $watch), я думаю скорость не сильно упадет.

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


          1. SVVer
            16.07.2015 10:16

            Спасибо!
            Еще попутно вопрос по структурированию кода приложения: где, по Вашему мнению, лучше размещать логику построения (перестроения) DOM, общую для нескольких директив? Ведь наследования директив нормальными способами не добиться?


            1. DigitalSmile Автор
              16.07.2015 10:33

              А можете привести пример такой логики? Не встречал такой, если честно…
              Наследования как такового конечно же нет, но есть очень полезная штука require. С ее помощью можно логически объединять директивы, зависящие друг от друга. Например, если Вы захотели сделать собственный dropdown, то можно директиву разбить на две части. Одна часть будет связана с логикой «навешивания» на элемент (и таких директив может быть несколько, в зависимости от элемента, например), а вторая часть будет реализовывать показ непосредственно панели со списком.
              Такой подход позволит использовать общую часть (панель со списком) с разными элементами и директивами (меню или инпут с селектом).


              1. SVVer
                17.07.2015 13:26

                Например, я хочу несколько директив, которые по сути своей dropdown-контролы, но содержимое выпадающей области разное: в одном случае простой список, в другом — какая-то более ложная конструкция, позволяющая искать/выбирать элементы. В силу того, что большую часть времени я пишу на языке с наследованием, мне описанное разнообразие хочется сделать с помощью соответствующих классов-наследников. Хотя, require, похоже тоже для этого подойдет.


  1. lazant
    15.07.2015 23:43

    Насколько Angular подходит для написания приложения с не очень сложной svg графикой в дуэте со Snap.svg, например? Или для подобных приложений он не нужен вовсе?


    1. DigitalSmile Автор
      16.07.2015 09:34

      Хотя у меня и не было опыта работы с Snap.svg, но мне кажется, если у Вас просто манипуляция без сложной логики, то Angular тут будет не нужен.


  1. anotherpit
    16.07.2015 02:50

    Избавиться от конструкций вида

    ['GoogleMapService', function (GoogleMapService) {

    поможет ng-annotate: github.com/olov/ng-annotate


    1. DigitalSmile Автор
      16.07.2015 09:18
      +1

      На момент старта нашего проекта, такой штуки не было, и поэтому все привыкли описывать зависимости инъекций руками.
      Добавлю в полезные инструменты.


    1. serf
      16.07.2015 10:13

      В целом такой подход (анонимные функции) объявления модулей вообще не рекоммендуется. Лучше объявлять именованные функции и их зависимости через $inject или ng-annotate (делает тот же $inject). В моем случае все моудли разбиты на отдельные файлы и линкуются через browserify, именем модуля является имя функции, довольно удобно.


      1. DigitalSmile Автор
        16.07.2015 10:24

        В целом такой подход (анонимные функции) объявления модулей вообще не рекоммендуется.

        А почему не рекомендуется? Не встречал такого мнения…

        В моем случае все моудли разбиты на отдельные файлы и линкуются через browserify, именем модуля является имя функции, довольно удобно.

        У Вас в одном файле собраны все сервисы, контроллеры и директивы, связанные с модулем?
        Мы разбивали каждую отдельную сущность на отдельный файл. Структура слегка «пухла», но в целом было удобнее вносить точечные правки и рефакторить файл, было меньше конфликтов.


        1. serf
          16.07.2015 10:33

          А почему не рекомендуется? Не встречал такого мнения…

          Хотя бы потому что анонимную функцию не получится вынести в отдальный файл, что желательно как по мне. Проще читается (особенно когда все разнесено по отдельным файлам). Отлаживать проше и прочее. Здесь еще можно почитать github.com/johnpapa/angular-styleguide

          У Вас в одном файле собраны все сервисы, контроллеры и директивы, связанные с модулем?

          Да, модуль который просто все связывает и больше ничего не делает. Примерно так:
          1. 'use strict';
          2.  
          3. var dependencies = [];
          4.  
          5. var controller = require('./profile-controller');
          6. var profilePreview = require('./profile-preview-directive');
          7.  
          8. // blocks
          9. var boxDirective = require('./blocks/profile-box-directive');
          10. var connectLinksDirective = require('./blocks/profile-connect-lInks-directive');
          11. var exploreDirective = require('./blocks/profile-explore-directive');
          12. var groupsDirective = require('./blocks/profile-groups-directive');
          13. var summaryDirective = require('./blocks/profile-summary-directive');
          14. var skillsDirective = require('./blocks/profile-skills-directive');
          15. var experienceDirective = require('./blocks/profile-experience-directive');
          16. var educationDirective = require('./blocks/profile-education-directive');
          17.  
          18. module.exports = angular.module('app.profile', dependencies)
          19.     .controller(controller.name, controller)
          20.     .directive(profilePreview.name, profilePreview)
          21.  
          22.     // blocks
          23.     .directive(boxDirective.name, boxDirective)
          24.     .directive(connectLinksDirective.name, connectLinksDirective)
          25.     .directive(exploreDirective.name, exploreDirective)
          26.     .directive(groupsDirective.name, groupsDirective)
          27.     .directive(skillsDirective.name, skillsDirective)
          28.     .directive(summaryDirective.name, summaryDirective)
          29.     .directive(experienceDirective.name, experienceDirective)
          30.     .directive(educationDirective.name, educationDirective);
          31.  


  1. qmax
    18.07.2015 21:00

    Эка…

    $scope.dataLoaded.then(handler)
    

    будет срабатывать каждый раз при переписывании dataLoaded?

    Оно же просто добавит handler к тому промизу, на который в данный момент ссылается dataLoaded.