From Angular
Jump to: navigation, search

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

Назначение Angular[edit | edit source]

AngularJS† - это JavaScript фреймворк, разработанный Google, позволяющий облегчить создание клиентской части одностраничных веб-приложений благодаря применению следующих подходов:

  • Модульная организация кода - приложения на AngularJS разбиты на небольшие модули, которые легко поддерживать и повторно использовать;
  • Декларативный подход к разработке - Angular использует директивы и выражения для придания интерактивности HTML, благодаря чему необходимость прямой работы с DOM сводится к минимуму;
  • Разделение логики и представления - в Angular-приложениях представление (HTML-шаблоны) и бизнес-логика (контроллеры и сервисы) отделены друг от друга, благодаря чему разработчики могут пользоваться всеми преимуществами разделения ответственности;
  • Двустороннее связывание данных - в Angular данные модели обновляются автоматически при изменении состояния представления и наоборот, в отличие от классического MVC, где пользователь применяет контроллер для обновления модели. AngularJS использует паттерн MVVM (Model View ViewModel), в котором контроллер заменен компонентом ViewModel. В случае Angular, компонентом, обеспечивающим автоматическую связь между моделью и представлением (ViewModel), является $scope - контекст.

† Название AngularJS подразумевает Angular версии 1.x. Более старшие версии называются просто Angular и доступны на angular.io

Общее описание архитектуры приложений на AngularJS[edit | edit source]

Типичное приложение на AngularJS включает в себя следующие компоненты:

  • Module - модули - контейнеры верхнего уровня, позволяющие группировать сервисы, директивы и другие компоненты;
  • Config - конфигурация - блок кода, в котором можно производить инициализацию приложения перед тем, как оно начнет работу;
  • Routes - маршруты - позволяют определить набор состояний приложения, загружаемых при переходе на определенный URL. Могут содержать настройки, специфичные для каждого конкретного состояния (URL, HTML-шаблон, имя контроллера и пр.);
  • Views - представления - отображаемая в браузере разметка, созданная в результате компиляции HTML-шаблонов и связывания с данными модели;
  • Controller - контроллеры - связанные с представлениями (через контекст) компоненты, обрабатывающие данные модели;
  • $scope - контекст - компонент, содержащий данные модели и обеспечивающий связь между представлением и контроллером (однако, опция controller-as, описанная ниже, позволяет работать с контроллерами напрямую из представлений);
  • Directive - директивы - используются для придания интерактивности представлениям и их структурирования. Как правило, они содержат повторно-используемый код, который декларативно применяется к элементам HTML-разметки шаблона;
  • Factory/Service - фабрики и сервисы - содержат основную бизнес-логику и могут использоваться контроллерами, а также другими компонентами. Как правило, синглтоны по умолчанию.

Файловая структура приложения на AngularJS[edit | edit source]

Существует несколько подходов к организации файловой структуры Angular-приложений:

  1. Официальные рекомендации от Google;
  2. Рекомендации комьюнити.

Достаточно удобным является смешанный подход. Например, относительно сложное приложение может быть сгруппировано следующим образом:

  index.html                        ; стартовая страница
  /app                              ; корневая директория приложения
    app.js                          ; определение модулей приложения
    app.config.js                   ; код конфигурации приложения
    app.routes.js                   ; конфигурация роутинга
    app.controller.js               ; корневой контроллер приложения (используется на стартовой странице)
    app.scss                        ; общие стили приложения
    /components                     ; повторно-используемые компоненты
      /directives                   ; директивы
        /tableEditor
          TableEditor.directive.js  ; JavaScript код директивы
          TableEditor.html          ; HTML-шаблон директивы
          TableEditor.scss          ; стили, используемые директивой
        /frequencyList
          FrequencyList.directive.js
          FrequencyList.html
          FrequencyList.scss
      /services                     ; сервисы
        AppConfig.service.js
        RadioService.service.js
    ...
    /main                           ; основные компоненты приложения - представления и контроллеры, 
                                    ; используемые роутером, описанные в разделе "Маршрутизация"
      main.html                     ; шаблон основного состояния приложения
      main.controller.js            ; контроллер основного состояния приложения
      /common                       ; повторно-используемые шаблоны
        header.html
      /states                       ; файлы состояний второго уровня вложенности
        /radioAdmin                   
          RadioAdmin.controller.js
          RadioAdmin.html
          RadioAdmin.scss
        /userPanel
          UserPanel.controller.js
          UserPanel.html
          UserPanel.scss
   ...

Минимальный пример приложения на AngularJS[edit | edit source]

HTML-шаблон[edit | edit source]

<!-- index.html -->
<html>
    <head>
        <title>Formula 1 Stats App</title>
        <script src="angular/angular.js"></script>
        <script src="app/app.js"></script>
    </head>
    <!--
        директива ng-app определяет корневой элемент приложения и имя загружаемого модуля
        директива ng-controller связывает указанный контроллер с представлением, создаваемым из данного шаблона
    -->
    <body ng-app="Formula1StatsApp" ng-controller="pilotsController">
        <table>
            <thead>
                <tr>
                    <th colspan="4">Pilots Championship Standings</th>
                </tr>
            </thead>
            <tbody>
                <!--
                    директива ng-repeat копирует элемент в котором она указана в количестве, равном размеру массива
                    pilotList; атрибут директивы содержит выражение Angular, создающее новую переменную контекста pilot 
                    переменная pilotList доступна в выражении из контекста ($scope) и добавлена в него контроллером
                -->
                <tr ng-repeat="pilot in pilotList">
                    <!-- 
                        специальная переменная Angular $index содержит индекс объекта в массиве pilotList, обрабатываемого
                        на текущей итерации
                    -->
                    <td>{{$index + 1}}</td>
                    <!-- 
                        выражения в фигурных скобках вставляют в текст шаблона значения полей объекта текущей итерации, 
                        сохраненного в переменной pilot (в терминах Angular это называется интерполяцией)
                        $event - специальная переменная Angular, содержащая ссылку на текущее событие
                    -->
                    <td ng-click="update($event, $index)">
                        <img src="img/flags/{{pilot.Pilot.nationality}}.png"/>
                        {{pilot.Pilot.givenName}}&nbsp;{{pilot.Pilot.familyName}}
                    </td>
                    <td>{{pilot.Teams[0].name}}</td>
                    <td>{{pilot.points}}</td>
                </tr>
            </tbody>
        </table>
    </body>
