Spec-Zone.ru › Angular.js 1.4

Улучшить эту документациюКомпилятор HTML

Примечание: это руководство предназначено для разработчиков, уже знакомых с основами AngularJS. Если вы только начинаете, рекомендуем сначала ознакомиться с туториалом. Если вы хотите создать пользовательские директивы, рекомендуем руководство по директивам. Если вы хотите более глубоко изучить процесс компиляции в Angular, вы попали по адресу.

Обзор

Компилятор HTML Angular позволяет разработчику обучить браузер новому синтаксису HTML. Компилятор позволяет вам привязать поведение к любому HTML-элементу или атрибуту и даже создавать новые HTML-элементы или атрибуты с пользовательским поведением. Angular называет эти расширения поведения директивами.

HTML содержит множество конструкций для форматирования HTML для статических документов декларативным способом. Например, если что-то нужно выровнять по центру, не нужно предоставлять инструкции браузеру о том, как разделить размер окна пополам, чтобы найти центр, и как выровнять этот центр с центром текста. Просто добавьте атрибут align="center" к любому элементу, чтобы достичь желаемого поведения. Такова сила декларативного языка.

Однако декларативный язык также ограничен, так как не позволяет обучить браузер новому синтаксису. Например, нет простого способа заставить браузер выровнять текст в 1/3 позиции вместо 1/2. Что нужно, так это способ обучить браузер новому синтаксису HTML.

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

Вся эта компиляция происходит в веб-браузере; не участвует серверная сторона или предварительная компиляция.

Компилятор

Компилятор — это служба Angular, которая проходит по DOM, ища атрибуты. Процесс компиляции происходит в двух фазах.

  1. Компиляция: проходит по DOM и собирает все директивы. Результатом является функция связывания.

  2. Связывание: объединяет директивы со scope и создаёт живой вид. Любые изменения в модели scope отражаются в представлении, а любые взаимодействия пользователя с представлением отражаются в модели scope. Это делает модель scope единственным источником истины.

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

Директива

Директива — это поведение, которое должно срабатывать при обнаружении определённых HTML-конструкций во время процесса компиляции. Директивы могут быть размещены в именах элементов, атрибутах, именах классов, а также в комментариях. Вот несколько эквивалентных примеров вызова директивы ng-bind.

<span ng-bind="exp"></span>
<span class="ng-bind: exp;"></span>
<ng-bind></ng-bind>
<!-- directive: ng-bind exp -->

Директива — это просто функция, которая выполняется, когда компилятор находит её в DOM. Подробную документацию по написанию директив см. в API директив.

Вот директива, которая делает любой элемент перетаскиваемым. Обратите внимание на атрибут draggable на элементе <span>.

Наличие атрибута draggable на любом элементе даёт элементу новое поведение. Мы расширили словарный запас браузера естественным для всех, кто знаком с принципами HTML, способом.

Понимание представления

Большинство других систем шаблонов используют статический шаблон строки и объединяют его с данными, результатом чего является новая строка. Результирующий текст затем innerHTML в элемент.

Это означает, что любые изменения в данных необходимо повторно объединить с шаблоном, а затем innerHTML в DOM. Некоторые проблемы с этим подходом:

  1. чтение пользовательского ввода и объединение его с данными
  2. затирание пользовательского ввода заменой его
  3. управление всем процессом обновления
  4. отсутствие выразительности поведения

Angular отличается. Компилятор Angular обрабатывает DOM, а не строковые шаблоны. Результатом является функция связывания, которая, когда объединяется с моделью scope, даёт живое представление. Связи между представлением и моделью scope прозрачны. Разработчику не нужно выполнять какие-либо специальные вызовы для обновления представления. И поскольку innerHTML не используется, вы не случайно затрете пользовательский ввод. Кроме того, директивы Angular могут содержать не только текстовые привязки, но и конструкции поведения.

Подход Angular приводит к стабильному DOM. Экземпляр элемента DOM, привязанный к экземпляру элемента модели, не изменяется на протяжении всего времени существования привязки. Это означает, что код может получить доступ к элементам, зарегистрировать обработчики событий и знать, что ссылка не будет уничтожена объединением данных шаблона.

