Перейти к содержимому

Понемногу — но каждый день.

Angular 6 → 22: путь обновления и на чём спотыкаешься

6 мин чтения

На этой странице
Коротко
Обновлять Angular через много версий можно только одним способом: по одной мажорной версии за раз, с зелёной сборкой и тестами после каждой. Большие ломки случаются редко, но они предсказуемы: смена движка рендеринга, смена сборщика, standalone, сигналы, zone.js. Ниже — карта пути от 6 до 22, что ломалось у нас на последних шагах (кольца импортов, оверлеи поверх всего, zone.js) и почему в монорепозитории это делается один раз, а не десять.

Правило номер один: по одной версии

Angular выпускает мажорную версию примерно раз в полгода. Команда обновления:

npx ng update @angular/core@17 @angular/cli@17

ng update не просто меняет номер в package.json. Он запускает миграции — скрипты, которые переписывают твой код под новую версию. Миграции написаны для перехода с N на N+1. Если прыгнуть с 12 на 17, миграции 13, 14, 15 и 16 не выполнятся, и разбираться придётся руками.

Поэтому цикл такой:

  1. Обновить на одну версию.
  2. Собрать, прогнать тесты, открыть приложение.
  3. Закоммитить отдельным коммитом.
  4. Следующая версия.

Отдельный коммит на версию — это возможность откатиться ровно на одну ступень и через git bisect найти, на какой из них что-то сломалось.

Для монорепозитория на Nx вместо ng update — nx migrate: он обновляет и Angular, и сам Nx, и плагины согласованно.

npx nx migrate 21          # пишет новые версии в package.json и migrations.json
pnpm install
npx nx migrate --run-migrations

И сверяйся с официальным Angular Update Guide: выбираешь «с какой» и «на какую», и он показывает, что сделать руками.

Карта пути: где большие ступени

Большинство обновлений — это «обновил, поправил пару предупреждений, поехал». Но есть несколько ступеней, где меняется что-то фундаментальное.

ВерсияЧто меняетсяЧем опасно
6ng update, RxJS 6старые импорты RxJS (rxjs/add/operator/...) нужно переписать на pipe()
8ленивая загрузка через import()строки вида './module#Module' в маршрутах устарели
9движок Ivy по умолчаниюстарые библиотеки, собранные под View Engine; более строгие проверки шаблонов
12–13View Engine удалён, IE11 больше не поддерживаетсябиблиотеки без поддержки Ivy перестают работать совсем
14–15standalone-компоненты, типизированные формыможно жить по-старому, но новый код пишется иначе
16–17сигналы, новый control flow (@if, @for), сборщик на esbuildогромный, но механический рефакторинг шаблонов
19standalone: true по умолчаниюкомпоненты с NgModule требуют явного standalone: false
20–22смена билдера, zone.js уходит из умолчаний, CDK меняет оверлеиподробно ниже

Самая болезненная ступень на практике — 9 и 13: проблема обычно не в твоём коде, а в зависимостях, которые никто не обновил под Ivy. Решение одно: заменить библиотеку или убрать её.

Как это было у нас

До монорепозитория у нас было больше десяти копий приложения, и каждая обновлялась отдельно — то есть, по сути, не обновлялась. Переход в монорепо (подробно — nx-monorepo-from-copies) начался уже на Angular 16. Дальше:

  • 16 → 17 — через пару месяцев после старта, отдельным коммитом.
  • 17 → 19 — через полтора года. Это ошибка: чем дольше стоишь, тем больше накапливается.
  • 19 → 20 → 21 → 22 — за один день, тремя отдельными коммитами, вместе с Nx 20 → 23 и TypeScript 5.7 → 6.0.

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

Ступень 20: новый билдер

Старый билдер @angular-devkit/build-angular:browser (на webpack) заменён на @angular/build:application (на esbuild). В project.json каждого приложения:

// было
"executor": "@angular-devkit/build-angular:browser",
"options": { "main": "src/main.ts", "buildOptimizer": true, "vendorChunk": true }

// стало
"executor": "@angular/build:application",
"options": { "browser": "src/main.ts" }

Часть опций просто исчезла (buildOptimizer, vendorChunk, namedChunks) — новому сборщику они не нужны. Изменилась и структура результата: файлы теперь лежат в dist/<app>/browser/. Если деплой копирует dist/<app>/ — на сервер уедет лишняя папка. Мы это поймали уже в CI.