</html>

Контроллер[edit | edit source]

// app.controller.js
// определение главного модуля приложения
// второй аргумент метода module должен содержать список имен модулей, используемых создаваемым
angular.module('Formula1StatsApp', []) 
.controller('pilotsController', function($scope) { // определение конструктора контроллера
    // конструктор контроллера вызывается перед связыванием с соответствующим представлением
    // в конструктор внедряется текущий контекст ($scope) изменение состояния которого будет автоматически 
    // отражаться на представлении

    $scope.pilotList = [ // массив pilotList добавляется в контекст и становится доступен из выражений в HTML-коде
      {                  
          Pilot: {       
              givenName: 'Sebastian',
              familyName: 'Vettel'
          },
          points: 322,
          nationality: "German",
          Teams: [
              {name: "Red Bull"}
          ]
      },
      {
          Pilot: {
              givenName: 'Fernando',
              familyName: 'Alonso'
          },
          points: 207,
          nationality: "Spanish",
          Teams: [
              {name: "Ferrari"}
          ]
      }
    ];

    // метод update добавляется в контекст и становится доступен для вызова из выражений в HTML-коде
    $scope.update = function(event, index) {              
      // присвоение новых значений, полям, использованным в выражениях интерполяции
      // автоматически отразится на состоянии отображаемой в браузере HTML-страницы;
      // в данном случае, значение ячейки, по которой кликнул пользователь, будет заменено на "You clicked me!"
      $scope.pilotList[index].Pilot.givenName = "You clicked me!";
    };                                                    
                                                          
});

Как работает Angular[edit | edit source]

При первом прочтении этот раздел можно пропустить

Применять пользовательскую HTML-разметку, интерполяцию и автоматическое обновление представлений при изменении данных модели позволяют следующие механизмы:

Компиляция HTML-шаблонов[edit | edit source]

При создания представлений из HTML-шаблонов, Angular преобразует код шаблона в DOM-дерево и обходит его в поисках директив и выражений. Процесс компиляции происходит в два этапа:

  1. Собственно компиляция - обход DOM-дерева шаблона и сохранение информации о директивах. Затем компилятор сортирует директивы каждого элемента в порядке приоритета (его можно задавать в настройках директив) и вызывает для каждой функцию компиляции, которая, в свою очередь, возвращает функцию связывания (link function). Результатом этого этапа является комбинированная функция, объединяющая функции связывания всех найденных директив.
  2. Связывание - применение комбинированной функции связывания к контексту ($scope) и представлению, в результате чего конкретное представление связывается с данными модели через слушатели событий, устанавливаемые функцией.

В случае динамического создания DOM-структур, содержащих директивы и выражения Angular, внутри функции связывания (что встречается достаточно часто), они не будут учтены компилятором и их нужно скомпилировать вручную с помощью сервиса $compile.

Следует обратить внимание на отличия в способах вызова компилятором функции компиляции директивы (на практике она используется относительно редко) и функции связывания (обе могут быть определены при создании директивы). Функция компиляции директивы может вносить изменения в DOM элемента шаблона, например, устанавливая значения атрибутов, до того как будет создан конкретный экземпляр соответствующего элемента в представлении. Это может быть важно для минимизации издержек производительности (именно для этого компиляция разделена на два этапа), например, в случае директивы ng-repeat, так как функция компиляции будет вызываться только один раз перед тем как будут создаваться конкретные копии повторяющегося элемента. Функция связывания вызывается для конкретного элемента сгенерированного представления и может редактировать или дополнять DOM элемента, устанавливать слушатели DOM-событий, конфигурировать наблюдение ($watch) контекста и пр.

Дайджест-цикл (digest loop)[edit | edit source]

В процессе выполнения дайджест-цикла Angular узнаёт о том, что значение переменной контекста изменилось и необходимо обновить представление или вызвать обработчик наблюдения контекста ($watch). На очередной итерации, Angular сравнивает значение переменной контекста с предыдущим сохраненным значением и предпринимает необходимые действия, в случае его изменения. Обычно итерация цикла инициируется с помощью внутренних механизмов angular, например, по таймеру, или перед вызовом обработчиков, указанных в директивах ng-click или $timeout, однако может быть вручную вызвана из кода, вносящего изменения в контекст, но не связанного с Angular, (например, в обработчиках событий OpenLayers), с помощью метода $scope.$apply. Обычные обработчики событий (например, click) или функция setTimeout не участвуют digest-цикле, поэтому их использования следует избегать в случае необходимости внесения изменений в контекст.

Выражения Angular[edit | edit source]

Выражения Angular используют синтаксис JavaScript со следующими ограничениями:

  • В них нельзя использовать глобальные объекты (свойства объекта window и пр.); доступны только свойства текущего контекста;
  • Невозможно использовать управляющие конструкции (for, if и пр) за исключением тренарного оператора (?:);
  • Нельзя создавать объекты с помощью оператора new, определять новые функции, использовать регулярные выражения в явном виде.

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

Выражения в совокупности с директивами являются основным способом придания интерактивности представлениям и делают прямую работу с DOM ненужной в большинстве случаев (в Angular прямая работа с DOM должна производиться только в коде функции связывания директив).

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

  ...
  $http(...).then(function onResponse(resp) {
    $scope.errorsFound = resp.data;
  });
  ...
  <div ng-show="!errorsFound">...</div>
  <span ng-style="{'background-color': errorsFound? 'red': 'blue'}">...</span>
  <span ng-class="{critical: errorsFound}">...</span>

