У каждого фреймворка есть дата, после которой ему перестают присылать исправления уязвимостей. Python 2 дожил до 1 января 2020 года, AngularJS до 31 декабря 2021 года, Vue 2 до 31 декабря 2023 года. После этой даты каждый найденный изъян в старой версии остаётся в продукте навсегда, и вопрос переезда превращается из желания в необходимость. Дальше команды обычно выбирают между двумя сценариями. Первый: выделить отдельный проект, заморозить разработку функций и переписать всё на новой версии за один заход. Второй: переезжать по шагам, продолжая выпускать релизы каждые пару недель. Второй сценарий выглядит сложнее в управлении, но именно он приводит команды к цели. Разберём, как устроена пошаговая миграция, какие инструменты её поддерживают и почему попытка закрыть всё одним проектом почти всегда заканчивается сорванными сроками.
Почему заморозка разработки на время переезда убивает проект изнутри
Заморозка кажется честным решением: половину команды снимают с продуктовых задач, дают им полгода на новый фреймворк, продукт ждёт. Арифметика против неё беспощадна. Продукт с двухнедельным ритмом за полгода теряет двенадцать релизов; за год таких релизов двадцать четыре. Конкуренты за то же время выпускают новые возможности, приоритеты заказчиков успевают измениться трижды, и к моменту разморозки бэклог частично обесценивается. Застаивается и сама команда: сильные инженеры не любят годами смотреть на заброшенный функционал и уходят туда, где есть движение.
Есть и экономический довод. Во время заморозки компания дважды платит за одно и то же: старый код продолжают чинить и обслуживать, потому что он в продакшене, и параллельно строится его новая копия. Двойные расходы без роста функциональности выматывают бюджет быстрее, чем кажется на старте. Добавим сюда типичный срыв сроков в полтора-два раза, и станет ясно, почему свежая оценка "шесть месяцев" превращается в год с четвертью.
Пошаговая миграция противостоит этому всему другим порядком работы:
- зафиксировать текущую версию и собрать инвентаризацию использования устаревших возможностей фреймворка;
- привести сборку и тесты в зелёное состояние до начала каких-либо переездов;
- развернуть совместимый режим и превратить предупреждения фреймворка в живой список задач;
- переносить модули по одному, выпуская очередной релиз каждые одну-две недели;
- отключить временные слои совместимости и закрыть миграцию с измеримым финалом.
Каждый пункт сам по себе скучен. Вместе они образуют стратегию, при которой продукт ни на день не выпадает из обычного ритма поставки.
Инвентаризация использования фреймворка и карта совместимости версий
Миграция начинается не с установки новой версии, а со списка того, что именно команда использует в старой. Глазами инженера это выглядит как аудит из трёх слоёв. Первый слой: прочитать список разрушительных изменений между версиями и отметить, какие из них касаются проекта. Второй: посчитать использование каждой затронутой возможности по всему репозиторию. Третий: разложить результат по модулям и оценить риски каждого.
Для подсчёта подходит и простой поиск по коду, и статический анализ. Если фреймворк умеет запускать автоматические преобразования, их прогоняют в режиме просмотра, без записи изменений: сухой прогон сразу показывает, сколько файлов и какие конструкции затронет переход. На выходе составляют таблицу: модуль, количество мест с устаревшим вызовом, наличие тестов, ответственный. Такая таблица превращает абстрактное "надо переписать всё" в конкретные сорок два вызова в шести модулях, и только после этого можно называть сроки с точностью лучше, чем "наверное, полгода".
Отдельно фиксируют внешние зависимости: плагины, библиотеки компонентов, сборочные плагины. Именно они чаще всего ломают миграцию, потому что сами ждут своих мажорных версий, и их готовность обязана входить в карту перехода наравне с собственным кодом.
Пример строки из такой карты: модуль оплаты, тридцать семь вызовов устаревшего фильтра событий, автотесты закрывают восемьдесят процентов сценариев, ответственный один, оценка три дня. Когда все модули описаны одинаковыми строками, сумма оценок даёт верхнюю границу объёма работ, а сортировка по риску определяет порядок. Инвентаризация занимает одну-две недели и остаётся лучшей инвестицией проекта: без неё сроки называют на глазок, с ней считают по списку.
Совместимый режим как мост между мажорными версиями фреймворка
Крупные экосистемы давно делают специальные режимы, в которых новая версия умеет вести себя как старая. Vue называет его migration build: пакет @vue/compat это сборка третьей версии с настраиваемым поведением второй. По умолчанию она работает в режиме Vue 2, а использование того, что в третьей версии изменилось или удалено, вызывает предупреждения во время выполнения. Совместимость настраивается вплоть до отдельного компонента, поэтому миграция идёт файл за файлом, а не во всём приложении сразу.
У подхода есть известные границы, и их честно описывает документация Vue. Если проект опирается на внутренние, недокументированные механизмы второй версии, режим совместимости не поможет: библиотеки компонентов, завязанные на устройство старой версии, ждут своих обновлений отдельно. В переходах между AngularJS и Angular такую же роль играл гибридный режим, где обе версии работали в одном приложении, а модули переезжали по одному. В мире Python между вторым и третьим релизом жил слой совместимости из библиотеки six, позволявший одному коду исполняться в обоих мирах.
Смысл моста не в том, чтобы остаться на нём. Смысл в метрике: число предупреждений совместимости это готовый индикатор прогресса миграции. Начинали с трёх тысяч предупреждений, через квартал осталось четыреста, ещё через один ноль. Руководителю проекта не нужны для этого отдельные отчёты, прогресс видно прямо в логах сборки.
Автоматические преобразования кода вместо ручной правки сотен файлов
Ручная правка одинаковых вызовов в четырёхстах файлах это верный способ возненавидеть миграцию и наделать опечаток. Экосистемы отвечают на это инструментами автоматической переработки кода. В Python с давних времён работает утилита 2to3, Angular обновляет код схемами через ng update, Rails перегенерирует конфигурацию командой app:update, для JavaScript существует движок jscodeshift, на котором пишут собственные преобразования. Типовая последовательность умещается в три команды:
2to3 -w -n app/
ng update @angular/core
bin/rails app:update
Первый запуск делают на одном модуле, просматривают каждое изменение глазами и прогоняют тесты. Автоматика правит механику, но не всегда понимает намерение: переименованный метод может потребовать и переименования теста, и правки строк в конфигурации. Поэтому правило такое: модуль, диф, тесты, коммит, и только потом следующий модуль.
Когда повторяющихся мест много, выгодно написать собственное преобразование под внутренние паттерны проекта. Скажем, если команда двести раз вызвала свой сервис через устаревшую обёртку, автоматическая замена обёртки на новый вход сэкономит несколько дней и, что важнее, снимет соблазн оставить пару мест на потом. Долю автоматических изменений измеряют: если скрипты закрыли семьдесят процентов правок, а тридцать остались на ручную доводку, оценка миграции становится расчётом, а не пожеланием.
Переход по одной минорной версии вместо прыжка через несколько мажоров
Самая недооценённая тактика миграции: не прыгать сразу на четыре версии вперёд. Хороший пример календарь Rails. Версия 5.2 вышла в апреле 2018 года, затем 6.0 в августе 2019-го, 6.1 в декабре 2020-го, 7.0 в декабре 2021-го, 7.1 в октябре 2023-го, 7.2 в августе 2024-го и 8.0 в ноябре 2024-го. Прыжок с 5.2 сразу на 8.0 означает, что придётся одновременно разруливать изменения четырёх мажорных релизов, а подсказок фреймворка по устаревшим вызовам будет так много, что они перестанут быть подсказками.
Правильный путь: поднимать версию по одной ступени. В Gemfile фиксируют следующую границу:
gem "rails", "~> 6.1.0"
Дальше обновляют зависимости, запускают генератор app:update, просматривают дифф конфигурационных файлов, чинят предупреждения и гоняют тесты. Каждый такой шаг занимает дни, а не месяцы, потому что между соседними версиями мало разрушительных изменений. Зато дойти с 5.2 до 8.0 можно за несколько спокойных итераций, ни одна из которых не требует остановки продуктовой разработки.
Тот же принцип работает в любой экосистеме: Angular обновляется мажорами по очереди через ng update, Vue поднимается через режим совместимости и предупреждения, Python переезжал релизами 2.6 и 2.7 с постепенным включением проверок третьей версии. Общее правило одно: расстояние между соседними ступенями должно быть таким, чтобы шаг проходил за неделю.
Тесты, параллельный прогон и зелёная сборка как страховка перехода
Без страховки пошаговая миграция не отличается от хождения по канату без сетки. Страховку строит инженер по тестированию ещё до первой правки. Критические пути: авторизация, оплата, расчёт тарифов, выгрузка отчётности, обязаны иметь автоматические проверки, причём договорные тесты на стыках с соседними системами важнее юнит-тестов на кнопках. Снимок разметки экрана ловит незаметную смену поведения нового фреймворка лучше, чем человек.
Дальше в игру вступает параллельный прогон. На стенде разворачивают обе версии, направляют копию реального трафика в новую и сверяют ответы. Расхождение в поле суммы заказа это не теоретическая проблема из чек-листа, а конкретное место, найденное до клиента. Когда совпадение стабильно, новую версию выпускают на небольшой доле пользователей, затем расширяют: 1%, 5%, 25%, 100%. Каждая ступень сопровождается метриками ошибок и скорости, и у любой из них есть кнопка отката.
Отдельно проверяют бюджет производительности. Новая версия фреймворка может изменить размер сборки или время отзыва, поэтому эти цифры проверяют автоматически на каждом релизе миграции, а не узнают о них в день полного переключения.
Как завершить миграцию и не остаться на совместимом мосту навсегда
Совместимый режим и переходные слои это долг, который растёт со временем. Он тормозит сборку, путает новых разработчиков и скрывает различия версий, поэтому у миграции обязан быть измеримый финал. Критерии закрытия формулируют заранее: ноль предупреждений об устаревших вызовах в логах сборки, ноль флагов совместимости в конфигурации, удалённые из зависимостей пакеты-мосты вроде six или @vue/compat.
Срок финала привязывают к дате прекращения поддержки старой версии, а не к настроению команды. Пока продукт живёт на мосту, к нему не применяются исправления новой версии, и каждое затягивание означает накопленный риск. Практика показывает: миграция, у которой нет дедлайна, длится вечно, а миграция с датой и списком модулей заканчивается вовремя, потому что каждое утро команда видит одну и ту же короткую доску задач.
Параллельно с переездом вводят храповик: устаревшие вызовы запрещают на уровне линтера, чтобы объём миграции не рос обратно. В JavaScript-проектах для этого годится правило no-restricted-imports:
"no-restricted-imports": ["error", {
"paths": [{ "name": "legacyui", "message": "use new screens module" }]
}]
Правило срабатывает и в редакторе, и в сборке, поэтому случайно потянуть старый вызов в новый код уже не выйдет. Храповик особенно важен там, где миграция длится кварталы: без него новые функции пишут на старом фундаменте, и финиш отодвигается с каждой неделей. С храповиком направление только одно: вперёд, потому что обратная дорога закрыта настройкой за десять минут.
Пошаговая миграция выглядит скучнее героического проекта с красиво написанным планом на вики. Зато она заканчивается. Продукт продолжает выпускать функции каждые две недели, заказчик не замечает переезда, разработчики не выгорают на переписывании одного и того же экрана четвёртый месяц, а старая версия тихо покидает продакшен, не собрав ни одного громкого инцидента. Фреймворк обслуживает продукт, и смена фреймворка обязана подчиняться ритму продукта, а не наоборот. Обратный порядок команда выбирает редко и добровольно: обычно его навязывает усталость, а усталость плохой архитектор.