Новый сборщик заметно быстрее. Но он строже к порядку модулей — и это приводит к следующему пункту.

Грабли: кольца импортов

После обновления часть страниц начала падать в рантайме с ошибкой NG0919 («Cannot read @Component metadata»). Сборка проходила.

Причина — циклический импорт. Пример:

// list.page.ts
import { EditDialog } from './edit.dialog';   // страница открывает диалог

// edit.dialog.ts
import { ListPage } from './list.page';        // диалог берёт тип у страницы

Модули в кольце загружаются в непредсказуемом порядке. Когда Angular читает imports: [EditDialog], класс EditDialog может быть ещё undefined. Старый сборщик случайно грузил их в «удачном» порядке. Новый — нет.

Как разрывать кольцо, от лучшего к худшему:

  1. Вынести общее в третий файл. Константы, токены, интерфейсы — в *.constants.ts или *.interface.ts, от которого зависят оба.
  2. Импорт только типа. Если нужен лишь тип, import type { ListPage } from './list.page'. TypeScript удаляет такой импорт при компиляции, и в рантайме кольца нет.
  3. Ослабить тип. Например, MatDialogRef<any> вместо MatDialogRef<EditDialog>. Работает, но теряет проверку типов.

Честно: в нескольких местах мы сделали третий вариант, чтобы быстрее выпустить обновление. Это долг, и он записан. Правильно — второй.

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

Грабли: тосты под диалогами

После обновления Angular CDK до 22 всплывающие уведомления (тосты) стали появляться под модальными окнами.

Причина: CDK 22 по умолчанию рендерит оверлеи (диалоги, выпадающие списки, тултипы) через Popover API — в так называемом top layer браузера. Top layer рисуется поверх всего документа, и z-index на него не влияет. Тост с z-index: 1001 в обычном документе оказывается под диалогом в top layer, как бы ты ни поднимал число.

Возвращаем старое поведение одним провайдером:

import { OVERLAY_DEFAULT_CONFIG } from '@angular/cdk/overlay';

providers: [
  { provide: OVERLAY_DEFAULT_CONFIG, useValue: { usePopover: false } },
]

Урок: читай changelog не только Angular, но и CDK и Material. Они обновляются вместе, а меняют поведение отдельно.

Ступень 21: zone.js и HttpClient

zone.js — библиотека, которая перехватывает все асинхронные события (клики, таймеры, HTTP) и говорит Angular: «что-то произошло, проверь экран». Angular уходит от неё к сигналам, и новые приложения создаются без zone.js.

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

bootstrapApplication(AppComponent, {
  ...appConfig,
  providers: [provideZoneChangeDetection(), ...appConfig.providers],
});

Это правильное поведение миграции: обновление не должно менять поведение незаметно. Переход на zoneless — отдельная задача, с отдельным тестированием.

Так же миграция добавила withXhr() в provideHttpClient(...), чтобы HTTP-клиент остался на XMLHttpRequest. Если у тебя есть загрузка файлов с прогрессом — проверь её после обновления.

После обновления: доводка

Обновление версии — не конец. Новый Angular предлагает новые способы писать код, и стоит постепенно переходить на них, иначе через пару лет снова будет «старый код»:

  • inject() вместо внедрения через конструктор;
  • сигналы (signal, computed) для состояния компонента;
  • @if/@for вместо *ngIf/*ngFor;
  • takeUntilDestroyed() для подписок.

Мы делали переход на inject() отдельной задачей: один коммит на одну библиотеку. Так ревью остаётся маленьким, а если что-то сломалось — понятно где. Для большей части есть официальные автоматические миграции (ng generate @angular/core:inject и аналоги) — запускай их, а не переписывай руками.

Чек-лист

  1. Одна мажорная версия за раз, отдельный коммит.
  2. ng update / nx migrate, а не ручная правка package.json.
  3. После каждой: сборка, тесты, приложение глазами.
  4. Перед стартом — список зависимостей: какие не поддерживают целевую версию.
  5. Changelog Angular и CDK/Material.
  6. Новые ошибки рантайма (NG0919 и т. п.) — сначала искать кольца импортов.
  7. Не стоять на одной версии полтора года: два обновления в год дешевле, чем одно раз в три года.

Связанные посты: nx-monorepo-from-copies, tailwind-v3-to-v4-monorepo, ci-nx-affected.