Перевод: Angular 2 NgModule Intro — Ahead Of Time Compilation And Lazy Loading — Avoid Common Pitfalls

В этой статье мы расскажем о модульности в Angular 2 (функциональность NgModule), а так же о таких вещах как Ahead of time компиляции AOT(метод компиляции перед исполнением) и lazy loading (отложенной загрузки). Мы рассмотрим следующие темы:

  • Что такое модули в Angular 2 ?
  • Angular 2 модули против ES6 модулей
  • Что такое Root (корневой) модуль ?
  • Создания модулей более удобными для чтения с помощью оператора spread (Оператор расширения)
  • Angular 2 модули и видимость
  • Angular 2 модули и внедрение зависимостей — избегаем сюрпризы
  • Dynamic bootstrapping (Динамическая начальная загрузка)
  • Angular 2 Ahead Of Time компиляция в действии
  • Static bootstrapping (Статическая начальная загрузка)
  • Функция модуль
  • Angular 2 модули и Router (роутер)
  • Lazy Loading (отложенная загрузка) модулей используя Router
  • Общие модули и отложенная загрузка
  • Заключение

Что такое модули в Angular 2 ?

Имя Module иногда в программирование может значить несколько вещей, в нашем случае использования термина «Module» это связь с предыдущей версией AngularJs терминологией.

Angular 2 Modules близкий аналог AngularJs modules, поэтому решили использовать такой же термин.

Так что же такое модули в Angular 2? Можно начать с чтения официальной документации документация:

Модули Angular объединяют компоненты, директивы и др. в единые функциональные блоки … Модули так же могут содержать сервисы

Итак модули в Angular 2 используются для объединения составных компонентов Angular 2 проектов.

Пример модуля

Хороший пример модуля директивы реактивных форм и сервисов.

Или другой пример, внедряемого сервиса такого как FormBuilder, с функциональностью привязанной к его директивам.

Еще пример директив роутинга и сервисов жестко связанных с формами и представляющих единое целое.

Так модульность может бы на уровне приложения: представьте приложение которое разбито на две части полностью разделенных экранов; мы можем разделить это приложения на два независимых модуля.

Как выглядят модули в Angular 2?

Далее пример модуля в Angular 2:

 

Здесь происходит следующее:

  • аннотация @NgModule определяет сам модуль
  • в declarations список компотентов, директив и pipe из которых будет состоять модуль
  • мы можем импортировать другие модули в списке imports
  • сервисы модуля перечислены в списке providers, далее вы узнаете почему это лучше делать только в определенных случаях

Эта декларативная группирова полезна не только полезна для организации приложения но так же может служить дополнительной функциональной документацие приложения.

Но модули Angular 2 намного больше чем просто организация приложения, что же Angular 2 делает с этой информацией?

Почему важны модули Angular 2 ?

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

Каждый HTML тег сравнивается с этим списком на предмет должен ли текущий компонент применен, для текущего элемента или нет. Но откуда Angular знает какие компоненты, директивы и фильтры нужно искать во время парсинга HTML?

Вот тогда модули Angular 2 выполняют свою функцию, они обеспечивают точную информацию в одном месте.

То есть если коротко Angular 2 модули:

  • необходимы для парсинга шаблонов, в сценариях Just In Time или Ahead Of Time о них мы расскажем позже
  • они так же полезна с точки зрения документирования проекта для группирования всей функциональности
  • они еще полезны для определения какой компонент и директива подразумевает публичное использование.

Модули Angular 2 против ES6 модулей

Модули Angular 2 сильно отличаются от модулей ES6: ES6 это формализованные типичные модули Javascript которые сообщества используется уже много лет: оборачивают внутрение детали функций в замыкание и выставляют наружу только публичное API.

Модули Angular 2 это в большей степени контекст компиляции шаблонов и также помогают с определением публичное API в группу функций так же как и внедрение зависимостей (dependency injection) в нашем приложение.

Модули Angular 2 на самом деле являются одним из основных инструментов реализации быстрых и мобильных приложений. Рассмотрим различные типы модулей и их использование.

Что такое корневой модуль (Root Module) ?

Каждое приложение может иметь только один корневой модуль, и каждый компонент, директива и фильтр должен ассоциироваться только с одним модулем.

Далее пример корневого модуля:

Несколько вещей которые определяют этот модуль как корневой:

  • корневой модуль имеет имя AppModule (это не обязательное имя, просто так принято)
  • корневой модуль в случае веб приложения импортирует BrowserModule, который в том числе обеспечивает зависимые от типа браузера рендеринг страниц, и инсталирует базовые директивы такие как ngIf, ngFor, и др.
  • свойство bootstrap используется для создания списка компонентов которые должны быть использованы при начальной загрузке приложения. Обычно в этом списке только один элемент: корневой компонент приложения

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

Делам модули более простыми с помощью оператора расширения (spread)

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

 

Мы сможем импортировать константы в наш модуль используя оператор расширения ... operator:

 

Angular 2 модули и область видимости

Для понимания как определяется области видимости для модулей в Angular 2, давай те определим отдельный модуль с одним компонентом и назовем его Home:

