Наставник по миграции с Vue 2 на Vue 3
Строит пошаговый план миграции с Vue 2 на Vue 3 под конкретный проект: оценка блокеров, порядок работ, замена Options API и mixins, совместимость библиотек.
Пример запуска
Сгенерировано Claude Opus 5.5, 24 сентября 2026 г.
Значения переменных
- Версия и размер проекта
- Vue 2.7.14, ~340 SFC-компонентов, около 62 000 строк во frontend-репозитории (внутренняя CRM для логистической компании, 4 года в проде)
- Основные библиотеки
- vue-router 3.6, Vuex 3.6, элементы UI на Element UI (не Element Plus), vue-cli 4 + Webpack, дополнительно vee-validate 3, vue-i18n 8, portal-vue для модалок, moment.js
- Особенности
- Активно используются mixins (около 40 штук, в т.ч. общий formMixin и tableMixin почти в каждом списке), 2 глобальных фильтра (formatDate, formatMoney) применяются в шаблонах повсеместно, event bus (new Vue()) для общения между несвязанными компонентами (уведомления, обновление счётчиков в шапке), $listeners используется в нескольких обёрточных компонентах над Element UI, functional components — 3 штуки для рендеринга иконок статусов
- Ограничения
- {"versiia_i_razmer_proekta": "Vue 2.7.14, ~340 SFC-компонентов, около 62 000 строк во frontend-репозитории (внутренняя CRM для логистической компании, 4 года в проде)", "osnovnye_biblioteki": "vue-router 3.6, Vuex 3.6, элементы UI на Element UI (не Element Plus), vue-cli 4 + Webpack, дополнительно vee-validate 3, vue-i18n 8, portal-vue для модалок, moment.js", "osobennosti": "Активно используются mixins (около 40 штук, в т.ч. общий formMixin и tableMixin почти в каждом списке), 2 глобальных фильтра (formatDate, formatMoney) применяются в шаблонах повсеместно, event bus (new Vue()) для общения между несвязанными компонентами (уведомления, обновление счётчиков в шапке), $listeners используется в нескольких обёрточных компонентах над Element UI, functional components — 3 штуки для рендеринга иконок статусов", "ograniceniia": "На миграцию выделено не более 6 недель силами 2 фронтенд-разработчиков параллельно с обычной разработкой фич (не более 30% времени команды на миграцию в неделю), останавливать выпуск новых фич полностью нельзя, регресс-тестирование только ручное силами QA (1 человек, 2 дня в конце каждого этапа), деплой возможен только по вторникам и четвергам"}
Миграция CRM с Vue 2.7 на Vue 3: оценка и план
1. Вердикт
Мигрировать стоит, но не за 6 недель. Путь поэтапный: сначала подготовка на Vue 2.7 с выкладкой в прод, затем короткое переключение в отдельной ветке с временным @vue/compat.
Бюджет. 2 разработчика × 30% × 6 недель ≈ 18 человеко-дней. Полная миграция этого проекта по моей оценке займёт 55–100 человеко-дней (разбивка в разделе 3). Это в 3–5 раз больше выделенного. При темпе 30% полный цикл займёт 5–8 месяцев. Если на 2–3 недели переключения дать команде 70–100% времени, выйдет 3–4 месяца.
Почему мигрировать всё-таки нужно:
- Vue 2 в EOL с 31.12.2023, то есть уже почти 3 года без патчей.
- Element UI мёртв.
- Зависимости заморожены: ни одну ключевую библиотеку нельзя обновить.
- Растёт стоимость найма и поддержки.
Когда мигрировать не стоит: если CRM планируют заменить в ближайшие 1–2 года. Тогда разумнее остаться на 2.7, при необходимости купить платную расширенную поддержку (HeroDevs NES) и не тратить 3–5 человеко-месяцев. Для внутренней системы за авторизацией риск безопасности умеренный, это не пожар.
Почему не big bang: при 30% загрузки и непрерывной разработке фич долгоживущая ветка «Vue 3» разойдётся с main за 2–3 недели. Её придётся бесконечно ребейзить.
Почему @vue/compat не решает задачу целиком: главный блокер — Element UI. Он завязан на внутренности Vue 2 и на compat надёжно не работает. Значит, UI-кит всё равно меняется одним скачком вместе с рантаймом. Поэтому compat здесь служит диагностическим инструментом на время переключения, а не способом месяцами жить в смешанном режиме.
Стратегия: перенести в main на Vue 2.7 всё, что туда переносится. Тогда ветка переключения становится маленькой, живёт не больше 2–3 недель, а изменения в ней делаются скриптами (codemods), которые можно перезапустить на свежем main.
Что дадут выделенные 6 недель:
- три релиза подготовительных изменений в прод;
- прогон переключения на нескольких реальных экранах (spike);
- откалиброванную оценку и обоснованный запрос бюджета на переключение.
2. Таблица блокеров
| Зависимость | Статус под Vue 3 | Замена | Можно ли готовить на 2.7 | Комментарий |
|---|---|---|---|---|
| Element UI | ❌ только Vue 2, не поддерживается | Element Plus | Частично (обёртки, иконки, v-slot) |
Главный блокер. Меняются иконки (классы el-icon-* → SVG-компоненты из @element-plus/icons-vue), значения size (medium/small/mini → large/default/small), :visible.sync → v-model, type="text" у кнопок, токены value-format у дат (dayjs: YYYY-MM-DD вместо yyyy-MM-dd), SCSS-переменные темы, локали. Codemod от GoGoCode (gogocode-plugin-element) закрывает, по опыту, 50–70% работы, остальное руками. |
| vee-validate 3 | ❌ только Vue 2 | vee-validate 4 (другой API: useForm/<Field>) или правила el-form (async-validator) |
✅ если выбрать el-form rules |
Второй по объёму блокер. У el-form rules почти одинаковый API в Element UI и Element Plus, поэтому формы можно переводить на 2.7 постепенно и катить в прод. vee-validate 4 так не получится: он только для Vue 3. Выбор делается в этапе 3 по результатам spike. |
| vue-router 3.6 | ❌ | vue-router 4 (или актуальная мажорная, API-совместимая с 4) | ❌ только при переключении | Меняется mode → createWebHistory(), catch-all * → /:pathMatch(.*)*, onReady → isReady(). У <router-link> убраны tag и event, router.match → resolve. 2–4 дня. |
| Vuex 3.6 | ❌, но есть Vuex 4 с тем же API | Vuex 4 при переключении, Pinia 2 постепенно | ✅ Pinia 2 работает на 2.7 | Не блокер. Переписывать весь стор на Pinia ради миграции не нужно. Важно: Pinia 3 поддержку Vue 2 убрала, поэтому ставим pinia@^2. |
| vue-cli 4 + Webpack | ⚠️ vue-cli 5 умеет Vue 3, но в режиме поддержки | Vite (@vitejs/plugin-vue2 сейчас, @vitejs/plugin-vue потом) |
✅ полностью | Затрагивает process.env.VUE_APP_* → import.meta.env.VITE_*, require() и require.context → import и import.meta.glob, ~-импорты в SCSS, загрузку source maps в Sentry. |
| vue-i18n 8 | ❌ | vue-i18n актуальной мажорной (v11 на момент написания; v9/v10 уже EOL) | ❌ | $t в шаблонах почти не меняется. $tc, v-t и legacy-режим объявлены устаревшими, поэтому при переключении сразу уходим в Composition-режим или ставим задачу на это. Локаль Element Plus подключается отдельно. |
| portal-vue | ⚠️ есть portal-vue 3 | Встроенный <Teleport> |
❌ | У модалок Element Plus уже есть teleported/append-to-body, поэтому часть порталов просто исчезнет. |
| moment.js | ✅ от фреймворка не зависит | Оставить, со временем dayjs | — | Не блокер. Трогать при переключении не нужно, только форматы в пикерах Element Plus. |
Ломающие изменения Vue 3, которые заденут именно этот проект
| Что | Где у вас | Готовим на 2.7? |
|---|---|---|
| Фильтры удалены | formatDate, formatMoney повсюду |
✅ Codemod {{ x | formatDate }} → {{ $fmt.date(x) }}. Сейчас через Vue.prototype.$fmt, после переключения через app.config.globalProperties.$fmt, это одна строка. |
$on/$off/$once удалены |
event bus (new Vue()) + паттерн this.$once('hook:beforeDestroy') (проверьте grep'ом, он встречается почти везде) |
✅ Шина → mitt за обёрткой с тем же API. Счётчики в шапке лучше перенести в Pinia-стор, шина для них не нужна. |
v-model на компонентах: value/input → modelValue/update:modelValue, .sync → v-model:prop |
Все кастомные инпуты и обёртки | ⚠️ Частично: в новых компонентах объявляем model: { prop: 'modelValue', event: 'update:modelValue' }, это работает уже на 2.7. |
$listeners слит с $attrs |
Обёртки над Element UI | ❌ При переключении убираем v-on="$listeners". Внимание: при inheritAttrs: false в $attrs теперь попадают class и style, и вёрстка обёрток может «поехать». |
.native удалён |
@click.native на компонентах |
❌ На 2.7 он ещё нужен. При переключении удаляем, а в компонентах объявляем emits. |
$children удалён |
Проверить grep'ом | ✅ Заменить на ref или provide/inject. |
<template functional> удалён |
3 иконки статусов | ✅ Сделать обычными компонентами, это полчаса. |
slot-scope (синтаксис 2.5) |
Скорее всего в таблицах Element UI | ✅ Автофикс eslint-plugin-vue → v-slot / #default="{ row }". |
data в mixins сливается поверхностно |
formMixin: если и миксин, и компонент объявляют form: {...}, в Vue 3 объект компонента затрёт объект миксина целиком |
⚠️ Проверить заранее, найти все такие пересечения. |
beforeDestroy/destroyed → beforeUnmount/unmounted; Vue.set/$set/$delete удалены; приоритет v-if над v-for; watch на массив требует deep; классы transition v-enter → v-enter-from |
По всему коду | Частично: убрать v-if+v-for на одном элементе, сократить $set. Остальное в ветке переключения, по предупреждениям compat. |
3. План по этапах
Общие правила для всех этапов:
- Этапы выкатываются во вторник. Четверг той же недели остаётся окном для хотфикса или отката. Миграционные изменения в четверг не выкатываем: иначе на выходные уходим без шанса быстро починить.
- QA (2 дня в конце этапа) работает по чек-листу из 25–35 критичных сценариев (заказы, рейсы, диспетчеризация, отчёты, права). Полная ручная регрессия CRM за 2 дня невозможна, поэтому чек-лист составляется один раз на старте и дальше переиспользуется.
- Оценки ниже в человеко-днях чистого времени. Один разработчик на 30% даёт примерно 1,5 дня в неделю, двое вместе примерно 3 дня в неделю.
В рамках выделенных 6 недель (≈18 чел.-дней)
Этап 1. Страховочная сетка и чистка на 2.7 (недели 1–2, 6–8 чел.-дней)
| Шаг | Оценка |
|---|---|
| Playwright smoke-набор из 12–15 критичных сценариев против стенда. Это главная инвестиция: при одном QA без автотестов каждый следующий этап будет лотереей. | 3–4 |
eslint-plugin-vue: включить vue3-правила с фиксацией текущего уровня (baseline). Существующие нарушения идут как warning, счётчик предупреждений может только уменьшаться (ratchet в CI). |
0,5 |
Фильтры → $fmt.date/$fmt.money (codemod + ручная проверка сложных выражений) |
1–1,5 |
Event bus → mitt-обёртка; счётчики шапки → Pinia-стор; $once('hook:…') → обычные хуки |
1–1,5 |
slot-scope → v-slot (автофикс), $children, functional-иконки, v-if+v-for |
0,5–1 |
- Критерий готовности: в коде 0 фильтров, 0
new Vue()-шин, 0slot-scope, 0$children, 0 functional-шаблонов; smoke-набор зелёный в CI. - Выкатка: codemod фильтров отдельным PR, остальное одним релизом. Вторник 3-й недели.
Этап 2. Vite на Vue 2.7 (недели 3–4, 5–7 чел.-дней)
| Шаг | Оценка |
|---|---|
Vite + @vitejs/plugin-vue2, алиасы, env-переменные, require.context → import.meta.glob, SCSS и тема Element UI |
3–4 |
| Прод-сборка: чанки, хеши, source maps в Sentry, nginx, CI | 1–2 |
| Прогон smoke + сравнение размера бандла | 0,5 |
Параллельно: <AppIcon name> вместо прямых el-icon-*, чтобы при переключении менять одну реализацию, а не сотни мест |
1 |
- Критерий готовности: прод-сборка идёт через Vite;
vue.config.jsудалён; время холодного старта dev-сервера измерено (обычно в 5–10 раз быстрее — это аргумент для бизнеса); smoke зелёный. - Откат: конфиг vue-cli живёт в ветке ещё 1 релиз.
- Выкатка: вторник 5-й недели.
Этап 3. Pinia, spike переключения, калибровка (недели 5–6, 5–6 чел.-дней)
| Шаг | Оценка |
|---|---|
pinia@^2 рядом с Vuex. Политика: новые модули только в Pinia. Существующие не переносим. |
0,5 |
Spike в отдельной ветке: Vue 3 + @vue/compat + Element Plus + router 4 на 3 типовых экранах (список с tableMixin, форма с formMixin и vee-validate, модалка через portal). Замерить реальную стоимость одного компонента. Прогнать GoGoCode и оценить долю ручной доводки. |
3–4 |
Решение по валидации: vee-validate 4 или el-form rules. Решение по i18n-режиму. |
0,5 |
| Документ с откалиброванной оценкой и запросом бюджета на этапы 4–6 | 0,5 |
- Критерий готовности: 3 экрана работают на Vue 3 в ветке; оценка этапов 4–6 уточнена до ±25%; решения по валидации и i18n зафиксированы.
- Выкатка: в прод уходит только Pinia-инфраструктура. Spike не мёржится, это исследование.
За пределами 6 недель (нужен отдельный бюджет)
Этап 4. Формы и обёртки на 2.7 (15–25 чел.-дней при 30% ≈ 5–8 недель)
- Если выбраны
el-form rules: формы переводятся пачками по 5–10 и выкатываются каждый вторник. ПараллельноtableMixin→useTable()(подробнее ниже). - Все обёртки над Element UI сведены к небольшому набору (
AppDialog,AppSelect…): при переключении правим обёртку, а не каждое место использования. - Критерий готовности: 0 импортов vee-validate; прямые использования
el-dialog/el-icon-*только внутри обёрток.
Этап 5. Переключение (25–45 чел.-дней)
- Прошу на это время исключение: 2–3 недели при 70–100% загрузки. Иначе ветка разойдётся с main.
- Ветка живёт не дольше 3 недель. Все изменения делаются скриптами, которые перезапускаются на свежем main. Ручные правки ведутся отдельным списком.
- В одной ветке: Vue 3 +
@vue/compat+ Element Plus + router 4 + Vuex 4 + i18n +Teleport. Также$listeners,.native,v-model, хуки жизненного цикла. - Canary. Для внутренней CRM это идеально: сборка Vue 3 раскатывается на отдельный адрес (
next.crm…) с тем же бэкендом. Пилотная группа (один офис или смена диспетчеров, 5–15 человек) работает там 1 неделю. Ошибки отслеживаются в Sentry с тегом сборки. Затем nginx переключает всех по cookie, а старая сборка остаётся доступной для отката ещё 1–2 релиза. - Критерий готовности: smoke зелёный; QA-чек-лист пройден; в canary за неделю нет ошибок уровня «не могу работать»; доля ошибок в Sentry не выше, чем у Vue 2.
Этап 6. Снятие compat и доводка (8–15 чел.-дней, 2–4 недели)
- Переходим на
configureCompat({ MODE: 3 })глобально. Точечные исключения задаём черезcompatConfigна уровне компонента, по списку предупреждений. - Compat снимаем, когда в консоли на smoke-наборе 0 предупреждений, но не позже 2–3 релизов после переключения. Не раньше, потому что compat ловит то, что пропустили codemods. Не позже, потому что compat заметно замедляет рантайм, а команда быстро привыкает его не замечать.
Сводная оценка
| Блок | Чел.-дни | Допущения |
|---|---|---|
| Этапы 1–3 (подготовка + spike) | 16–21 | Укладываемся в 6 недель |
| Этап 4: формы/обёртки | 15–25 | 60–90 форм, около 15 обёрток |
| Этап 5: переключение (из них Element Plus 20–35) | 25–45 | Element используют около 250 из 340 компонентов, GoGoCode закрывает около 50% |
| Этап 6: compat + стабилизация | 8–15 | Включая баги после выкатки |
| Итого | ≈ 65–105 | Главный разброс даёт Element Plus |
Composition API: где переписывать, а где нет
Важно: Vue 2.7 уже поддерживает setup(), <script setup> и composables. Написанное сейчас переедет на Vue 3 без изменений. Mixins в Vue 3 работают, поэтому их переписывание нужно ради ясности кода, а не ради миграции.
Переписываем (окупается):
tableMixin→useTable({ fetch, defaultSort }). Пагинация, сортировка, фильтры, loading, синхронизация с query. Используется почти в каждом списке, значит максимальный выигрыш. Мигрируем постепенно: новый список пишем на composable, старые переводим, когда их всё равно трогаем.formMixin→useForm(), только вместе со сменой валидации (этап 4). Иначе делаем двойную работу.- Event bus →
useNotifications()и стор счётчиков. - Mixins, которые используются в 10+ компонентах или неявно зависят от полей компонента (
this.someFieldбез объявления).
Оставляем на Options API навсегда:
- стабильные страницы, которые не трогаются;
- простые презентационные компоненты;
- компоненты на 50–150 строк без переиспользуемой логики.
Options API в Vue 3 — полноценный first-class API, а не устаревший. Переписывать 340 компонентов «для единообразия» — лучший способ сорвать сроки и получить регрессии без пользы.
Типичные регрессии и как их ловить
| Регрессия | Как ловить |
|---|---|
Вёрстка обёрток «поехала» из-за class/style в $attrs |
Playwright-скриншоты ключевых экранов (toHaveScreenshot) + ручной просмотр обёрток |
Двойные обработчики событий (click и из $attrs, и из emit) |
Обязательные emits в каждом компоненте; smoke-проверка счётчиков и запросов (дублированные POST) |
Молча сломанный v-model на кастомных инпутах |
Smoke: сохранение форм с проверкой данных в ответе API |
| Даты в пикерах: неверный формат после смены токенов | Отдельный пункт QA: создание и фильтрация по датам, часовые пояса |
Затёртые данные formMixin из-за поверхностного слияния |
Grep пересечений до переключения + unit-тест на миксин |
Пропавшие реакции на изменения массивов (watch без deep) |
Предупреждения compat + smoke на экранах со списками |
Утечки подписок после удаления $on |
Sentry + профилирование памяти на длинной сессии диспетчера (они держат CRM открытой весь день) |
4. Правила для команды (с первого дня)
-
Никаких новых mixins. Общая логика оформляется только как composable
useXxx()вsrc/composables/. Новые компоненты пишутся на<script setup>или на Options API без mixins. Всё это работает на 2.7 и без изменений переедет на Vue 3. -
Запрещённые API — ошибка линтера в новых и изменённых файлах: фильтры,
$on/$off/$once(в том числеhook:-события),new Vue()как шина,$children,slot-scope,<template functional>,this.$setв новом коде (заменять объект целиком). Правилаeslint-plugin-vue:no-deprecated-filter,no-deprecated-events-api,no-deprecated-slot-scope-attribute,no-deprecated-functional-template,no-deprecated-dollar-scopedslots-api..nativeи$listenersпока оставляем как warning: на 2.7 они ещё легитимно нужны. -
Новые компоненты с
v-modelсразу в формате Vue 3:model: { prop: 'modelValue', event: 'update:modelValue' }и явныйemits. Вместо.sync— событиеupdate:xxx. Тогда при переключении их не придётся трогать. -
Новое состояние — только в Pinia (
pinia@^2), новые модули Vuex не создаются. Для связи несвязанных компонентов используется стор илиmitt-обёртка, не шина. -
Element UI используется только через наши обёртки (
AppIcon,AppDialog,AppSelect…). Новый код не использует классыel-icon-*и:visible.syncнапрямую. Даты в новом коде проходят через@/utils/date, прямой импорт moment запрещён: при переключении меняется одна реализация, а не сотни вызовов.
Главное: выделенные 6 недель стоит потратить на то, чтобы переключение стало маленьким, предсказуемым и имело оценку ±25%. Попытка уложить в них всю миграцию закончится незавершённой веткой, которая к концу квартала устареет.
Похожие промпты
Ревью pull request по diff
Проводит ревью изменений по diff и описанию PR: соответствует ли код заявленной цели, что сломается рядом, чего не хватает в тестах, — и пишет комментарии в формате для GitHub или GitLab.
Ревью Python-кода: типизация и идиомы
Проводит ревью Python-кода: ошибки, которые проявятся в рантайме, аннотации типов, идиоматичность, обработка исключений и ресурсов — с правками «до/после».
Ревью SQL-запросов и миграций
Проверяет SQL-запросы и миграции схемы: корректность на граничных данных, индексы под реальные выборки, блокировки при миграции на живой таблице и откат.
Ревью Next.js-кода: App Router и Server Components
Проводит ревью Next.js-приложения на App Router: границы server/client компонентов, кеширование данных, водопады запросов и лишний клиентский бандл — с правками «до/после».