Как компилируются директивы

Важно отметить, что Angular работает с узлами DOM, а не со строками. Обычно вы этого не замечаете, потому что при загрузке страницы веб-браузер автоматически анализирует HTML в DOM.

Компиляция HTML происходит в три этапа:

  1. $compile проходит по DOM и ищет директивы.

    Если компилятор обнаруживает, что элемент соответствует директиве, то директива добавляется в список директив, соответствующих элементу DOM. Один элемент может соответствовать нескольким директивам.

  2. После того, как все директивы, соответствующие элементу DOM, будут идентифицированы, компилятор сортирует директивы по их priority.

    Выполняются функции compile каждой директивы. Каждая функция compile имеет возможность изменить DOM. Каждая функция compile возвращает функцию link. Эти функции объединяются в «объединённую» функцию связывания, которая вызывает возвращённую функцию link каждой директивы.

  3. $compile связывает шаблон со scope, вызывая объединённую функцию связывания из предыдущего шага. Это, в свою очередь, вызовет функцию связывания отдельных директив, зарегистрирует слушателей на элементах и настроит $watch со scope, так как каждая директива настроена на это.

В результате получается живая привязка между scope и DOM. Таким образом, в этот момент изменение модели в скомпилированном scope будет отражено в DOM.

Ниже приведен соответствующий код, использующий службу $compile . Это должно помочь вам понять, что делает Angular внутренне.

var $compile = ...; // injected into your code
var scope = ...;
var parent = ...; // DOM element where the compiled template can be appended

var html = '<div ng-bind="exp"></div>';

// Step 1: parse HTML into DOM element
var template = angular.element(html);

// Step 2: compile the template
var linkFn = $compile(template);

// Step 3: link the compiled template with the scope.
var element = linkFn(scope);

// Step 4: Append to DOM (optional)
parent.appendChild(element);

Разница между компиляцией и связыванием

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

Редко директивы имеют **функцию компиляции**, так как большинство директив работают с определённым экземпляром элемента DOM, а не с его общей структурой.

Директивы часто имеют **функцию связывания**. Функция связывания позволяет директиве регистрировать слушателей на конкретный клонированный экземпляр элемента DOM, а также копировать содержимое из scope в DOM.

Лучшая практика: Любая операция, которую можно совместно использовать между экземплярами директив, должна быть перенесена в функцию компиляции по причинам производительности.

Пример «компиляции» против «связывания»

Чтобы понять, рассмотрим реальный пример с ngRepeat:

Hello {{user.name}}, you have these actions:
<ul>
  <li ng-repeat="action in user.actions">
    {{action.description}}
  </li>
</ul>

При компиляции вышеприведённого примера компилятор посещает каждый узел и ищет директивы.

{{user.name}} соответствует директиве интерполяции, а ng-repeat соответствует директиве ngRepeat.

Но ngRepeat сталкивается с проблемой.

Ей нужно иметь возможность клонировать новые элементы <li> для каждого элемента action в user.actions . Это, на первый взгляд, кажется тривиальным, но становится сложнее, если учесть, что в user.actions могут позже добавляться элементы. Это означает, что она должна сохранить чистую копию элемента <li> для целей клонирования.

По мере вставки новых action шаблон <li> элемент нужно клонировать и вставить в ul . Но клонирования элемента <li> недостаточно. Ей также нужно скомпилировать <li> , чтобы её директивы, такие как {{action.description}}, оценивались с правильным scope.

Примитивный подход к решению этой проблемы заключался бы в простом вставлении копии элемента <li> и последующей его компиляции. Проблема с этим подходом заключается в том, что компиляция при каждом элементе <li> , который мы клонируем, будет дублировать значительную часть работы. То есть, мы будем проходить по <li> каждый раз перед клонированием, чтобы найти директивы. Это сделает процесс компиляции медленнее, в свою очередь, уменьшит отзывчивость приложений при вставке новых узлов.

Решение состоит в разделении процесса компиляции на две фазы:

**фазу компиляции**, где все директивы идентифицируются и сортируются по приоритету, и **фазу связывания**, где выполняется любая работа, которая «связывает» конкретный экземпляр scope и конкретный экземпляр элемента <li> .

