javascript

Кризис идентичности, или как Vue-разработчик мигрировал enterprise-проект на Angular 18

  • суббота, 26 сентября 2026 г. в 00:00:10
https://habr.com/ru/companies/ppr/articles/1085816/

Всем привет! Меня зовут Павел Барсуков, я frontend-разработчик в компании «Программный Продукт».

Основной мой стек – Vue, Nuxt и все рядом лежащее. Angular до недавнего времени я видел в основном в курсах на Youtube, пару раз на фрилансе, где особо с ним не взаимодействовал, и в мемах про исключительность Angular разработчиков и очень частые обновления версий. 

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

Справедливости ради, я не начинал эту историю с нуля. До меня другая команда уже проделала огромную работу: проект был переведен примерно до Angular 18. Но именно «примерно». Версия самого Angular была 18, часть библиотек уже обновили, часть – нет, кодовая база при этом осталась на уровне Angular 16. Проект собирался, но назвать миграцию законченной было нельзя. И тут пришел я.

Очень важная информация

Изначально я хотел назвать статью «Я был обычным Vue-разработчиком, но после пробуждения оказался в Angular-проекте 2019 года и теперь должен победить древний Redux, используя верного ИИ-помощника», но мне сказали, что я должен пить таблетки.

Коротко о подходе

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

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

Объем работы

Прежде чем что-либо менять, я решил оценить объем работ. Вместо того чтобы самому обходить кучу компонентов, я поручил это Claude. Он проанализировал проект, версии зависимостей и состояние кодовой базы и составил список того, что можно автоматизировать, а что придется делать вручную. Получилось следующее: версии уже подняты (Angular 18.2.13, RxJS 7.8, TS 5.5, ng-zorro 18, Node 18), сборка работает, но код написан в стиле Angular 8. Цель – привести код к стандартам Angular 18 (standalone, новый control flow, inject(), signals).

Результат первичного анализа проекта
Результат первичного анализа проекта

Вывод: ~70% работы автоматизируется официальными схематиками Angular. Ручной труд смещается в сторону ревью и починки после миграции, а не переписывания руками.

На протяжении всей миграции Claude был в курсе состояния проекта. Он знал текущие версии библиотек, уже выполненные изменения и оставшиеся проблемы. Поэтому вместо отдельных вопросов вроде «как обновить ng-zorro» диалог выглядел скорее как обсуждение следующего этапа миграции. После каждого шага проект собирался заново, появлялись новые баги, и цикл повторялся.

Обновление версий

Первое, что приходит на ум, когда тебе ставят задачу «обновить версию Angular» – собственно обновление зависимостей фреймворка. Конечно, это самое простое: ng update @angular/core@18 @angular/cli@18 – и дело в шляпе. И именно поэтому это уже сделали до меня. Сложности возникли с вспомогательной библиотекой. В проекте использовался @angular-redux/store v10, который отлично работал в восьмом ангуларе, но был блокером в 18, поэтому было принято решение установить более новый @angular-redux2/store – community-форк, который поддерживает актуальные версии Angular. Но даже форк не был полностью совместим «из коробки» – в ReducerService (класс, оборачивающий state в Proxy для отслеживания изменений) была ошибка null-деструктуризации: 