Переменные, созданные внутри выражений, доступны в рамках всего контекста текущего шаблона:

  <div ng-repeat="team in teams">
    <div ng-repeat="person in team.engineers">
      <div ng-click="displayTeamMember($event, person, team)">
        ...

Часто встречающейся проблемой при определении обработчиков с помощью директив Angular являются последствия распространения событий DOM, из-за которого обработчики дочерних элементов будут срабатывать при активации соответствующих событий родительского элемента пользователем (даже если для последних не определен обработчик). Для предотвращения распространения событий из родительских элементов можно использовать метод $event.stopPropagation().

  <div ng-repeat="team in teams" ng-click="$event.stopPropagation()">
    <div ng-repeat="person in team.engineers">
      <div ng-click="displayTeamMember($event, person, team)">
        ...

В случаях отдельных директив, выражения могут принимать достаточно сложную форму:

  <select id="inputReporter" name="inputReporter"
    ng-model="storyboard.editedStory.reporter" ng-required="true"
    ng-options="user.id as user.name for user in storyboard.users"
    class="form-control">
      <option value="">Please select...</option>
  </select>

Их детальный синтаксис можно выяснить в документации соответствующей директивы.

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

Создание объектов компонентов[edit | edit source]

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

Типы компонентов[edit | edit source]

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

Сервисы[edit | edit source]

Сервисы в терминах Angular - это именованные значения, доступные через механизм внедрения зависимостей.

