Переход с AngularJS на Angular: проблемы и решения гибридного режима (2/3)
Переход в гибридном режиме — естественная процедура, хорошо подготовленная и описанная командой Angular. Тем не менее, на практике возникают сложности и затыки, которые приходится решать на лету. В сегодняшнем продолжении нашей статьи про миграцию на Angular мы расскажем про проблемы, с которыми столкнулась команда Skyeng, и поделимся своими решениями.
Динамическая компиляция из строкиВ angularjs все очень просто:
А в Angular не совсем.
Первое решение — взять вариант из ангуляра, через JiT компилятор. Оно подразумевает, что в продакшен сборку, несмотря на AoT компиляцию статичных компонентов, всё равно тащится тяжёленький компилятор для сборки динамических шаблонов. Выглядит как-то так:
И вроде бы всё относительно неплохо (толстый компилятор в бандле всё равно нивелируется горой других либ и кодом самого проекта, если это что-то большее, чем todo list), но тут конкретно мы въехали вот в такую проблему:
Шесть секунд на компиляцию одного из наших слайдов с упраженениями, пусть и довольно большого. При том, что три секунды идёт непонятный простой. Судя по ответу в issue, ситуация ближайшие месяцы не изменится, и нам пришлось искать другое решение.
Также оказалось, что мы не можем в этом случае задействовать уже скомпилированные при AoT сборке фабрики компонентов, используемых в слайдах, т.к. нет возможности заполнить кэш JiT компилятора. Такие компоненты по сути компилировались два раза — на бэкэнде при AoT сборке и в рантайме при компиляции первого слайда.
Вторым решением на скорую руку была сделана компиляция шаблонов через $compile из angularjs (у нас же всё ещё гибрид и ангуляржс):
Компонент ангуляра использовал апгрейженную версию DynamicTemplateComponent из ангуляржса, который использовал $compile сервис для сборки шаблона, в котором все компоненты были даунгрейжены из ангуляра. Такая короткая прослойка angular -> angularjs ($compile) -> angular.
Этот вариант имеет немного проблем, например, невозможность инжекта компонентов через компонент-сборщик из ангуляржса, но главное — он не будет работать после окончания апгрейда и выпиливания ангуляржса.
Дополнительное гугление и задалбывание народа в gitter'е ангуляра привело к третьему решению: вариации на тему того, что используется непосредственно на офф сайте ангуляра для подобного кейса, а именно вставке шаблона напрямую в DOM и ручной инициализации всех известных компонентов поверх найденных тегов. Код по ссылке.
Вставляем пришедший шаблон в DOM как есть, для каждого известного компонента (получаем по токену CONTENT_COMPONENTS в сервисе) ищем соответствующие DOM-ноды и инициализируем.
- немного коряво проставляем инжекторы для корректной работы инжектов родителей;
- небольшой хак для поддержки content projection с select'ами (вытащили пару методов из @angular/upgrade модуля);
- инпуты только статичные и только строковые;
- полное доверие пришедшему хтмлу (вставляется без обработки, т.к. может содержать инлайн стили и всякое другое непотребство из нашей админки);
- некорректная последовательность инит хуков для родителей-детей (сначала OnInit/AfterViewInit родителей, только потом OnInit/AfterViewInit детей).
Но в целом мы имеем довольно шустрый способ инициализировать динамический шаблон, в основе своей решающий конкретно нашу задачу средствами ангуляра и без лагов, как с JiT компилятором.
Казалось бы, на этом можно остановиться, но для нас проблема до конца так и не решилась из-за того, как ангуляр работает с content projection. Нам необходимо содержимое некоторых компонентов (по типу спойлеров) инициализировать только при определённых условиях, что невозможно при использовании обычного ng-content , а ng-template мы не можем вставить из-за особенностей способа сборки контента. В дальнейшем будем искать более гибкое решение, возможно, заменим html-контент на JSON структуру, по которой обычными ангуляр-компонентами будем рендерить слайд с учётом динамического показа/скрытия части контента (потребует использования самописных компонентов вместо ng-content ).
Кому-то может подойти четвёртый вариант, который станет официально доступен в виде беты с релизом angular 6 — @angular/elements . Это custom elements, реализованные через ангуляр. Регистрируем по некоторому тегу, любым способом вставляем этот тег в DOM, и на нём автоматически инициализируется полноценный ангуляр компонент со всем привычным функционалом. Из ограничений — взаимодействие с основным приложением только через события на таком элементе.
Информация по ним пока доступна только в виде нескольких выступлений с ng-конференций, статей по этим выступлениям и техническим демкам:
Сайт ангуляра планирует сразу же, с первой версией @angular/elements , перейти на них вместо текущего способа сборки:
Change Detection
В гибриде есть несколько неприятных проблем с работой CD между ангуляром и ангуляржсом, а именно:
AngularJS в зоне AngularСразу после инициализации гибрида мы получим просадку по производительности из-за того, что angularjs код будет запускаться в зоне angular'а, и любые setTimeout / setInterval и другие асинхронные действия из кода angularjs и из используемых thirdparty библиотек будут дёргать тик CD angular'а, который дёрнет $digest angularjs . Т.е. если раньше мы могли не беспокоиться о лишних digest'ах от активности сторонних либ, т.к. angularjs требует явного пинания CD, то теперь он будет срабатывать на каждый чих.
Чинится пробраcыванием NgZone сервиса в angularjs (через даунгрейд) и оборачиавния инициализации сторонних либ или родных таймаутов в ngZone.runOutsideAngular . В будущем обещают возможность инициализировать гибрид так, чтобы CD ангуляра и ангуляржса не дёргали друг друга в принципе (ангуляржс будет работать вне зоны ангуляра), и для взаимодействия между разными кусками надо будет явно дёргать CD соответствующего фреймворка.
downgradeComponent и ChangeDetectionStrategy.OnPushДаунгрейженные компоненты некорректно работают с OnPush — при изменении инпутов не дёргается CD на этом компоненте. Код.
Если закомментировать changeDetection: ChangeDetectionStrategy.OnPush, в angular.component , то счётчик будет обновляться корректно
Из решений только убрать OnPush с компонента, пока он используется в шаблонах ангуляржс компонентов.
UI Router
У нас изначально был ui-router, который работает с новым ангуляром и имеет кучку хаков для работы в гибридном режиме. С ним было немало возни по бутстрапу приложения и проблемам с protractor.
В итоге пришли к таким хакам инициализации:
Встречаются неочевидные даже по официальной документации роутера места, например, использование angularjs-like инжектов для OnEnter / OnExit хуков в angular части роутинга:
Информацию об этом пришлось добывать через gitter канал ui-router'а, часть её уже внесли в документацию.
Protractor
Через протрактор у нас работает куча e2e тестов. Из проблем в гибридном режиме столкнулись только с тем, что совсем отвалился метод waitForAngular . QA команда впиливала какие-то свои хаки, а также попросила нас реализовать meta-тег в хэдере со счётчиком активных апи запросов, чтобы на основе этого понимать, когда основная активность на странице прекратилась.
Счётчик делали через появившиеся в ng4 HttpClient Interсeptor'ы:
В окончании этой истории мы делимся новыми конвенциями, которые помогают команде привыкнуть к работе в Angular.