[Перевод] Angular 6 и движок рендеринга Ivy
Добрый день, коллеги. Мы обдумываем, стоит ли обновить книгу Якова Файна и Антона Моисеева «Angular и TypeScript. Сайтостроение для профессионалов». Новое издание выходит этой осенью и включает материал об Angular 5 и 6.
Сначала мы думали опубликовать материал о движке Ivy, который, вероятно, будет самым интересным нововведением в Angular 6, но потом остановились на более обзорной публикации от Седрика Эксбраята (оригинал вышел в мае).
В Angular 6 появилось немало серьезных нововведений, причем, важнейшее из них фичей не назовешь: это Ivy, новый движок рендеринга. Поскольку движок пока экспериментальный, о нем мы поговорим в конце этой статьи, а начнем с других новых фич и революционных изменений.
Tree-shakeable провайдеры
Теперь появился новый, рекомендуемый способ регистрации провайдера, непосредственно в декораторе @Injectable()
, с применением нового атрибута providedIn
. Он принимает 'root'
в качестве значения любого модуля вашего приложения. При использовании 'root'
внедряемый объект будет регистрироваться в приложении как одиночка, и вам не потребуется добавлять его к провайдерам в корневом модуле. Аналогично, при применении providedIn: UsersModule
внедряемый объект регистрируется как провайдер UsersModule
, и к провайдерам модуля не добавляется.
@Injectable({
providedIn: 'root'
})
export class UserService {
}
Такой новый способ был введен для более качественного удаления нефункционального кода в приложении (tree-shaking). В настоящее время ситуация такова, что сервис, добавляемый к providers модуля, окажется в финальном наборе, даже если он не используется в приложении — и допускать такое немного грустно. Если же вы применяете ленивую загрузку, то можете угодить сразу в несколько ловушек, либо оказаться в ситуации, когда сервис будет занесен не в тот набор.
Такая ситуация в приложениях вряд ли случается часто (если вы пишете сервис, то используете его), но в сторонних модулях иногда предлагаются сервисы, которые нам не нужны — в итоге имеем целый ворох бесполезного JavaScript.
Итак, данная возможность будет особенно полезна разработчикам библиотек, но теперь рекомендуется регистрировать внедряемые объекты именно таким образом — это касается и разработчиков приложений. В новом CLI теперь даже по умолчанию применяется скаффолдинг providedIn: 'root'
при работе с сервисами.
В том же духе, теперь можно объявить InjectionToken
, напрямую зарегистрировать его с providedIn
и добавить сюда factory
:
export const baseUrl = new InjectionToken('baseUrl', {
providedIn: 'root',
factory: () => 'http://localhost:8080/'
});
Обратите внимание: при этом также упрощается модульное тестирование. Для целей такого тестирования привыкли регистрировать сервис в провайдерах тестового модуля. Вот как мы поступали ранее:
beforeEach(() => TestBed.configureTestingModule({
providers: [UserService]
}));
Теперь, если UserService использует providedIn: 'root'
:
beforeEach(() => TestBed.configureTestingModule({}));
Только не волнуйтесь: все сервисы, зарегистрированные с providedIn
, не загружаются в тест, а лениво инстанцируются, только в тех случаях, когда они действительно нужны.
RxJS 6
Angular 6 теперь внутрисистемно использует RxJS 6, так что вам потребуется обновить приложение с учетом этого.
И… RxJS 6 меняет подход к импорту!
В RxJS 5 вы могли бы написать:
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/of';
import 'rxjs/add/operator/map';
const squares$: Observable = Observable.of(1, 2)
.map(n => n * n);
В RxJS 5.5 появились конвейеризуемые (pipeable) операторы:
import { Observable } from 'rxjs/Observable';
import { of } from 'rxjs/observable/of';
import { map } from 'rxjs/operators';
const squares$: Observable = of(1, 2).pipe(
map(n => n * n)
);
А в RxJS 6.0 изменились импорты:
import { Observable, of } from 'rxjs';
import { map } from 'rxjs/operators';
const squares$: Observable = of(1, 2).pipe(
map(n => n * n)
);
Итак, однажды вам придется поменять импорты в пределах всего приложения. Я пишу «однажды», а не «прямо сейчас», поскольку в RxJS была выпущена библиотека rxjs-compat, позволяющая прокачать RxJS до версии 6.0, даже если во всем вашем приложении или в одной из используемых библиотек по-прежнему применяются «старые» варианты синтаксиса.
Команда Angular написала целый документ на эту тему, и его совершенно необходимо прочесть перед миграцией на Angular 6.0.
Обратите внимание: здесь упоминается очень классный набор правил tslint под названием rxjs-tslint
. В нем всего 4 правила, и, если добавить их в проект, система автоматически выполнит миграцию всех ваших импортов и кода RxJS, и это делается простейшим tslint --fix
! Ведь, если вы еще не знаете, в tslint
есть опция fix
, автоматически исправляющая все ошибки, которые найдет! Ее можно использовать даже еще проще: глобально установить rxjs-tslint
и запустить rxjs-5-to-6-migrate -p src/tsconfig.app.json
. Я попробовал rxjs-tslint
в одном из наших проектов, и она сработала вполне хорошо (запустите ее как минимум дважды, чтобы также свернуть все импорты). Ознакомьтесь с README этого проекта, если хотите разобраться подробнее: github.com/ReactiveX/rxjs-tslint.
Если вам интересно дополнительно изучить RxJS 6.0, рекомендую следующий доклад Бена Леша на ng-conf.
i18n
Важнейшая перспектива, связанная с i18n — это возможность сделать «i18n на время исполнения», без необходимости собирать приложение отдельно для каждой локальной точки. Пока эта возможность недоступна (есть лишь прототипы), и для ее работы потребуется движок Ivy (подробнее о нем ниже).
Другое изменение, связанное с i18n, уже состоялось и доступно. Канал currency оптимизирован самым дельным образом: теперь он округляет все валюты не до 2 знаков, как ранее, а до нужного количества знаков (например, до 3 в случае бахрейнского динара или до 0 у чилийского песо).
Если требуется, это значение можно извлечь программно при помощи новой функции i18n getNumberOfCurrencyDigits
.
В общем доступе также появились другие удобные функции форматирования, например formatDate
, formatCurrency
, formatPercent
и formatNumber
.
Достаточно удобно, если требуется применить те же преобразования, что делаются в каналах, но сделать это из кода TypeScript.
Анимации
В Angular 6.0 анимации уже возможны и без полифилла web-animations-js
, если только вы не используете AnimationBuilder
. Ваше приложение может выиграть несколько драгоценных байт! В случае, если браузер не поддерживает API element.animate
, Angular 6.0 откатывается к применению ключевых кадров CSS.
Элементы Angular
Angular Elements — проект, позволяющий обертывать компоненты Angular в виде веб-компонентов и встраивать их в приложение, не использующее Angular. Первое время этот проект существовал лишь в «Лаборатории Angular» (то есть, он до сих пор экспериментальный). С выходом v6 он немного выдвигается на передний план и официально включается в состав фреймворка. Это большая тема, заслуживающая отдельной статьи.
ElementRef
Если вы хотите взять у себя в шаблоне ссылку на элемент, то можете воспользоваться @ViewChild
или @ViewChildren
, или даже напрямую внедрить ElementRef
. Недостаток в данном случае таков: в Angular 5.0 или ниже указанный ElementRef
получит для свойства nativeElement
тип any
.
В Angular 6.0 вы можете типизировать ElementRef строже, если желаете:
@ViewChild('loginInput') loginInput: ElementRef;
ngAfterViewInit() {
// nativeElement теперь `HTMLInputElement`
this.loginInput.nativeElement.focus();
}
Что признается нежелательным, а что принципиально меняется
Поговорим о том, что обязательно нужно держать в уме, приступая к миграции!
preserveWhitespaces
: по умолчанию false
В разделе «неприятности, которые могут произойти при обновлении» отметим, что preserveWhitespaces теперь по умолчанию равно false
. Эта опция появилась в Angular 4.4, и, если вам интересно, чего при этом ожидать — вот целый пост на эту тему. Спойлер: все может обойтись, а может полностью поломать ваши шаблоны.
ngModel
и реактивные формы
Раньше было можно снабдить одно и то же поле формы и ngModel
, и formControl
, но сегодня такая практика признана нежелательной и перестанет поддерживаться в Angular 7.0.
Здесь возникает небольшая путаница, и весь механизм работал, пожалуй, не так как вы ожидали (ngModel
— это была не давно знакомая вам директива, а ввод/вывод директивы formControl
, выполняющей практически такую же, но не идентичную задачу).
Итак, теперь, если мы применим код:
то получим предупреждение.
Можно сконфигурировать приложение так, чтобы оно выдавало предупреждение always
(всегда), once
(однократно) или never
(никогда). По умолчанию действует always
.
imports: [
ReactiveFormsModule.withConfig({
warnOnNgModelWithFormControl: 'never'
});
]
Так или иначе, готовясь к переходу на Angular 7, нужно приспособить код к использованию либо шаблон-ориентированных форм, либо реактивных форм.
Проект Ivy: новый (новый) движок рендеринга в Angular
Тааак…. Это 4-й крупный релиз Angular (2, 4, 5, 6), и движок рендеринга переписывается уже в 3-й раз!
Напоминаем: Angular компилирует ваши шаблоны в эквивалентный код TypeScript. Затем этот TypeScript компилируется вместе с тем TypeScript, который вы записали в JavaScript, и полученный результат поступает в распоряжение пользователя. А перед нами уже 3-я версия данного движка рендеринга в Angular (первая была в исходном релизе Angular 2.0, а вторая — в Angular 4.0).
В этой новой версии движка рендеринга подход к написанию шаблонов не меняется, однако, в нем оптимизируется ряд показателей, в частности:
- Время сборки
- Размер набора
Все это по-прежнему глубоко экспериментально, и новый движок рендеринга Ivy включается по флажку, который вы сами должны проставить в опциях компилятора (в файле tsconfig.json
), если хотите попробовать.
"angularCompilerOptions": {
"enableIvy": true
}
Учтите, что этот механизм, пожалуй, не слишком надежен, поэтому пока не используйте его в продакшене. Возможно, он пока даже не заработает. Но в ближайшем будущем он будет принят в качестве варианта по умолчанию, поэтому стоит разок его попробовать, посмотреть, работает ли он в вашем приложении, и что вы от этого выигрываете.
Давайте подробнее обсудим, чем Ivy отличается от более старого движка рендеринга.
Код, генерируемый старым движком
Рассмотрим небольшой пример: пусть у нас будет компонент PonyComponent
, принимающий в качестве ввода модель PonyModel
(с параметрами name
и color
) и выводящий изображение пони (в зависимости от масти), а также кличку пони.
Выглядит так:
@Component({
selector: 'ns-pony',
template: `
`
})
export class PonyComponent {
@Input() ponyModel: PonyModel;
getPonyImageUrl() {
return `images/${this.ponyModel.color}.png`;
}
}
Движок рендеринга, появившийся в Angular 4, генерировал для каждого шаблона класс под названием ngfactory
. Класс обычно содержал (код упрощен):
export function View_PonyComponent_0() {
return viewDef(0, [
elementDef(0, 0, null, null, 4, "div"),
elementDef(1, 0, null, null, 1, "ns-image", View_ImageComponent_0),
directiveDef(2, 49152, null, 0, i2.ImageComponent, { src: [0, "src"] }),
elementDef(3, 0, null, null, 1, "div"),
elementDef(4, null, ["", ""])
], function (check, view) {
var component = view.component;
var currVal_0 = component.getPonyImageUrl();
check(view, 2, 0, currVal_0);
}, function (check, view) {
var component = view.component;
var currVal_1 = component.ponyModel.name;
check(view, 4, 0, currVal_1);
});
}
Читается сложно, но основные части этого кода описываются так:
- Структура создаваемой DOM, где содержатся определения элементов (
figure
,img
,figcaption
), их атрибуты и определения текстовых узлов. Каждый элемент структуры DOM в массиве определений представлений представлена собственным индексом. - Функции обнаружения изменений; содержащийся в них код проверяет, результируют ли используемые в шаблоне выражения в те же значения, что и ранее. Здесь проверяется результат метода
getPonyImageUrl
и, если он изменится, то обновится входное значение для компонента-изображения. То же касается и клички пони: если она изменится, то обновится текстовый узел, в котором содержится эта кличка.
Код, генерируемый Ivy
Если мы работаем с Angular 6, и флаг enableIvy
установлен в true
, то в таком же примере не будет генерироваться отдельная ngfactory
; информация будет встраиваться непосредственно в статическое поле самого компонента (упрощенный код):
export class PonyComponent {
static ngComponentDef = defineComponent({
type: PonyComponent,
selector: [['ns-pony']],
factory: () => new PonyComponent(),
template: (renderFlag, component) {
if (renderFlag & RenderFlags.Create) {
elementStart(0, 'figure');
elementStart(1, 'ns-image');
elementEnd();
elementStart(2, 'div');
text(3);
elementEnd();
elementEnd();
}
if (renderFlag & RenderFlags.Update) {
property(1, 'src', component.getPonyImageUrl());
text(3, interpolate('', component.ponyModel.name, ''));
}
},
inputs: { ponyModel: 'ponyModel' },
directives: () => [ImageComponent];
});
// ... остальной класс
}
Теперь все содержится в этом статическом поле. Атрибут template
содержит эквивалент привычной ngfactory
, со слегка иной структурой. Функция template
, как и раньше, будет запускаться при любом изменении, но теперь у нее два режима:
- Режим создания: компонент только создается, в нем содержатся статические узлы DOM, которые нужно создать
- Остальная часть функции выполняется при каждом изменении (при необходимости обновляет источник изображения и текстовый узел).
Что это меняет?
Теперь все декораторы встраиваются непосредственно в свои классы (одинаково для @Injectable
, @Pipe
, @Directive
), и для их генерации необходимо знать лишь об актуальном декораторе. Этот феномен в команде Angular называют «принцип локальности»: чтобы перекомпилировать компонент, не требуется заново анализировать приложение.
Сгенерированный код немного сокращается, но гораздо важнее, что удается исключить ряд зависимостей, ускорив таким образом перекомпиляцию, если одна из частей приложения изменится. Кроме того, с современными сборщиками, например, Webpack, все получается гораздо симпатичнее: надежно отсекается нефункциональный код, те части фреймворка, которыми вы не пользуетесь. Например, если у вас в приложении нет каналов, то фреймворк, нужный для их интерпретации, даже не включается в финальный набор.
Мы привыкли, что код Angular получается тяжелым. Бывает, это и не страшно, но Hello World весом 37 кб после минификации и сжатия — это слишком. Когда за генерацию кода отвечает Ivy, нефункциональный код отсекается гораздо эффективнее. Теперь Hello World после минификации ужимается до 7,3 кб, а после сжатия — всего до 2,7 кб, а это большаааая разница. Приложение TodoMVC после сжатия — всего 12,2 кб. Это данные от команды Angular, и другие у нас получиться не могли, так как чтобы Ivy работал как тут описано, его все равно нужно патчить вручную.
Если интересуют подробности — посмотрите это выступление с ng-conf.
Совместимость с имеющимися библиотеками
Возможно, вы заинтересуетесь:, а что будет с библиотеками, которые уже опубликованы в старом формате, если у вас в проекте используется Ivy? Не волнуйтесь: движок сделает Ivy-совместимую версию зависимостей вашего проекта, даже если они компилировались без Ivy. Нутрянку сейчас излагать не буду, но все детали должны быть прозрачны.
Новые возможности
Рассмотрим, какие новые возможности у нас появятся при работе с этим движком отображения.
Приватные свойства в шаблонах
Новый движок добавляет новую возможность или потенциальное изменение.
Такая ситуация напрямую связана с тем, что функция шаблона встраивается в статическое поле компонента: теперь мы можем использовать приватные свойства наших компонентов в шаблонах. Ранее это было невозможно, из-за чего мы были вынуждены держать публичными все поля и методы компонента, используемые в шаблоне, и они попадали в отдельный класс (ngfactory
). При обращении к частному свойству из другого класса компиляция TypeScript не удалась бы. Теперь это в прошлом: поскольку функция шаблона находится в статическом поле, ей открывается доступ к частным свойствам компонента.
Я видел комментарий от членов команды Angular по поводу того, что не рекомендуется использовать приватные свойства в шаблонах, хотя это теперь и возможно — поскольку в будущем может быть снова запрещено… поэтому, пожалуй, разумнее и дальше использовать в шаблонах только публичные поля! В любом случае, писать модульные тесты теперь проще, поскольку тест может проверять состояние компонента, даже не генерируя и не проверяя для этого DOM.
i18n во время исполнения
Обратите внимание: новый движок рендеринга наконец-то открывает перед нами долгожданную возможность и дает «i18n во время исполнения». На момент написания статьи она еще была не совсем готова, но мы видели сразу несколько коммитов, а это добрый знак!
Самое классное, что вам не придется изрядно менять ваше приложение, если вы уже работаете с i18n. Но теперь вам уже не потребуется собирать приложение заново для каждой локали, которую вы планируете поддерживать — достаточно будет просто загрузить JSON с переводами для каждой локали, а Angular позаботится об остальном!
Библиотеки с AoT-кодом
На данный момент библиотека, выпускаемая в NPM, должна публиковать файл metadata.json и не может публиковать AoT-код своих компонентов. Это грустно, поскольку издержки, связанные с такой сборкой, перекладываются на наше приложение. С Ivy необходимость в файле метаданных отпадает, и авторы библиотек теперь смогут публиковать свой AoT-код непосредсnвенно в NPM!
Улучшенные стектрейсы
Теперь сгенерированный код должен давать улучшенные стектрейсы, если у вас выйдет проблема с вашими шаблонами — результировать в аккуратную ошибку с указанием той строки шаблона, в которой она произошла. Вы даже сможете ставить в шаблонах точки останова и прослеживать, что на самом деле происходит в Angular.
NgModule
исчезнет?
Это еще далекая перспектива, но, возможно, в будущем удастся обойтись без NgModules. Первые ласточки таких перемен — это tree-shakeable провайдеры, и логично предположить, что в Ivy есть все необходимые базовые блоки для тех, кто готов постепенно отказываться от NgModules (или, по крайней мере, сделать их менее доставучими). Правда, все это еще в перспективе, будем терпеливы.
В этом релизе будет не так много новых фич, но Ivy определенно интересен на будущее. Поэкспериментируйте с ним — интересно, как он вам понравится!