if (typeof target[prop] === 'object' && !target[prop]._isProxy) {

Проблема в том, что typeof null === 'object' в JS. То есть если в state оказывалось поле со значением null, код пытался прочитать null._isProxy и падал с TypeError: Cannot read properties of null. А null в состоянии Redux – обычное дело (например, необязательные значения до загрузки данных). Патч добавляет проверку на ненулевое значение:

if (typeof target[prop] === 'object' && target[prop] !== null && !target[prop]._isProxy) {

Правка внесена во все три сборочных варианта пакета (esm2020, fesm2015, fesm2020), так как это dist-код, а не исходники – патчить пришлось саму сборку. Как это работает: патч лежит в patches/@angular-redux2+store+5.0.2.patch и накатывается автоматически через patch-package в хуке postinstall при каждом npm install. Важно: при апгрейде или переустановке @angular-redux2/store нужно заново проверять, применяется ли патч — новая версия может как исправить баг сама, так и изменить эту часть кода так, что патч не наложится. Для меня вообще было в новинку патчить зависимости с помощью patch-package (век живи – век учись).

Переменные окружения

А теперь расскажу про проблему, которая вызвала у меня непонимание. В Angular-проектах переменные окружения задаются в environment.ts файлах. Под каждое окружение создается отдельный файл (*.prod.ts, *.staging.ts и т.д.), что показалось мне дико неудобным, и я решил поехать в Тулу со своими пряниками. То есть, сделать так, чтобы был единый файл environment.ts, а значения заполнялись из process на этапе сборки. И да простят меня за это ангуларщики, я уже очень сильно привык к тому, что у меня есть либо process.env, либо import.meta.env, чтобы в gitlab/github можно было удобно пробрасывать переменные в зависимости от площадки. Наверняка сейчас кто-то пишет комментарий «так делать нельзя». Возможно. Но мне было нужно решить конкретную задачу.

Стандартный билдер Angular CLI не даёт доступа к нативному webpack-конфигу, поэтому в angular.json он был заменён на @angular-builders/custom-webpack:browser с указанием customWebpackConfig.path: ./webpack.config.js. Это открыло возможность подключать собственные webpack-плагины поверх стандартной Angular-сборки без форка билдера. В webpack.config.js через dotenv подтягивается .env-файл, а webpack.DefinePlugin инлайнит нужные значения в виде process.env.* прямо в JS-бандл.

В результате сборка стала гибче, все URL-зависимости вынесены за пределы кода в .env/CI-переменные и попадают в бандл декларативно через DefinePlugin.

Схематики

Когда ангулар обновляет себя сам
Когда ангулар обновляет себя сам

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

  • Первый схематик переводит компоненты в standalone;

  • Второй – меняет *ngIf на @if;

  • Следующий – переводит DI на inject().

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

В итоге схема выглядела примерно так:

  • запускаем схематик;

  • радуемся;

  • собираем проект;

  • грустим;

  • чиним баги;

  • повторяем до победного.

Удобно, что Angular предоставляет такую возможность для обновления, хотя стоит признать, что запуск схематика не гарантирует полное обновление компонентов. Довольно большой пласт у меня остался с ngIf, ngFor и подобными, так что к этому нужно будет возвращаться впоследствии. Благо, для подобных вещей есть обратная совместимость, так что этот момент не особо критичен. А учитывая, что в нашу эру высоких технологий рутинная замена одного типа записи на другой не является проблемой.

После всех страданий с схематиками, отловом ошибок обновленного TS во время билда, я попробовал запустить ng serve и увидел сломанную верстку. Но проект запустился! И дальше меня ждал следующий этап: …

Обновление UI-библиотеки

Первое, с чем столкнулись сразу после апдейта ng-zorro в package.json, – компилятор перестал находить привычные импорты. Часть компонентов, которые раньше жили в основном пакете, авторы библиотеки вынесли в отдельные легаси подпакеты, оставив в основном новую, переработанную реализацию с несовместимым публичным API.

Легаси компоненты

Самый заметный пример – NzInputNumber. Старая реализация переехала в отдельный модуль с суффиксом -legacy, и весь код, где использовался этот компонент (и как отдельный компонент, и как часть общего shared-модуля с ng-zorro-зависимостями), пришлось переключить на новый путь импорта и новое имя класса. По сути – механическая, но повсеместная правка: нашли все точки использования, заменили импорт и имя в списке imports/declarations. Никакой логики это не меняет, но забыть хотя бы одно место – значит получить рантайм-ошибку об отсутствующем компоненте, которую TypeScript до сборки не поймает, если тип импортирован откуда-то ещё по старой памяти. Чтобы ничего не упустить, я снова использовал Claude Code, и вместо нескольких часов я потратил на эту задачу несколько минут.

Смена некоторых API

Следующая категория правок касалась работы с модальными окнами – и тут изменения были уже архитектурными, а не косметическими. Раньше в проекте был распространён паттерн: получить ссылку на модалку, вызвать у неё метод получения «внутреннего инстанса» и напрямую поменять его свойства (например, включить/выключить кнопку подтверждения). Этот метод в новой версии библиотеки перестал существовать в публичном API – что, в общем, ожидаемо, поскольку он и раньше был скорее костылём, чем задокументированным способом работы, и в коде такие места нередко сопровождались комментарием, отключающим проверку типов. Правильный путь, который библиотека предлагает теперь – вызывать метод обновления конфигурации модалки, передавая туда нужные свойства декларативно, без прямого доступа к внутренностям компонента.

Заодно поменялся и способ получения данных, с которыми модалка была открыта: вместо ранее принятого способа передачи данных через параметры открытия теперь стало правильнее забирать их через инъекцию специального токена, привязанного к контексту диалога. Это уже не просто переименование метода, а смена самого подхода – данные стали доступны не «откуда-то сбоку», а как инъекция, что поддерживает философию Angular.

Естественным следствием такой замены стало то, что данные, которые раньше точно были, теперь в типах стали необязательными – токен инъекции может ничего не вернуть, если модалка была открыта без данных. Это потребовало точечно пройтись по местам использования и добавить проверки на существование там, где раньше разработчик был уверен, что значение точно есть. После этого, когда все ошибки тайпскрипта были вычищены, ng serve успешно открыл моему взору рабочий UI.

Правки классов в компонентах библиотеки

Но все еще поломанный. Стили тоже нужно было немного подправить. И тут мне пришлось порешать ребусы. В старой версии ng-zorro, как я понял, добавление пользовательского класса не заменяло оригинальный от библиотеки. В новой версии он стал его заменять. Понял я это, когда решил посмотреть, а как выглядят компоненты библиотеки на официальном сайте документации, и увидел, что у них есть классы, которые у меня по какой-то причине не проставляются. Я дописал в одном месте нужный класс, и, о чудо, компонент начал выглядеть нормально. Я понял, что в большом проекте вручную дописывать столько классов – весьма затратный по времени процесс, поэтому я снова обратился к Claude. Я уже понимал, что именно нужно менять, поэтому оставалось поручить ему механическую работу и потом проверить результат. За минут 10 он мне все классы проставил, и в diff после такой правки было очень много измененных компонентов, в основном правки были в использовании nz-select и nz-table. Неприятный момент, потребовавший от меня некоторое время считать себя плохим разработчиком. Но тем не менее, я наконец-то увидел чистый UI, красивый и собранный, каким он был до миграции

Итоги

За время миграции было сделано следующее:

  • обновлён Angular и сопутствующие библиотеки;

  • часть компонентов схематиком переведены на standalone и блочный синтаксис @if, @for и т.д.;

  • восстановить и написать новые тесты (скучный пункт, расписывать не стал).

Когда я получил задачу «Нужно обновить проект со старого Ангулара на новый», я сначала испугался, потому что я вьюшник, какой к лешему ангулар. Но раз долг зовёт – надо делать. Засучив рукава, я проделал всю необходимую работу и получил в итоге рабочий интерфейс, который дальше можно дорабатывать напильником точечно. Даже если бы у меня не было ИИ-помощника, я бы все равно сделал эту миграцию. Просто она заняла бы значительно больше времени. В какой-то момент работы я решил попробовать обновиться до 19 версии, благо среда, где приложение будет собираться, позволяла это сделать, хоть и впритык.

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