javascript

Переход с AngularJS на Angular: жизнь после AngularJS (3/3)

  • пятница, 9 февраля 2018 г. в 03:15:21
https://habrahabr.ru/company/skyeng/blog/348606/
  • Разработка веб-сайтов
  • JavaScript
  • Angular
  • Блог компании Skyeng



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


Предыдущие серии: первая, про подготовку миграции и вторая, про особенности работы в гибридном режиме.


Компиляция приложения и AoT


Одно из основных изменений в Angular – логика компиляции компонентов.


В AngularJS мы брали HTML первого компонента (самого верхнего), запихивали в DOM, ждали, пока браузер его или спарсит, или построит DOM фрагмент, или вставит в DOM, получали ссылку на полученный кусок DOM'а, парсили в нём ангулярские штуки (директивы, компоненты), повторяли для каждого найденного компонента.


В Angular используется компиляция шаблонов в js/ts в 2 вариантах — JiT и AoT. На старте приложения (в JiT режиме) фрейм берёт и компилирует все (вообще все) шаблоны всех компонентов из строки с HTML в JS-код, который создаст нужный кусок DOM. Т.е. парсинг и построение DOM фрагмента вообще никак не зависят от браузера, Angular это делает сам. После чего, начиная с верхнего компонента, он просто начинает выполнять соответствующий готовый JS-код, создающий куски DOM'а и сразу вставляющий их в нужные места дерева. После вставки дополнительный парсинг не требуется, т.к. компилятор всё уже разобрал.


AoT компиляция выполняется на сервере/локально при сборке, занимает некоторое время (не сильно много) и на выходе даёт в отдельном каталоге кучу данных для всех компонентов в виде TS файлов. Они цепляются к основному приложению, и компиляция TypeScript происходит уже с учётом скопмилированных шаблонов.


В AoT режиме шаблоны компилируются в TypeScript, а не JS. Это даёт статическую типизацию всех шаблонов. Пропустил переменную, указал её как private или обходишься с ней неподобающим образом (пытаешься пропихнуть объект в инпут компонента, ожидающего строку) – получаешь ошибку при сборке. Но это всё только в режиме сборки для продакшна, т.к. из-за тонны добавившегося типизированого кода сборка (компиляция ts -> js) несколько замедляется, и AoT режим при разработке выключен (ждём счастливых времён многопоточной компиляции или какой-нибудь магии, чтобы без страданий использовать AoT в девелоперском режиме).


Ссылки на тему: первая, вторая и третья.


Change detection (CD)


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


Как Angular узнаёт, что что-то произошло, без отдельных директив под события и специальных сервисов для таймаутов/интервалов?


Из языка Dart, на котором есть отдельная версия Angular, позаимствовали полезную концепцию зон, которая позволяет быть в курсе, когда в конкретной зоне началось выполнение какого-то кода, и когда оно закончилось с учётом асинхронных вызовов. Это всё оформилось в либу zone.js, позволяющую создавать/оперировать такими зонами и активно используется внутри движка Angular. Чтобы быть в курсе асинхронных событий, zone.js манкипатчит (подменяет на свою версию) соответствующие методы — addEventListener, setTimeout, setInterval, методы Promise и подобное. Это и избавило от необходимости отдельных директив для биндинга на события/доп сервисов. Соответственно, по окончании любого события, которое отлавливается в zone.js, Angular дёргает change detection, чтобы синхронизировать модель с DOM. В дев режиме детекшен дёргается 2 раза – второй, чтобы убедиться, что после первого ничего не изменилось, или дать по рукам (ошибка в консоли), если изменилось; в продакшене всегда ровно один раз (больше никаких десятикратных вызовов digest цикла и соответствующей ошибки).


Как проходит change detection, если нет глобального пула проверяемых параметров?


Это теперь работа компонентов. У каждого из них может быть набор параметров, которые так или иначе используются в DOM'е или специальных биндингах (HostBinding, например). Такие параметры сохраняются в отдельный список в рамках компонента (и нигде больше не известны).


Когда Angular дёргает глобальный чендж детекшен, то каждый компонент (сверху вниз по дереву) сам занимается проверкой своих параметров. При этом у компонента может быть вообще отцеплен механизм change detection, тогда его параметры проверяться не будут в принципе. Или он может быть настроен на проверку только изменений его инпутов (ChangeDetectionStrategy.OnPush), тогда он не только не будет ничего проверять, но ещё и целиком обрежет запуск проверок вложенных в него компонентов/директив. Таким образом, грамотно раскидав OnPush, мы можем исключать из проверки на каждый тик целые ветви компонентов.


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


detectChanges и markForCheck


Если у нас компонент с detached CD или на компоненте (на текущем или выше по дереву) прописано ChangeDetectionStrategy.OnPush, а у нас какие-то изменения, не подхватывающиеся автоматически, то есть 2 метода локального ChangeDetectorRef сервиса — detectChanges и markForCheck:


  • markForCheck: от текущего компонента (включительно) и вверх по дереву до самого рут-компонента помечает все вьюшки, как требующие проверки на ближайший тик глобального CD. Сам тик при этом не дёргается, а проверка вьюшек произойдёт, только если кто-нибудь ещё этот тик дёрнет (или он уже был дёрнут, но ожидает стабилизации изменений);
  • detectChanges: дёргает CD только для вьюшки текущего компонента, независимо от состояния детектора/инпутов. Для всех детей работает стандартная логика – если есть OnPush, то смотрим инпуты, если change detector отцеплен, то ничего не смотрим и игнорим. Из особенностей работы – если компонент в процессе уничтожения (или считается уничтоженым), и его вьюшка отцеплена, то вызов detectChanges в этот момент выкинет ошибку (например, решили дёрнуть по таймауту, а компонент уже уничтожен из-за смены роута, и мы дёргаем CD на несуществующей вьюшке). Ugly hack — проверка на уже убитую вьюшку (такие случаи надо избегать):

