Статьи

Компоненты приложения с AngularJS


Этот пост о автономных бизнес-компонентах с
AngularJS,  которые могут быть созданы в произвольных местах в приложении. 

С AnuglarJS каждый пишет HTML-код ясно, а затем реализует поведение в JavaScript с помощью контроллеров и директив. Контроллеры — это модель, представлением которой является HTML, а директивы — о дополнительных тегах и атрибутах, которыми вы можете расширить HTML. Вы должны реализовать бизнес-логику в контроллерах и логику пользовательского интерфейса в директивах. Хороший. Но бывают ситуации, когда различие не столь очевидно, в частности, когда вы создаете пользовательский интерфейс путем многократного использования бизнес-функций.

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


Если вы используете
AngularJS , вот способ легко добиться такого рода инкапсуляции: 
  1. Поместите HTML-файл в файл в виде отдельного шаблона «частичный» (т. Е. Без HTML-тегов документа верхнего уровня). 
  2. Сделайте так, чтобы его контроллер JavaScript был как-то включен в основную страницу HTML.
  3. Подключите его к любой другой части приложения, например, к другим HTML-шаблонам. 

Эта последняя часть не может быть выполнена с помощью API AngularJS. Мы должны написать код склеивания. Поскольку мы будем подключаться, ссылаясь на наш компонент в шаблоне HTML, мы должны написать собственную директиву. Вместо того чтобы писать отдельную директиву для каждого компонента, как рекомендует документация AngularJS, мы напишем одну директиву, которая будет обрабатывать все наши компоненты. Конечно, есть общая директива, включающая HTML-партиалы в AngularJS,
директива
ng-view , но она ограничена заменой основного содержимого страницы, слишком грубого, то есть. Наша директива, напротив, может использоваться где угодно, рекурсивно вкладываться и т. Д. Вот пример ее использования:

<be-plug name="shippingAddressList">
  <be-model-link from-child-scope="currentSelection" 
       from-parent-scope="shippingAddress">
</be-model-link></be-plug>

В этом небольшом фрагменте предполагается, что у нас есть файл шаблона HTML с именем
shippingAddressList.ht, который позволяет пользователю выбрать один из нескольких адресов для отправки содержимого корзины покупок. У нас есть тег верхнего уровня, называемый
be-plug, и вложенный тег, называемый
be-model-link . В
быть-модель-ссылка тег связывает атрибуты модели компоненты к атрибутам модели (т.е. области в терминах AngularJS лет) вмещающее HTML. Подробнее об этом ниже. Вот реализация:

app.directive('bePlug', function($compile, $http) {
  return {
    restrict:'E',
    scope : {},
    link:function(scope, element, attrs, ctrl) {
      var template = attrs.name + ".ht";
      $http.get(template).then(function(x) {
        element.html(x.data);
        $compile(element.contents())(scope); 
        $.each(scope.modelLinks, function(atParent, atChild) {
          // Find a parent scope that has 'atParent' property
          var parentScope = scope;
          while (parentScope != null && 
                 !parentScope.hasOwnProperty(atParent))
            parentScope = parentScope.$parent;
          if (parentScope == null) 
            throw "No scope with property " + atParent + 
                  ", be-plug can't link models";
          scope.$childHead.$watch(atChild, function(newValue) {
            parentScope[atParent] = newValue;
          });
          parentScope.$watch(atParent, function(newValue) {
            scope.$childHead[atChild] = newValue;
          });            
        });
      });
    }
  };
});

Давайте разберем приведенный выше код. Во-первых, убедитесь, что вы знакомы с тем,
как писать директивы в AngularJS,  и понимаете, что такое области применения AngularJS. Далее, обратите внимание, что мы создаем область действия для нашей директивы, объявляя объект
области видимости: {}  . Цель двоякая: (1) не загрязнять родительскую область и (2) убедиться, что у нас есть единственный дочерний элемент в нашей области видимости, поэтому мы имеем указатель на область действия компонента, который мы включаем.