Теперь попробуем его использовать в нашем корневом модуле:

Вы возможно удивитесь но это не сработает. Если вы используете компонент <home></home> в вашем шаблоне, ничего не отобразиться.

Почему компонент Home не видим ?

Добавляя компонент Home в декларацию (declarations) HomeModule не делает его автоматически видимым в других модулях которые его импортируют.

Это потому что Angular не знает наших намерений и возможно нам нужно только создать модуль Home но не делать его компоненты публично доступными.

Что бы сделать компоненты модуля публично доступными, нам нужно экпортировать их:

После этого компонент Home будет корректно отображаться в любом шаблоне где используется тег home.

Мы так же может только экпортировать его без добавления в declarations. Это потребуется в том случае если компонент не используется внутри модуля.

Можем ли мы импортировать компонент напрямую ?

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

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

Angular 2 модули и внедрение зависимостей

Как насчет сервисов и providers, сможем ли мы их использовать ?

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

Давай те посмотри, правда ли это. Начнем с создания простого сервиса:

Сейчас добавим сервис к HomeModule:

Сейчас как мы и ожидаем сервис доступен в компоненте Home:

 

Но проблема в том что новый модуль не создает его собственный контекст внедренных зависимостей! Сервис lessons будет автоматически добавлен в глобальный контекст внедренных зависимостей.

Это означает что LessonsService доступен для внедрения в любом месте приложения, включая:

  • корневой компонент
  • любой компонент модуля HomeModule
  • любой компонент любого другого модуля в общем

Но контейнер внедренных зависимостей Angular 2 имеет иерархическую структуру, это означает что в отличие от AngularJs мы можем создать отдельный контекст. Так почему же это не случилось?

Почему не отделяется DI конекст создаваемый по умолчанию ?

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

Обычно цель состоит не в том что бы на создавать небольшие отдельные приложения внутри главного приложения.

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

Это помогает предотвращает следующие ошибочные ситуации:

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

Но как насчет отложенной загрузки (lazy-loading) ?

Одним из недостатков Angular 1 заключается в том что контейнер внедряемых зависимостей не иерархический: все находится в одной большой корзине.

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

Рассмотрим как использовать корневой модуль приложения.

Динамическая загрузка и Just In Time компиляция

Angular 2 модули определяют контекст компиляции шаблонов, но как он передается в компилятор при загрузке приложения?

Вариант которых хорош при разработке это загрузить компилятор Angular 2 в браузер, и динамически загрузить приложение:

Этот код скомпилирует все шаблоны и запустить приложение и это нормальная практика вовремя разработки приложения.

Так же существует другой вариант запуска приложения для использования в продакшине.

Angular 2 Ahead Of Time компиляция (предварительная компиляция)

Альтернативный вариант это использование информации о модуле перед компиляцией. Angular-cli позволит сделать это прозрачно. Что бы понять как это работает установите компилятор в ручную следуя гиду разработчика:

Это установит команду ngc которую можно использовать для компиляции всех шаблонов приложения. И именно это команда используется в компиляторе Typescript.

Как мы можем использовать ngc компилятор для компиляции шаблонов ?

Команда ngc ищет Angular 2 классы компонентов и создает Typescript файлы. Пример запуска:

Она пройдется по всем src содержащими tsconfig.json в вашем приложение. В случае нашего main.ts, будет создан файл с именем main.ngfactory.ts, который будет содержать что то типа такого:

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

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

Но как мы можем использовать эти фабрики классов для запуска нашего приложения?

Статический запуск приложения

Мы можем статически запустить простой сгенерированной фабрикой на ES5 Javascript:

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

И все будет сделано прозрачно используя CLI:

  • ng serve будет работать в режиме Just In Time Mode
  • ng build -prod && ng serve -prod будет работать в режиме Ahead Of Time

Как насчет отложенной загрузки, как она связана с модулями? Вначале давайте рассмотрим свойства модулей, для понимания концепции отложенной загрузки.

Функция модуль

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

В нашей текущей версии HomeModule есть что то ошибочное в определении:

 

Что бы понять в чем проблема давайте попробуем использовать базовые директивы Angular 2 в Home шаблоне, например ngStyle:

Если вы попытаетесь запустить, то получите ошибку:

Но ngStyle должна была бы быть импортирована в приложение через BrowserModule, которая включена в CommonModule где ngStyle определена. Так почему же это работает в компонентах приложения но не работает в компоненте Home?

Это потому что HomeModule сама по себе не импортирует CommonModule где ngStyle определена.

Внутри каждого модуля определяется его собственный контекст отдельно от других, поэтому решение проблемы здесь это импортировать CommonModule:

Что нам дает функция модуль: она импортирует CommonModule, и предоставляет все необходимые компоненты и сервисы.

Функции модули и отложенная загрузка

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

Но тогда у нас может быть проблемы с внедряемыми зависимостями и вот почему.

Angular 2 модули и роутер (Router)

Для начало добавим роутер в наше приложение:

 

Здесь мы определили /home url и привязали к нему компонент Home. Когда пользователь перейдет на ссылку /home компонент Home отобразиться там где указан HTML тег <router-outlet>.