Значения[edit | edit source]
Значения - это простые сервисы, содержащие изменяемые данные.
  angular.module('Formula1StatsApp').value('bestPitstop', 1.9);

  angular.module('Formula1StatsApp').controller('pilotsController', function($scope, bestPitstop) {
    console.assert(bestPitstop === 1.9, 'Someone has changed bestPitstop value');
  }
Константы[edit | edit source]
Константы - это сервисы с неизменяемым значением, доступные как на этапе конфигурации, так и на этапе выполнения приложения.
  angular.module('Formula1StatsApp').constant('CURRENT_HAMILTON_PODIUMS', '110');

  angular.module('Formula1StatsApp').controller('pilotsController', function($scope, CURRENT_HAMILTON_PODIUMS) {
    ...
  }
Фабрики[edit | edit source]
Фабрики - это сервисы, возвращающие значение, (однократно) сгенерированное функцией, переданной в метод module.factory(). Это позволяет Angular отложить вычисление значения до момента, когда это будет необходимо. Таким значением может быть, например, литерально определяемый объект:
  angular.module('Formula1StatsApp').factory('radioService', function () {
    return {
      beginTransmission: function() {...}
    };
  });
Сервисы[edit | edit source]
Метод создания сервисов получает конструктор†, результат вызова которого доступен через механизм внедрения зависимостей Angular. По умолчанию сервисы являются синглтонами и создаются только когда в этом возникает необходимость. Их можно использовать для хранения глобального состояния модуля и повторно-используемой логики (в методах объекта сервиса).
† В JavaScript конструктор - это функция, создаваемая с расчетом на то, что она будет вызвана с использованием оператора new и this будет содержать ссылку на экземпляр создаваемого объекта.
Результатом работы следующего примера является объект, аналогичный создаваемому фабрикой в примере выше:
  angular.module('Formula1StatsApp').service('radioService', function () {
    this.beginTransmission = function() {...}
  });

Специальные объекты[edit | edit source]

Контроллеры[edit | edit source]
Метод создания контроллера получает на вход конструктор, результат вызова которого (объект контроллера) связывается с представлением при помощи контекста ($scope). Контроллеры применяются для работы с данными модели (переменными контекста). В случае использования синтаксиса controller as объект создаваемого ниже контроллера pilotsController будет доступен в Angular-выражениях через переменную pilotsCtrl.
  angular.module('Formula1StatsApp').controller('pilotsController', function($scope) {
    var vm = this; // vm = ViewModel (соглашение об именовании для контроллеров)
    vm.controllerMethod = function () {...}
    ...
  });
  <div ng-app="..." ng-controller="pilotsController as pilotsCtrl">
  ...
    <input type="button" ng-click="pilotsCtrl.controllerMethod($event)" ...
Директивы[edit | edit source]
В отличие от метода contorller, принимающего конструктор, метод directive принимает на вход фабричную функцию, которая должна вернуть объект создаваемой директивы. Директивы используются компилятором DOM при генерации представлений.
  angular.module('Formula1StatsApp').directive('cars', function() {
    return {
      restrict: 'E',
      templateUrl: 'car-list.html'
    };
});
Компоненты[edit | edit source]
Компоненты представляют собой упрощенные директивы, содержащие только HTML-шаблон и контроллер (для компонента нельзя задать функцию связывания и функцию компиляции). Метод создания компонентов принимает объект конфигурации:
  angular.module('Formula1StatsApp').component('cars', {
    templateUrl: 'car-list.html',
    controller: function() {
      this.carList = ...
    }
  });
Фильтры[edit | edit source]
Фильтры - это декларативный способ форматирования результатов Angular-выражений (принимает функцию, преобразующую входной параметр). Они могут использоваться, когда форматирование требует применения средств, недоступных в выражениях Angular (например, функций Math.*).
  angular.module('Formula1StatsApp').filter('pointFilter', function() {
    return function(input) {
      return Math.log10(input) + 'pt. log';
    }
  });
  <td ng-repeat="p in pilot.pointList | pointFilter">{{p}}</td>
Провайдеры[edit | edit source]
Провайдер - это абстрактный интерфейс на основе которого реализованы все предыдущие виды компонентов. Его следует использоваться в случаях, когда необходима дополнительная конфигурация компонента до того, как приложение начнет работу.

Специальные блоки[edit | edit source]

Конфигурация[edit | edit source]
Задаёт функцию, которая вызывается при загрузке модуля, перед созданием инжектора. В нее могут быть внедрены только провайдеры и константы.
  angular.module('Formula1StatsApp').config('pointFilter', function(myProvider, MY_CONSTANT) {
    ...
  });
Выполнение[edit | edit source]
Задаёт функцию, которая вызывается после регистрации компонентов и создания инжектора (в нее могут быть внедрены сервисы, зарегистрированные модулем).
  angular.module('Formula1StatsApp').run('pointFilter', function(myService) {
    ...
  });

Внедрение зависимостей (Dependency injection)[edit | edit source]

Angular вызывает функции и конструкторы, передаваемые во все описанные выше методы создания компонентов, через сервис $injector, который делает ранее зарегистрированные компоненты автоматически доступными через соответствующие параметры вызываемой функции (или конструктора):

  angular.module('Formula1StatsApp').service('radioService', function () {
    this.beginTransmission = function() {...};
  });
  angular.module('Formula1StatsApp').controller('transmissionController', function($scope, radioService) {
    $scope.frequency = radioService.beginTransmission();
  });

По умолчанию, $injector использует только имена параметров для сопоставления их с компонентами, что не будет работать в случае минификации кода. Существует несколько вариантов решения этой проблемы:

1. Аннотация параметров с помощью массива, содержащего в качестве последнего элемента функцию (или конструктор), создающую компонент:

  angular.module('Formula1StatsApp').controller('transmissionController', 
    ['$scope', 'radioService', function($scope, radioService) {
      $scope.frequency = radioService.beginTransmission();
    }]);

2. Использование свойства $inject

  var transmissionController = function($scope, radioService) {
      $scope.frequency = radioService.beginTransmission();
    };
  transmissionController.$inject = ['$scope', 'radioService'];
  angular.module('Formula1StatsApp').controller('transmissionController', transmissionController);

3. Ипользование препроцессора позволяет автоматизировать предыдущий вариант с помощью аннотации ngInject:

  /* ngInject */
  angular.module('Formula1StatsApp').controller('transmissionController', function($scope, radioService) {
    $scope.frequency = radioService.beginTransmission();
  });

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

Связывание данных модели и представления[edit | edit source]

Существует несколько способов связывания данных модели (переменных контекста) и DOM-элементов представления:

Двустороннее связывание[edit | edit source]

Директива ng-model устанавливает двустороннее связывание данных модели и представления. В ее выражении указывается переменная контекста, которой должно быть присвоено значение (value) элемента, содержащего данную директиву (ng-model использована в примере выше). Как правило, данный вид связывания используется при создании HTML-форм и позволяет изменять состояние модели автоматически при изменении состояния соответствующих элементов ввода (и наоборот).

Одностороннее связывание[edit | edit source]

Одностороннее связывание производится с помощью директив ng-bind, ng-bind-html и выражений интерполяции. В данном случае, представление будет реагировать на изменение данных модели, но не на оборот. Директива ng-bind-html форматирует передаваемый ей в качестве значения HTML-код, например, преобразуя HTML-мнемоники в соответствующие символы. См. также: ng-cloak, ng-bind-template.

Однократное связывание[edit | edit source]

Для экономии ресурсов, в выражениях интерполяции переменную контекста можно пометить как переменную для однократного связывания, предварив ее имя двумя двоеточиями: {{::variableName}}. В этом случае, изменение значения переменной не будет отслеживаться Angular и останется неизменным после первого присвоения значения, отличного от undefined.

Наблюдение контекста[edit | edit source]

Все описанные выше механизмы связывания данных внутренне основаны на API наблюдения контекста. С помощью метода $scope.$watch() можно установить функции-слушатели, вызываемые на очередной итерации дайджест-цикла в случае изменения значения наблюдаемой переменной:

  $scope.$watch("errorsFound", function(newValue, oldValue) {
    console.log("**** Errors detected! ****")
  });

Эта возможность часто применяется при передаче данных в директивы с изолированным контекстом.

По умолчанию, сравнение значений переменных производится по ссылке, однако с помощью третьего параметра метода $watch можно задать сравнение по значению. Кроме этого, можно отслеживать изменения коллекций с помощью метода $scope.$watchCollection.

Директивы[edit | edit source]

Умение создавать директивы важно для модуляризации приложения

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

Типы директив[edit | edit source]

Существуют следующие четыре типа директив (ACME), тип задается соответствующей буквой в свойстве restrict объекта директивы:

  • A - атрибуты (attributes): <div my-directive="expression">...</div>
  • C - классы (class names): <div class="my-directive: exp;">...</div>
  • M - комментарии (comments): <!-- directive: my-directive expression -->
  • E - элементы (elements): <my-directive></my-directive>

Записанное в kebab-case имя директивы нормализуется компилятором для возможности его использования в JavaScript. Например, имя директивы my-directive из примера выше при ссылках на нее из JavaScript-кода нужно записывать в camelCase ('myDirective'). Для избежания конфликтов с будущими расширениями стандарта HTML в именах директив рекомендуется использовать уникальные префиксы.

Создание директив[edit | edit source]

Метод создания директив module.directive() принимает фабричную функцию, которая вызывается в момент первой идентификации директивы компилятором и возвращает объект, свойства которого должны содержать настройки директивы, перечисленные ниже.

Настройки директив[edit | edit source]

  • restrict - строка, содержащая одну или несколько букв, задающих ограничения на область применения директивы (элемент, атрибут и др.);
  • scope - определяет настройки контекста директивы:
  • содержит false - директива будет использовать существующий контекст (не подходит для директив, встречающихся в шаблоне более одного раза, так как все директивы одного типа будут разделять общий контроллер);
  • содержит true - для директивы будет создан новый контекст, прототипически наследованный от существующего (подразумевает те же ограничения, что и выше, так как все директивы одного типа будут иметь доступ к общим данным);
  • содержит объект - в этом случае директива будет использовать вновь созданный изолированный контекст. Объект в свойстве scope должен определять свойства, задающие начальные переменные создаваемого контекста, к которым, при его инициализации, будут привязаны значения атрибутов HTML-элемента директивы (как в примере ниже). Символьный префикс перед именем атрибута задает тип связывания данных:
  • @ - в переменную будет скопирована строка, содержащая литеральное текстовое значение атрибута;
  • = - устанавливает двустороннюю связь между переменной изолированного контекста и результатом вычисления значения атрибута как Angular-выражения в родительском контексте, (им может быть, например, ссылка на переменную родительского контекста; в родительском контексте будет доступно наблюдение изменения ее значения через переменную в дочернем).
В Angular 1.5+ использование двустороннего связывания при создании изолированных контекстов считается плохой практикой. В место него предлагается использовать одностороннее связывание и сообщения, однако, если связанные данные разделены иерархией контекстов с неочевидной структурой, двустороннее связывание позволяет существенно упростить код (при этом нужно учитывать требуемый уровень абстрактности кода).
  • < - односторонняя связь между переменной контекста и значением атрибута, вычисленного так же, как описано выше. В этом случае изменения значения переменной контекста не будут наблюдаемы извне, однако данные, доступные через свойства переменной (если это объект) будут изменяемы в родительском контексте;
  • & - переменной контекста будет присвоена ссылка на функцию-обертку, вычисляющую в родительском контексте выражение, содержащееся в атрибуте (применяется для возвращения директивой значения в родительский контекст). Значение переменных в этом выражении можно задавать из изолированного контекста, вызывая функцию-обертку с параметром-объектом, содержащим свойства, соответствующие названиям переменных в выражении (более подробную информацию можно найти в документации).
  • bindToController - в случае указания true в качестве значения данной настройки, значения переменных, полученные из атрибутов элемента директивы, будут привязаны не к контексту, а к контроллеру директивы (в отличие от директив, компоненты используют эту настройку по умолчанию);
  • compile - функция компиляции (возвращающая набор функций связывания). Функция компиляции вызывается для элемента HTML-шаблона до его клонирования и может использоваться для настройки DOM-шаблона перед генерацией представления. На практике эта возможность используется относительно редко;
  • link - функция связывания (этот параметр используется компилятором только в случае, если не установлен предыдущий). Функция связывания вызывается для (клонированного) экземпляра элемента представления и необходима для обеспечения связи его DOM-структуры с данными контекста;
  • templateUrl - ссылка на HTML-шаблон директивы в случае директивы, разворачивающей шаблон;
  • controller - контроллер, обеспечивающий работу с данными, используемыми представлением, сгенерированным из указанного в настройке выше HTML-шаблона;
  • controllerAs - имя контроллера, доступное из выражений шаблона.

Полный список настроек объекта директивы доступен в документации.

Передача данных в директиву с изолированным контекстом[edit | edit source]

Основной способ передачи данных в директиву с изолированным контекстом - это указание переменных родительского контекста в атрибутах элемента директивы:

  <!-- index.html -->
  <div ng-controller="Iam as im">
    <my-directive attr1="I'm a string" attr2="imAParentValue" some-object-ref="im.anObjectRef()" callback-ref="parentContextVar = newValue"></my-directive>
  </div>
// пример в стиле Angular 1.5+
angular.module('myModule', [])
.controller('Iam', function () {
    this.anObjectRef = function() { return {someData: "Some value"}; };
  })
.directive('myDirective', function() {
 return {
    scope: {
            imAString: "@attr1",
            imAValue: "<attr2",
            someObjectRef: "<", // название атрибута совпадает с названием переменной
            callbackRef: "&"
           },
    link: function(scope, element) {
      element.text(scope.imAString);
      scope.callbackRef({newValue: "My response"}); // присвоение значения "My response" переменной parentContextVar родительского контекста
    }
  }]);

В данном примере, первый параметр передается в изолированный контекст как строка, переменная imAValue содержит ссылку, получаемую из переменной родительского контекста imAParentValue через атрибут attr2, someObjectRef получает ссылку на объект, возвращаемый в результате вычисления выражения из атрибута some-object-ref. Последний параметр содержит выражение, устанавливающее значение переменной родительского контекста.

Способы применения директив[edit | edit source]

  • Создание повторно-используемых компонентов, разворачивающих HTML-шаблон:
  <!-- index.html -->
  <div ng-controller="Controller">
    <div my-customer></div>
  </div>
  <!-- template.html -->
  <!-- 
    HTML-код шаблона, который будет вставлен внутрь тэга с атрибутом my-customer при создании представления;
    переменная customer доступна из родительского контекста, так как настройка scope директивы не указана
  -->
  <div>
    <span>Name: {{customer.name}} Address: {{customer.address}}</span>
  </div>
angular.module('docsTemplateUrlDirective', [])
.controller('Controller', ['$scope', function($scope) {
  // установка значений переменной контекста, используемой при интерполяции в шаблоне
  $scope.customer = {
    name: 'Naomi',
    address: '1600 Amphitheatre'
  };
}])
.directive('myCustomer', function() { // фабричная функция директивы
  return {
    // задание файла шаблона в настройках директивы
    templateUrl: 'template.html'
  };
});
  • Изменение или генерация DOM-структуры представления:
  <!-- index.html -->
  <div ng-controller="Controller">
    Date format: <input ng-model="format"> <hr/>
    <!-- 
      значение элемента с директивой-атрибутом my-current-time будет периодически заменяться на текущее время,
      отформатированное в соответствии с шаблоном, заданным в поле выше
    -->
    Current time is: <span my-current-time="format"></span>
  </div>
angular.module('docsTimeDirective', [])
// определение контроллера для представления выще
.controller('Controller', ['$scope', function($scope) {
  // инициализация переменной контекста, значение которой будет использоваться для передачи шаблона форматирования в директиву
  // (директива использует родительский контекст)
  $scope.format = 'M/d/yy h:mm:ss a';
}])
// определение директивы
.directive('myCurrentTime', ['$interval', 'dateFilter', function($interval, dateFilter) { // фабричная функция директивы

  function link(scope, element, attrs) { // функция связывания директивы
    var format,
        timeoutId;

    function updateTime() {
      element.text(dateFilter(new Date(), format)); // использование возможностей jqLite (метод text) для установки текстового содержимого элемента
    }

    // установка наблюдения значения атрибута my-current-time элемента директивы, содержащего ссылку на переменную контекста format
    // так как эта переменная указана и в атрибуте ng-model элемента input, ввод нового значения в него пользователем будет изменять контекст
    // и вызывать данный обработчик
    scope.$watch(attrs.myCurrentTime, function(value) {
      format = value;
      updateTime(); // форматирование времени в соответствии с новым шаблоном, полученным из элемента ввода
    });

    // слушатель события удаления представления
    element.on('$destroy', function() {
      $interval.cancel(timeoutId);
    });

    // периодическое обновление DOM
    timeoutId = $interval(function() {
      updateTime();
    }, 1000);
  }

  return {
    link: link
  };
}]);
  • Установка слушателей событий:
  <!-- index.html -->
  <!-- обработчик в коде ниже будет вызываться для всех элементов с атрибутом my-clickable -->
  <span my-clickable test-attribute="test">Click Me</span>
angular.module('clickModule', [])
.directive('myClickable', ['$document', function($document) {
 return {
    scope: {},                              // директива создает пустой изолированный контекст
    link: function(scope, element, attr) {
      element.on('click', function(event) { // Использование возможностей jqLite (метод on)
        alert('You clicked me!');
      });
    }
  }]);
  • Клонирование или преобразование HTML-содержимого элемента директивы с помощью трансклюзии. В то время, как первый пример вставляет в пустой элемент директивы содержимое шаблона, в котором выражения интерполяции заменены на текстовые значения из контекста, трансклюзия позволяет обобщить шаблонизацию, и вставлять в шаблон произвольную HTML-разметку. Добавляемая в шаблон разметка должна содержаться внутри элемента директивы.
  <!-- index.html -->
  <div ng-controller="Controller">
    <!-- 
       при генерации представления содержимое тега my-dialog будет извлечено, вместо него в тег будет вставлено содержимое 
       шаблона template.html куда, в свою очередь, (под элемент, помеченный директивой ng-transclude) будет добавлено извлеченное 
       ранее содержимое тега my-dialog с текстом "Initial string" в качестве интерполяции выражения {{text}}
    -->
    <my-dialog><span>Here the text is inserted {{text}}!</span></my-dialog>
  </div>
  <!-- template.html -->
  <div class="alert" ng-transclude></div>
angular.module('docsTransclusionExample', [])
.controller('Controller', ['$scope', function($scope) {
  $scope.text= 'Initial string';
}])
.directive('myDialog', function() {
  return {
    restrict: 'E',
    // директива использует трансклюзию - содержимое элемента директивы будет доступно в коде в качестве DOM-структуры, и
    // добавляемая в шаблон разметка, при необходимости, может быть изменена или размножена (так работает директива ng-repeat)
    // в этом примере работа с разметкой не используется
    transclude: true,
    scope: {},
    templateUrl: 'template.html',
    link: function(scope) {
      // следующая строка не будет иметь эффекта, так как контекст директивы с трансклюзией наследуется от внешнего контекста директивы;
      // в выражения интерполяции будут попадать только переменные из родительского контекста
      scope.text= 'Changed string';
    }
  };
});

Функция связывания[edit | edit source]

Имеет следующую сигнатуру:

  link: function(scope, element, attrs, controller, transcludeFn) {
  ...
  }

Где:

  • scope - контекст, с которым связывается генерируемое компилятором представление;
  • element - ссылка на DOM-элемент в обертке jqLite из конкретного экземпляра представления, генерируемого компилятором;
  • attrs - значения атрибутов элемента директивы;
  • controller - по умолчанию, собственный контроллер директивы;
  • transcludeFn - функция, позволяющая получить содержащуюся в элементе директивы разметку, если в настройках указано использование трансклюзии.

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

Часто используемые встроенные директивы[edit | edit source]

Следующие директивы позволяют декларативно изменять состояние DOM в зависимости от условий (состояния переменных контекста):

  • ng-style - позволяет установить произвольный стиль
  • ng-class - устанавливает класс элемента
  • ng-selected - помечает элемент списка как выбранный
  • ng-checked - помечает флаг как установленный
  • ng-readonly - делает элемент ввода доступным только для чтения
  • ng-disabled - делает элемент заблокированным в зависимости от условий
  • ng-show/ng-hide - показывает или скрывает элемент
  • ng-if - позволяет исключить помеченный атрибутом элемент из DOM шаблона
  • ng-switch - более продвинутая версия ng-if, учитывающая несколько возможных вариантов условия
  • ng-repeat - повторение элемента
  • ng-src/ng-href - позволяет корректно использовать интерполяцию в ссылках
  • ng-<event> - устанавливает соответствующий DOM-событию event обработчик, участвующий в дайджест-цикле
  • ng-attr-<attr-name> - добавляет в элемент атрибут с именем attr-name и указанным значением, если значение не равно undefined

Разделение приложения на компоненты[edit | edit source]

Для организации структуры приложений в Angular 1.5+ предлагается использование повторно-используемых компонентов и директив вместо простого набора контроллеров и связанных с ними представлений:

Рекомендации по созданию компонентов:

  • Компоненты управляют только собственным представлением и данными. Они не должны изменять DOM и данные за пределами собственного контекста.
  • Компоненты должны иметь хорошо продуманный программный интерфейс - документированный набор входных и выходных атрибутов, а так же принимаемых и отправляемых сообщений. В Angular 1.5+ для приема входных данных рекомендуется использовать литеральное (@) и одностороннее (<) связывание, для возврата результатов необходимо использовать функцию ответа (&).
  • Компоненты должны использовать методы жизненного цикла, инициализируя ресурсы только когда требуется, и освобождая неиспользуемые ресурсы.
  • Приложение должно быть организовано в виде иерархии компонентов.

Сообщения и иерархия контекстов[edit | edit source]

Использование компонентов и директив многократной вложенности, как правило, приводит к созданию запутанной иерархии изолированных контекстов, в результате чего прямой обмен данными между экземплярами компонентов может быть затруднен. Для решения этой проблемы следует использовать обмен сообщениями (events), доступный через API контекста:

  • $scope.$emit() - отправляет сообщение вверх по иерархии контекстов;
  • $scope.$broadcast() - отправляет сообщение вниз по иерархии контекстов. Если требуется отправить сообщение из нижних уровней иерархии элементом того же уровня, область действия данной функции можно ограничить, используя ссылку на родительский контекст $scope.$parent, однако, если нет другого выхода, можно использовать глобальный контекст $rootScope;
  • $scope.$on() - устанавливает слушатель сообщений заданного типа.

В некоторых случаях Angular отсылает в контекст специальные сообщения, например сообщение $destroy в случае удаления директивы при замене текущего отображения (ng-view) роутером. Директива должна обрабатывать сообщение $destroy для удаления слушателей DOM-событий, слушателей $watch, установленных на глобальный контекст ($rootScope), работающих таймеров и освобождения прочих ресурсов. В Angular 1.5+ для этой цели вместо встроенных событий можно использовать методы жизненного цикла компонента.

Другие способы обмена данными[edit | edit source]

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

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

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

Маршрутизация[edit | edit source]

В современных версиях Angular для маршрутизации рекомендуется использование библиотеки ui-router, которая позволяет настраивать соответствие между URL-path и состояниями приложения (одна из концепций ui-router; с состоянием ассоциируется представление, контроллер и пр.). Среди наиболее существенных преимуществ ui-router можно выделить:

  • возможность организации состояний в виде иерархической структуры - при активации URL одного из состояний нижнего уровня, его шаблон будет вставляться в элемент шаблона родительского состояния, помеченный директивой ui-view;
  • возможность указания нескольких представлений для одного состояния, благодаря чему возможно повторное использование шаблонов, например, заголовка страницы;
  • возможность исключать переход на какое-либо состояние из истории браузера.

Определение состояний[edit | edit source]

Набор состояний приложения может быть задан с помощью провайдера $stateProvider, внедренного в блок конфигурации модуля:

 angular
  .module('Formula1StatsApp')
  .config(function ($stateProvider) {
    $stateProvider
      .state('main', {
        url: '/',
        views: {
          'index@': { // абсолютное имя представления с пустым названием состояния (после символа @), указывающее на то, что 
                      // шаблон данного представления должен быть вставлен в элемент с ui-view='index' в корневом шаблоне index.html, 
                      // принадлежащем корневому безымянному состоянию 
                      // (более подробно в документации: https://github.com/angular-ui/ui-router/wiki/Multiple-Named-Views )
            templateUrl: 'app/main/main.html',
            controller: 'MainController',
            controllerAs: 'main'
          }
        }
      })
      .state('radioState', {
        url: '/radio',
        parent: 'main',  // указание родительского состояния
        views: {
          'tableView': { // шаблоны представлений состояний radioState и usersState размещаются в элементе, помеченном ui-view='tableView', 
                         // из шаблона (main.html), указанного в настройках представления предыдущего состояния
            templateUrl: 'app/main/states/radioService/RadioService.html',
            controller: 'RadioController',
            controllerAs: 'radioCtrl'
          },
          'headerView' : { // то же, что и для представления выше (шаблон попадает в элемент с ui-view='headerView')
            templateUrl: 'app/main/common/header.html'
          }
        },
        // опции data или resolve позволяют ассоциировать с состоянием произвольные данные,
        // которые будут доступны в его контроллере через внедряемый сервис $state
        // в случае настройки resolve возможно использование promises, которые будут разрешены перед созданием состояния,
        // это можно использовать для получения данных из внешних источников
        resolve: {
          freqList:  function($http){
             return $http({method: 'GET', url: '/frequencyList'});
          }
        }
      })
      .state('usersState', {
        url: '/user/:userId', // переменная пути позволяет получить соответствующую часть актуальной URL в контроллере состояния
        parent: 'main',
        views: {
          'tableView': {
            templateUrl: 'app/main/states/userPanel/UserPanel.html',
            controller: 'UserController',
            controllerAs: 'userCtrl'
          },
          'headerView' : {
            templateUrl: 'app/main/common/header.html'
          }
        }
      })
    ...

  angular.module('Formula1StatsApp').controller('RadioController', function($scope, $state) {
    var vm = this;
    $scope.freqList = $state.freqList.data; // получение данных из настройки resolve
    ...
  });

  angular.module('Formula1StatsApp').controller('UserController', function($scope, $stateParams) {
    var vm = this;
    $scope.userId = $stateParams.userId; // получение значения переменной пути через внедряемый объект $stateParams
    ...
  });
  <!-- index.html -->
  ... 
  <div ui-view='index'></div>
  ...

  <!-- main.html -->
  ... 
  <div ui-view='headerView'></div>
  <div ui-view='tableView'></div>
  ...

Переходы между состояниями[edit | edit source]

Для переходов между состояниями, инициируемых из кода, следует использовать метод $state.go(), принимающий три параметра:

  • to - состояние назначения. Можно указывать полное имя состояния в иерархии, разделяя уровни иерархии точкой (например: "^.state1.state2", в данном случае ^ означает родительское состояние текущего);
  • params - набор параметров, доступных в состоянии назначения через сервис $stateParams;
  • options - опции перехода, среди которых можно выделить reload, принудительно перезагружающую текущее состояние, если имя целевого состояния совпадает с его собственным.

В HTML-шаблонах ссылки на состояния можно указывать, используя директиву-атрибут ui-sref="<имя состояния>".

Отслеживание переходов[edit | edit source]

В ui-router версии 1.0+ cервис $transitions предоставляет набор методов (например: onStart, onRetain, onSuccess), позволяющий вызывать обработчики событий, генерируемых роутером на различных стадиях перехода. Первый аргумент метода задает набор критериев сравнения, определяющих набор состояний, для которых будет вызываться функция, передаваемая вторым параметром (ей передается объект Transition, содержащий информацию о текущем переходе):

  angular.module('Formula1StatsApp').controller('RadioController', function($scope, $transitions) {
    $transitions.onStart( { to: 'usersState' }, function(trans) {
       ...
    }
  });

Критерии сравнения допускают обобщение с помощью групповых символов, например { to: 'main.**' }.

Работа с HTTP[edit | edit source]

Сервис $http[edit | edit source]

Сервис $http предоставляет ряд методов (get, post и др.), соответствующих основным методам, используемым в HTTP-запросах, принимающих помимо URL запроса, объект данных в случае POST-запроса (по умолчанию сериализуемый в JSON), а так же объект с настройками, позволяющий устанавливать значения HTTP-заголовков, использовать пользовательский маршалер данных и т.д. Запрос с произвольным HTTP-методом можно выполнить, передавая в сервис объект конфигурации, содержащий название требуемого метода:

  $http({
    method: 'HEAD',
    url: '/myService'
  }).then(...);

Сразу после осуществления асинхронного запроса $http возвращает promise, которое будет разрешено при получении ответа от сервера.

Promises[edit | edit source]

Promise - это объект, позволяющий получить доступ к результату отложенного вычисления, которым, в случае асинхронного запроса, является объект, содержащий данные и статус HTTP-ответа сервера. После получения HTTP-ответа, объект с результатом запроса передается в обработчики успешного или ошибочного разрешения (resolve) promise, :

  var myPromise = $http.get(url);
  myPromise
    .then(function (response) {             // обработчик успешного разрешения
      console.log(response.data);           // вывод объекта с данными, в случае JSON-ответа, автоматически десериализуемыми сервисом $http
    },
    function (response) {                   // обработчик ошибочного разрешения передается вторым параметром функции then 
                                            // (он может быть опущен, однако, это считается плохой практикой)
      console.log("*** The request to " + url + " has failed with status: " + response.status);           
    });

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

  var myPromise = $http.get(url);
  myPromise
  .then(function (response) {    
          return $http.get(url2);            // обработчик успешного разрешения возвращает promise, полученное в результате вызова $http.get 
        },                                   // оно используется как результат метода then (на самом деле процесс немного более сложен)
                                             // и на него можно установить новые обработчики разрешения
        function (response) {                
          console.log("*** The request to " + url + " has failed with status: " + response.status);           
        })
  .then(function (response) {                // еще один HTTP-запрос в цепочке, аналогичный предыдущему, ответ предыдущего запроса 
                                             // доступен через параметр обработчика, однако в данном случае он игнорируется
          return $http.get(url3);
        },
        function (response) {                                                   
          console.log("*** The request to " + url2 + " has failed with status: " + response.status);           
        })
  .then(function (response) {    
          return response.data;              // в этом случае, для демонстрации метод then возвращает отличное от promise значение (данные из 
        },                                   // результата предыдущего запроса) они будут обернуты в promise, возвращаемое текущим методом then
        function (response) {                                                   
          console.log("*** The request to " + url3 + " has failed with status: " + response.status);           
        })
  .then(function (data) {    
          console.log(data);                 // data содержит объект, полученный из предыдущего обработчика разрешения
        });

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

Библиотека $q[edit | edit source]

Сервис $q позволяет создавать promises в пользовательском коде:

  function myAsyncComputation() { // myAsyncComputation - функция, использующая асинхронные вычисления
    var defer = $q.defer();       // создание отложенного результата с использованием $q
    ...
    myAsyncComputationService.compute( // вызов асинхронной функции
      function (result) {              // обработчик завершения асинхронной функции (захватывает объект defer из лексического контекста)
        if (result.success)            // при завершении вычисления: 
          defer.resolve(result.data);  // вызов обработчика успешного разрешения promise, возвращаемого myAsyncComputation с передачей ему полученных данных
        else 
          defer.reject(result.error);  // вызов обработчика ошибочного разрешения promise, возвращаемого myAsyncComputation с передачей ему кода ошибки
    }
    ...
    return defer.promise; // сразу после вызова асинхронной функции myAsyncComputationService.compute, myAsyncComputation возвращает promise, 
  }                       // позволяющее позже, при завершении вычислений, получить доступ к отложенному результату

Часто встречающимся вариантом неправильного использования $q является использование его вместе с $http:

  function performRequest(url) {
  var defer = $q.defer();
    $http({method: 'GET', url: url})
      .then(function (response) {
              defer.resolve(response.data);
            },
            function (response) {
              defer.reject(response);
            });
    return defer.promise;
  };

Так как $http возвращает promise, его можно использовать напрямую:

  function performRequest(url) {
    return $http({method: 'GET', url: url});
  };

Демонстрационное приложение[edit | edit source]

Демонстрационное приложение, код которого позволяет понять, как всё описанное выше может выглядеть собранным вместе: https://github.com/angular-app/angular-app (прямая ссылка на клиентскую часть)

Рекомендуемая литература[edit | edit source]