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

Статья разбирает весь долгосрочный переезд: подготовку кода AngularJS к будущему до единой строчки нового фреймворка, гибридный режим ngUpgrade, где обе версии живут в одном приложении, план маршрут за маршрутом, разделение общих слоёв, тесты и цену гибрида, и финальный демонтаж старого каркаса. С кодом переходных моментов и честными сроками, потому что миграция старого приложения измеряется кварталами, а не спринтами.

Почему переезд стал неизбежным конец поддержки и цена застоя

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

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

Третье измерение это экосистема: библиотеки компонентов, инструментов и обучения живут вокруг актуальных версий, и старое приложение отрезано от них прогрессом. Поддержка старых версий в сторонних пакетах прекращается одна за другой, и точка, где приложение остаётся на необслуживаемом фреймворке с необслуживаемыми зависимостями, называется риск-квадратом.

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

Подготовка без миграции как AngularJS код приближают к современным формам

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

Первый шаг выравнивания это компоненты: замена директив с шаблонами-функциями на компонентный API старого фреймворка, с изолированными областями и явными входами-выходами. Компонентный API не современный Angular, но он приучает код к той же форме мысли: экран это компонент со входами, событиями и жизненным циклом, и после этой привычки синтаксис нового фреймворка читается как знакомый язык.

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

Четвёртый шаг это сервисы как классы с явными зависимостями: аннотационное внедрение зависимостей старого фреймворка превращается в конструкторные, и сервис перестаёт быть мешком с глобальным состоянием. Пятый шаг это модульность по экранам: разложение монолита на тематические модули, потому что мигрировать экранами и модулями можно, а мигрировать кашу нельзя.

Финал выравнивания это каталог экранов с оценкой сложности каждого: зависимости, объём шаблонов, внешние интеграции, и список становится планом переезда. Без каталога план строится по памяти, а память о собственном приложении обманчивее любой документации.

Гибрид ngUpgrade как две версии фреймворка живут в одном приложении

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

@NgModule({
  imports: [BrowserModule, UpgradeModule]
})
export class AppModule {
  constructor(private upgrade: UpgradeModule) {}

  ngDoBootstrap() {
    this.upgrade.bootstrap(document.body, ["legacyApp"], {
      strictDi: true
    });
  }
}

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

angular.module("legacyApp").directive(
  "userCard",
  downgradeComponent({ component: UserCardComponent })
);

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

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

Маршрут за маршрутом план переезда экранов без большого взрыва

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

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

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

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

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

Сервисы и зависимости инжекция в двух мирах и общие слои

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

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

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

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

Стили и разметка переезжают со своими правилами: старая страница собрана на глобальных таблицах стилей, новая на компонентных областях, и переходный период живёт с обеими системами по слоям. Общие таблицы стилей остаются для старых островов, новые экраны несут стили с собой, и конфликт области видимости решается гигиеной классов, а не надеждой.

Тесты производительность и дисциплина в период гибрида

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

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

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

Порядок долгосрочной миграции свёрнут в короткую лестницу:

  1. провести экономическое обоснование и зафиксировать решение документом;
  2. выровнять код внутри старого фреймворка: компоненты, связки, типы, сервисы;
  3. составить каталог экранов с оценками и очередью по бизнес-приоритету;
  4. поднять гибридный режим с минимальной загрузкой старого приложения;
  5. переносить экраны по одному со сквозными проверками и метриками шага;
  6. выносить общий слой в нейтральные модули и сокращать мост инжекции;
  7. завершить демонтаж старого каркаса и провести церемонию закрытия.

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