Если захотите узнать больше о роутерах взгляните на пост введение в роутеры.

Отложеная загрузка модуля Home

Сейчас мы улучшим код для того чтобы компонент Home и все что внутри HomeModule загружалось бы по мере необходимости. Это означает что все связанное с HomeModule будет загруженно только когда будет переход по ссылке (смотри App HTML шаблон выше ) в компоненте App, а не во время первоначальной загрузке.

Вначале нам нужно удалить любое упоминание компоненте Home или HomeModule из компонента App и главного определения роутинга:

 

Здесь мы можем видеть что компонент App больше не импортирует HomeModule, вместо этого используется loadChildren что бы сказать что в случае перехода по ссылке /home загружаем файл home.module через Ajax.

В зависимости от конфигурации загрузчика будет загружен или home.module.js или home.module.ts.

Как выгдядит модуль отложенной загрузки

Давайте взглянем на текущую версию HomeModule:

 

С этой небольшой корректировкой теперь у нас есть полноценный модуль отложенно! Здесь мы можем видеть:

  • модуль HomeModule определяет его собственную конфигурацию роутинга, которая будет добавлена к основной конфигурации но при этом она будет привязана к пути /home
  • The HomeModule экпортируется с ключевым словом default: это очень важно в противном бы случае роутер не будет знать что имортировать их этого файла, потому что нет информции о имени необходомого экпорта; роутер будет знать только о имени файла модуля
  • Конфигурация роутинга HomeModule добавляется через forChild, мы будем точно поимать что это и почему это необходимо

Чем это отличается от импорта модуля с не отложенной загрузкой?

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

Для того что бы избежать этого Angular 2 будет создать отдельные контексты для внедряемых зависимостей. DI контекст Home будет содержать LessonsService, но этот сервис будет недоступен для остальной части приложения.

Этот сервис будет доступен компоненту Home, поэтому если вы попытаетесь использовать его, то теперь это будет работать:

Но если для примера мы попытаемся использовать его в главном компоненте App мы получим ошибку:

Будет следующая ошибка:

Возможно несколько версий внедренных зависимостей с одним и тем же именем

Что если мы определить еще одну версию LessonsService:

Сейчас мы определили альтернативную еще одну версию LessonsService которая определяется в файле other-lessons.service.ts и добавим его в конфигурацию AppModule в providers:

И это будет работать: Компонент App и компонент Home использую разные версии LessonsService.

Последняя проблема с модулями и отложеннной загрузкой

Мы почти прошлись по всему тому что нужно знать о модулях, и последняя вещь которую еще нужно знать о модулях это общие модули (Shared Modules) и их связь с отложенной загрузкой и вызов из forRoot и forChild.

Общие модули

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

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

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

Мы можем просто импортировать везде где нам понадобиться, для примера на уровне AppModule:

Но вдруг нам так же понадобится использовать App внутри HomeModule:

Не будет ни каких ошибок, но появилась проблема в том что сейчас у нас две версии AuthenticationService:

  • один экземпляр создается во время запуска и внедряется в App
  • второй экземпляр создается когда мы переходим по ссылке Home которая вызывает отложенную загрузку HomeModule

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

Нам то нужно что бы сервис был один (сингельтон) на все приложение. Как же нам решить проблему? Вот где методы forRoot и forChild необходимы.

Общие модули и отложенная загрузка

Если нам нужно использовать общие модули мы должны придерживаться следующего правила:

Общие модули не могут определять сервисы декларируемые в свойстве providers

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

  • создавать эеземпляр AuthenticationService когда добавляем его к корневому модулю
  • этот экземпляр будет автоматически доступен в подчиненых DI контекстах, таком как контекст HomeModule
  • недопускать создание второго экземпляра сервиса, удалив его объявление из providers

Определим это для нашего примера методом forRoot в SharedModule:

 

Обратите что мы удалили AuthenticationService из массива providers. Что означает что когда мы импортируем этот модуль в HomeModule мы не будем создавать дубликат.

Сейчас мы можем использовать этот метод при импорте в главном модуле:

Так как в имени модуля есть forRoot, Angular теперь будет знать как использовать его:  будет создан контекст модуля а сервисы необходимо объявлять в providers.

Завершение

В этой статье мы рассказали почему модули в Angular 2 необходимы: они позволяют группировать функциональность вашего приложения в отдельные блоки и так же необходимы при использование предварительной компиляции a head of time (что позволяет увеличить скорость загрузки приложения ) а еще необходимы при использование отложенной загрузке.

Модули очень полезны но помните о проблемах при их использования:

  • не объявляйте компоненты, директивы и т.п. более чем в одном модуле
  • модули не создают их собственны DI контексты, поэтому внедреные зависимости будут так же доступны вне модуля
  • если модуль не использует отложенную загрузку отдельный DI контекст будет создан роутером
  • если модуль общий то необходимо для него использовать отложенную загрузку и его нельзя объявлять в providers, что бы измежать создания дубликтов экземпляров модуля
It's only fair to share...Share on FacebookShare on Google+Tweet about this on TwitterShare on LinkedInShare on VK