Примечание: Связывание означает настройку слушателей на DOM и настройку $watch на Scope для синхронизации их работы.

ngRepeat работает, предотвращая спуск процесса компиляции в элемент <li> , чтобы она могла сделать клон оригинала и управлять вставкой и удалением узлов DOM самостоятельно.

Вместо этого директива ngRepeat компилирует <li> отдельно. Результатом компиляции элемента <li> является функция связывания, которая содержит все директивы, содержащиеся в элементе <li> , готовые к подключению к конкретному клону элемента <li> .

При выполнении ngRepeat отслеживает выражение, и по мере добавления элементов в массив клонирует элемент <li>, создает новый объект scope для клонированного элемента <li> и вызывает функцию link для клонированного <li>.

Понимание работы объектов scope с трансклюдированными директивами

Одно из наиболее распространенных применений директивы — создание переиспользуемых компонентов.

Ниже представлен псевдокод, демонстрирующий работу упрощенного компонента диалога.

<div>
  <button ng-click="show=true">show</button>

  <dialog title="Hello {{username}}."
          visible="show"
          on-cancel="show = false"
          on-ok="show = false; doSomething()">
     Body goes here: {{username}} is {{title}}.
  </dialog>
</div>

Нажатие на кнопку «Показать» откроет диалог. Диалог будет иметь заголовок, связанный данными с username, и тело, которое мы хотим трансклюдировать в диалог.

Вот пример того, как может выглядеть определение шаблона для виджета dialog.

<div ng-show="visible">
  <h3>{{title}}</h3>
  <div class="body" ng-transclude></div>
  <div class="footer">
    <button ng-click="onOk()">Save changes</button>
    <button ng-click="onCancel()">Close</button>
  </div>
</div>

Это не будет отображаться должным образом, если мы не применим некоторые трюки с объектом scope.

Первая проблема, которую нужно решить, заключается в том, что шаблон диалогового окна ожидает, что title будет определен. Но мы хотим, чтобы свойство scope шаблона title было результатом интерполяции атрибута <dialog> элемента (т.е. "Hello {{username}}"). Кроме того, кнопки ожидают, что в объекте scope будут присутствовать функции onOk и onCancel. Это ограничивает полезность виджета. Для решения проблемы сопоставления мы используем scope для создания локальных переменных, которые шаблон ожидает следующим образом:

scope: {
  title: '@',             // the title uses the data-binding from the parent scope
  onOk: '&',              // create a delegate onOk function
  onCancel: '&',          // create a delegate onCancel function
  visible: '='            // set up visible to accept data-binding
}

Создание локальных свойств в объекте scope виджета создает две проблемы:

  1. изоляция — если пользователь забудет установить атрибут title виджета диалога, шаблон диалога будет привязываться к свойству родительского scope. Это непредсказуемо и нежелательно.

  2. трансклюзия — трансклюдированная DOM-структура может видеть локальные переменные виджета, что может перезаписывать свойства, необходимые для привязки данных. В нашем примере свойство title виджета перекрывает свойство title трансклюзии.

Для решения проблемы отсутствия изоляции директива объявляет новый изолированный isolated scope. Изолированный scope не наследует свойства от родительского scope, и поэтому нам не нужно беспокоиться о случайном перекрытии свойств.

Однако изолированный isolated scope создает новую проблему: если трансклюдированная DOM-структура является дочерним элементом изолированного scope виджета, то она не сможет привязаться ни к чему. По этой причине трансклюдированный scope является дочерним элементом исходного scope до того, как виджет создал изолированный scope для своих локальных переменных. Это делает трансклюдированный и изолированный scope виджета «братьями».

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

Поэтому окончательное определение директивы выглядит примерно так:

transclude: true,
scope: {
    title: '@',             // the title uses the data-binding from the parent scope
    onOk: '&',              // create a delegate onOk function
    onCancel: '&',          // create a delegate onCancel function
    visible: '='            // set up visible to accept data-binding
},
restrict: 'E',
replace: true

© 2010–2017 Google, Inc.
Licensed under the Creative Commons Attribution License 4.0.
https://code.angularjs.org/1.4.14/docs/guide/compiler

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API