Хороший. Теперь давайте посмотрим на суть директивы, ее
ссылкуметод. (Я уверен, что есть веская причина, по которой этот метод называется «ссылка». Возможно, потому что мы «связываем» шаблон HTML с содержащим его элементом. Или с моделью через область действия? Что-то в этом роде.) В любом случае, это были манипуляции с DOM. Итак, вот что происходит в нашей реализации:

  • Мы получаем HTML-шаблон с сервера. Согласно соглашению об именах, мы ожидаем, что файл будет иметь расширение .ht . Остальная часть относительного пути файла шаблона указывается в атрибуте name .
  • Как только шаблон загружен, мы устанавливаем его как HTML-содержимое элемента директивы. Таким образом, полученный DOM будет иметь DOMnode с подключаемым модулем, который браузер с радостью проигнорирует, и внутри этого узла будет HTML-шаблон нашего компонента.
  • Затем мы «компилируем» содержимое HTML с помощью сервиса $ compile AngularJS . Этот вызов метода — по сути, весь смысл упражнения. Это то, что позволяет AngularJS связывать модель для просмотра, рекурсивно обрабатывать любые вложенные директивы и т. Д. Короче говоря, это то, что делает наше текстовое содержимое включенным в «экземпляр компонента времени выполнения». Ну, это, а также следующее:
  • … связывание атрибутов области действия между нашим включающим элементом и компонентом, который мы включаем. Это связывание достигается в последующем для цикла путем наблюдения за переменными изменениями в интересующих областях.

Этот последний пункт требует более подробного объяснения. HTML-код, который включает в себя наш компонент, предположительно имеет некоторую связанную область модели с атрибутами, относящимися к бизнес-логике. С другой стороны, включенный компонент получает свою собственную область с собственным набором атрибутов, как определено его собственным контроллером. Обе области заканчиваются отношениями родитель-потомок с областью действия директивы (третьей) между ними. С точки зрения приложения, у нас, вероятно, есть одна или несколько связанных родительских областей, содержащих соответствующие атрибуты модели, и мы бы хотели каким-то образом связать данные в нашей компонентной модели с данными в пределах объема. В приведенном выше примере мы соединяем
атрибут
shippingAddress нашей основной области приложения с
currentSelectionатрибут компонента выбора адреса. В контексте прилагаемой логики мы имеем дело с «адресом доставки», но в контексте компонента выбора адреса, который просто отображает выбор адресов для выбора, мы имеем дело с «текущим выбором». Таким образом, мы связываем две независимые концепции.

Для реализации такого рода привязки заданной пары атрибутов модели нам необходимо знать: родительскую область, дочернюю область, имя атрибута в родительской области и имя атрибута в дочерней области. Чтобы собрать пары атрибутов, мы используем вложенный тег с именем
be-model-link,  реализованный следующим образом:

app.directive('beModelLink', function() {
  return {
    restrict:'E',    
    link:function(scope, element, attrs, ctrl) {
      if (!scope.modelLinks)
        scope.modelLinks = {};
      scope.modelLinks[attrs.fromParentScope] = attrs.fromChildScope;
    }
  };
});

Поскольку мы не объявили частную область видимости для
директивы
be-model-link , получаемая область видимости является областью действия родительской директивы. Это дает нам возможность поместить в него некоторые данные. И данные, которые мы помещаем, представляют собой сопоставление атрибутов родительской модели с дочерней в форме
объекта
modelLinks . Обратите внимание, что мы ссылаемся на этот
объект
modelLink  в настройке наблюдения за переменными в
 директиве
be-plug, где мы зацикливаемся на всех его свойствах и используем
механизм $ watch AngularJS для отслеживания изменений с обеих сторон и воздействия на одно и то же изменение связанных атрибутов. Чтобы найти правильную родительскую область видимости, мы идем вверх по цепочке и получаем первую, которая имеет заявленную
исходную область видимости атрибут, выдавая ошибку, если мы не можем ее найти. Дочерняя область действия проста, потому что в нашей директиве есть только одна дочерняя область.

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