if (!this.changeDetectorRef["destroyed"]) {
   this.changeDetectorRef.detectChanges();
}

Когда что использовать:


  • если у нас компонент с OnPush, и изменения пришли 'легальным' способом (из апи, по таймауту, как угодно внутри зоны с тиком глобального CD), — юзаем markForCheck;
  • если компонент с detached CD, или что-то изменилось вне зоны ангуляра (глобальный CD явно никто не дёрнул), — юзаем detectChanges и не забываем, что компонент мог быть уничтожен.

ng-content — НЕ то же самое, что ng-transclude


Концепция трансклюда в Angular немного изменилась (под влиянием того, как это сделано в вебкомпонентах, и особенностей работы движка/компилятора ангуляра). Внешне ng-content выглядит и обладает теми же возможностями, что и ng-transclude: прокинуть что-то в компонент, прокинуть в определённое место (select атрибут).


Но есть одно мелкое отличие – отсутствует возможность указать дефолтное содержимое внутри ng-content – и одно очень важное: за инициализацию контента, прокидываемого в ng-content, отвечает тот, кто этот контент кидает, а не компонент, в котором лежит тег ng-content.


Пояснение на примере:


@Component({
  ...
  template: `
    Visible: {{ visible }}
    <ng-content *ngIf="visible"></ng-content>
  `,
})
class ProjectionComponent implements OnInit {
  public visible = false;

  public ngOnInit() {
    window.setTimeout(() => this.visible = true, 1000);
    window.setTimeout(() => this.visible = false, 2000);
  }
}

@Component({...})
class ChildComponent implements OnInit, AfterViewInit, OnDestroy {
  public ngOnInit() {
    console.log("Child component ngOnInit");
  }

  public ngAfterViewInit() {
    console.log("Child component ngAfterViewInit");
  }

  public ngOnDestroy() {
    console.log("Child component ngOnDestroy");
  }
}

@Component({
  ...
  template: `
    <vim-projection>
      <vim-child-component></vim-child-component>
    </vim-projection>
  `,
})
class ParentComponent {}

Основное внимание на ngIf в ProjectionComponent и лог консоли:


// сразу как появится `Visible` из шаблона `ProjectionComponent`
Child component ngOnInit

// спустя секунду
Child component ngAfterViewInit

// спустя 2 секунды
// ничего…

В первую секунду, несмотря на *ngIf="false", ChildComponent был инициализирован (сработал OnInit хук), а спустя 2 секунды не сработал OnDestroy, хотя вроде компонент скрыли.


Т.е. независимо от того, что происходит в компоненте, содержащем ng-content, всё, что туда пробрасывается, будет инициализировано (вызваны OnInit хуки). Это может оказаться важно, если в инициализации компонента есть какие-то запросы, тяжёлые обработки и подобное.


Если надо скрыть такой контент, то этим должен заниматься тот, кто его прокидывает:


<vim-projection>
  <vim-child-component *ngIf="visibleInParent"></vim-child-component>
</vim-projection>

или использовать интересный способ обойти это ограничение с помощью TemplateRef:


<div *ngIf="condition" class="smth1">
  <ng-container *ngTemplateOutlet="contentTpl"></ng-container>
</div>
<div *ngIf="!condition" class="smth2">
  <ng-container *ngTemplateOutlet="contentTpl"></ng-container>
</div>

<div *ngIf="condition2" class="smth1">
  <ng-container *ngTemplateOutlet="contentWithSelectorTpl"></ng-container>
</div>
<div *ngIf="!condition2" class="smth2">
  <ng-container *ngTemplateOutlet="contentWithSelectorTpl"></ng-container>
</div>

<ng-template #contentTpl><ng-content></ng-content></ng-template>
<ng-template #contentWithSelectorTpl><ng-content select="[some-attribute]"></ng-content></ng-template>

Из ограничений — вставлять такой ng-content можно только в одно место за раз.


Также ng-content не позволяет в шаблоне указать несколько одинаковых слотов, над которыми висит условие, когда их выводить.


<!-- так НЕЛЬЗЯ -->
<div *ngIf="smth" class="smth1">
 <ng-content></ng-content>
</div>
<div *ngIf="!smth" class="smth2">
 <ng-content></ng-content>
</div>

<!-- так тоже нельзя -->
<div *ngIf="smth" class="smth1">
 <ng-content select="[some-selector]"></ng-content>
</div>
<div *ngIf="!smth" class="smth2">
 <ng-content select="[some-selector]"></ng-content>
</div>

Ссылки на полезные материалы


В гугле множество всего по ангуляру 2+, можно смело искать на любые темы, которые будут сходу/по докам не понятны. Народ очень активно с релиза (да и до него) разбирал фрейм на части и выяснял, как оно работает.


Вот немного полезных ссылок:


ElementRef, TemplateRef, ViewContainerRef


Structural Directives 1


Structural Directives 2


ViewChildren, ContentChildren, QueryList


Dynamic components 1


Dynamic components 2


Change Detection


ng-template и микросинтаксис структурных директив


Ну и напоследок, как всегда, напоминаем, что мы всегда ищем крутых разработчиков (не только Angular).