Кастомные Vue‑компоненты в сгенерированном UI: безопасно с регенерацией
Узнайте, как добавлять кастомные Vue‑компоненты в сгенерированный UI и не потерять их при регенерации: простые границы, паттерн обёртки и правила передачи данных.

Что ломается, если вы правите сгенерированный UI
Сгенерированные интерфейсы предполагают, что их будут пересобирать. На платформах вроде AppMaster код веб‑приложения на Vue3 получается из визуальных билдов. Регенерация — это способ сохранить согласованность экранов, логики и модели данных.
Проблема проста: если вы правите сгенерированные файлы напрямую, следующая регенерация может перезаписать ваши правки.
Именно поэтому команды добавляют пользовательский код. Встроенные блоки интерфейса покрывают стандартные формы и таблицы, но реальные приложения обычно требуют нескольких особых частей: сложные диаграммы, выбор места на карте, редакторы форматированного текста, поля для подписи или планировщики с перетаскиванием. Это хорошие причины добавить кастомные Vue‑компоненты — если обращаться с ними как с дополнениями, а не редактируемыми артефактами.
Когда пользовательский код смешан с сгенерированным, проблемы часто проявляются поздно и неочевидно. Вы можете не заметить их, пока не произойдёт следующая регенерация интерфейса или пока коллега не изменит экран в визуальном редакторе. Частые проблемы:
- Ваша разметка исчезает, потому что шаблон был регенерирован.
- Импорты или регистрация ломаются из‑за переименований файлов или изменений структуры.
- «Маленькая правка» превращается в конфликт слияния при каждом деплое.
- Сгенерированная логика и кастомная логика расходятся, и начинают падать пограничные сценарии.
- Обновления кажутся рискованными, потому что вы не знаете, что будет заменено.
Цель не в том, чтобы отказаться от кастомизации, а в том, чтобы сделать регенерацию предсказуемой. Если вы держите чистую границу между сгенерированными экранами и кастомными виджетами, регенерация остаётся рутинной, а не стрессовой.
Правило границы, которое делает регенерацию безопасной
Если нужно кастомизировать без потери правок, следуйте одному правилу: никогда не редактируйте сгенерированные файлы. Рассматривайте их как read‑only выходной артефакт.
Думайте об интерфейсе как о двух зонах:
- Сгенерированная зона: страницы, лейауты и экраны, созданные генератором.
- Кастомная зона: ваши вручную написанные Vue‑компоненты в отдельной папке.
Сгенерированный UI должен потреблять ваши компоненты, но не быть местом их разработки.
Чтобы это работало долго, держите «границу» маленькой и понятной. Кастомный виджет должен вести себя как небольшой продукт с контрактом:
- Props внутрь: только то, что нужно для рендера.
- События наружу: только то, на что страница должна реагировать.
Избегайте доступа к глобальному состоянию или вызовов посторонних API изнутри виджета, если это не часть контракта.
В AppMaster‑подобных сгенерированных Vue3‑экранах это обычно означает минимальную проводку в сгенерированном экране: передача props и обработка событий. Эта проводка может меняться при регенерациях, но остаётся маленькой и простой для восстановления. Реальная работа остаётся в кастомной зоне.
Паттерны изоляции, которые хорошо работают с Vue3
Цель проста: регенерация должна свободно заменять сгенерированные файлы, а ваш код виджета должен оставаться нетронутым.
Практический подход — держать уникальные виджеты как небольшой внутренний модуль: компоненты, стили и утилиты в одном месте. В сгенерированных Vue3‑приложениях кастомный код обычно живёт вне сгенерированных страниц и импортируется как зависимость.
Очень помогает обёртка. Пусть обёртка читает форму данных страницы, нормализует её и передаёт чистые props виджету. Если форма данных изменится позже, вы обычно обновляете обёртку, а не переписываете сам виджет.
Несколько устойчивых паттернов:
- Рассматривайте виджет как чёрный ящик: props внутрь, события наружу.
- Используйте обёртку для маппинга ответов API, дат и идентификаторов в удобный для виджета формат.
- Делайте стили scoped, чтобы сгенерированные страницы случайно не переопределяли ваш виджет.
- Не полагайтесь на DOM‑структуру родителя или специфичные классы страницы.
По части стилей — предпочтите scoped CSS (или CSS Modules) и неймспейс для общих классов. Если виджету нужно соответствие теме приложения, передавайте токены темы как props (цвета, отступы, размер шрифта), вместо импорта стилей страницы.
Слоты безопасны, если они небольшие и опциональные, например, сообщение «пустого состояния». Если слоты начинают контролировать основную разметку или поведение, значит вы вернули часть логики в сгенерированный слой — и там начинается боль при регенерации.
Проектирование стабильного контракта компонента (props и события)
Самый надёжный способ сохранить регенерацию бесшовной — считать каждый виджет стабильным интерфейсом. Сгенерированные экраны могут меняться, а ваш компонент — нет.
Начните с входов (props). Держите их немного, предсказуемыми и простыми для валидации. Предпочитайте простые примитивы и контролируемые объекты. Добавляйте значения по умолчанию, чтобы виджет адекватно работал, даже если страница ещё ничего не передала. Если что‑то может быть некорректным (ID, строка даты, перечисления), валидируйте и мягко падайте: показывайте пустое состояние вместо краша.
Для выходов стандартизируйте события, чтобы виджеты вели себя единообразно в приложении. Надёжный набор событий:
update:modelValueдляv-modelchangeдля подтверждённых изменений (не каждое нажатие клавиши)errorкогда компонент не может выполнить задачуreadyкогда асинхронная работа завершена и виджет готов к использованию
Если задействована асинхронная логика, сделайте это частью контракта: выставьте loading и disabled props и подумайте о errorMessage для ошибок со стороны сервера. Если компонент сам делает запросы, всё равно эмитьте error и ready, чтобы родитель мог отреагировать (показать тост, залогировать, показать альтернативный UI).
Ожидания по доступности
Включайте доступность в контракт: принимайте label (или ariaLabel), документируйте поведение с клавиатурой и делайте фокус предсказуемым после действий.
Например, таймлайн‑виджет на дашборде должен поддерживать стрелочные клавиши для переключения между элементами, Enter — для открытия деталей, и возвращать фокус к контролу, который открыл диалог, после его закрытия. Это делает виджет переиспользуемым на разных сгенерированных экранах без дополнительной доработки.
Пошагово: добавляем уникальный виджет, не трогая сгенерированные файлы
Начните с малого: один экран, который важен пользователям, один виджет, который делает его заметно лучше. Узкая первая правка помогает понять, что именно регенерация затрагивает.
-
Создайте компонент вне сгенерированной области. Поместите его в папку под вашим контролем и под версионным контролем (часто
customилиextensions). -
Держите публичную поверхность минимальной. Несколько props внутрь, несколько событий наружу. Не передавайте весь стейт страницы.
-
Добавьте тонкую обёртку, которой вы тоже владеете. Её задача — переводить «сгенерированные данные страницы» в контракт виджета.
-
Интегрируйте через поддерживаемую точку расширения. Ссылайтесь на обёртку так, чтобы не требовалось править сгенерированные файлы.
-
Регенерируйте и проверяйте. Ваша папка с кастомным кодом, обёртка и компонент должны остаться без изменений и компилироваться.
Держите границы резкими: виджет отвечает за отображение и взаимодействие. Обёртка маппит данные и пересылает действия. Бизнес‑правила остаются в логике приложения (backend или общие процессы), а не внутри виджета.
Полезная проверка: если регенерация случится прямо сейчас, сможет ли коллега пересобрать приложение и получить тот же результат без ручных правок? Если да — паттерн удачный.
Куда класть логику, чтобы UI было проще поддерживать
Кастомный виджет в основном должен думать о внешнем виде и реакции на действия пользователя. Чем больше бизнес‑правил в виджете, тем труднее его переиспользовать, тестировать и менять.
Хороший принцип: храните бизнес‑логику на уровне страницы или фичи, а виджет делайте «тупым». Страница решает, какие данные передать виджету и что делать, когда виджет эмитит событие. Виджет рендерит и сообщает о намерениях пользователя.
Если логика действительно нужна рядом с виджетом (форматирование, небольшое состояние, клиентская валидация), спрячьте её за небольшим сервисным слоем. Во Vue3 это может быть модуль, composable или store с чётким API. Виджет импортирует этот API, а не рандомные части приложения.
Практическое разделение:
- Виджет (компонент): UI‑состояние, обработка ввода, визуал, эмитит события вроде
select,change,retry. - Сервис/composable: формирование данных, кеширование, преобразование ошибок API в сообщения для пользователя.
- Страница/контейнер: бизнес‑правила, права, когда загружать данные, когда сохранять.
- Сгенерированные части: остаются нетронутыми; передают данные и слушают события.
Избегайте прямых API‑вызовов внутри виджета, если это не часть его контракта. Если виджет сам отвечает за загрузку, сделайте это очевидным (назовите его, например, CustomerSearchWidget и держите код запросов в одном сервисе). В противном случае передавайте items, loading и error через props.
Сообщения об ошибках должны быть ориентированы на пользователя и единообразны. Вместо показа сырых серверных текстов мапьте ошибки в небольшой набор фраз, используемых в приложении, например: «Не удалось загрузить данные. Попробуйте ещё раз.» По возможности добавляйте действие повторной попытки и логируйте подробные ошибки вне виджета.
Пример: кастомный ApprovalBadge не должен решать, можно ли согласовать счёт. Пусть страница вычисляет status и canApprove. Бэйдж эмитит approve, а страница запускает правило и вызывает backend, затем передаёт обратно чистое состояние успеха/ошибки в UI.
Распространённые ошибки, которые создают проблемы после регенерации
Большинство проблем — не про Vue. Они про смешивание кастомной работы в местах, которыми владеет генератор, или про зависимость от деталей, которые скорее всего поменяются.
Ошибки, которые превращают небольшой виджет в постоянную головную боль:
- Правка сгенерированных Vue‑файлов и потеря контроля над изменениями.
- Глобальные стили или широкие селекторы, которые тихо влияют на другие экраны, когда меняется разметка.
- Чтение или мутация форм сгенерированного стейта напрямую, так что простое переименование ломает виджет.
- Набивание компонентой слишком многих предположений, специфичных для одной страницы.
- Изменение API компонента (props/events) без плана миграции.
Обычная ситуация: вы добавили кастомную таблицу, и всё работает. Через месяц сгенерированная разметка изменилась и ваш глобальный .btn теперь влияет на логин и админ‑страницы. Или объект данных сменил путь с user.name на user.profile.name, и виджет молча падает. Проблема не в виджете, а в зависимости от нестабильных деталей.
Две привычки решают большую часть проблем:
Во‑первых, считайте сгенерированный код read‑only и держите кастомные файлы отдельно, с ясной точкой импорта.
Во‑вторых, делайте контракт компонента маленьким и явным. Если нужно его эволюционировать, добавьте простой prop версии (например, apiVersion) или поддерживайте старую и новую формы props на один релиз.
Быстрый чек‑лист перед выпуском кастомного компонента
Перед мержем убедитесь, что пользовательский виджет переживёт очередную регенерацию без героических усилий и что другой разработчик сможет его переиспользовать.
- Тест регенерации: выполните полную регенерацию и сборку. Если пришлось править сгенерированный файл — граница неверна.
- Ясные входы и выходы: props внутрь, emits наружу. Избегайте магических зависимостей вроде доступа к внешнему DOM или конкретному стору страницы.
- Изоляция стилей: стили scoped и явный префикс классов (например,
timeline-). - Все состояния выглядят корректно: loading, error и empty состояния должны присутствовать и быть осмысленными.
- Переиспользование без копипаста: проверьте, что можно поставить виджет на вторую страницу, изменив только props и обработчики событий, а не копируя внутренности.
Простой валидационный тест: представьте, что ставите виджет на админскую страницу и на клиентский портал. Если оба случая работают с изменением только props и обработчиков — вы в безопасности.
Реалистичный пример: добавляем таймлайн‑виджет на дашборд
Саппорт‑команде часто нужен экран, который рассказывает историю тикета: смены статусов, внутренние заметки, ответы клиента и события оплаты или доставки. Таймлайн‑виджет подходит, но редактировать сгенерированные файлы нежелательно — вы не хотите потерять правки при следующей регенерации.
Безопасный подход — держать виджет изолированным вне сгенерированного UI и вставлять его через тонкую обёртку.
Контракт виджета
Держите его простым и предсказуемым. Например, обёртка передаёт:
ticketId(string)range(последние 7 дней, последние 30 дней, кастом)mode(compact vs detailed)
Виджет эмитит:
selectкогда пользователь кликает по событиюchangeFiltersкогда пользователь меняет диапазон или режим
Тогда виджет ничего не знает про страницу, модели данных или способ запросов. Он рендерит таймлайн и сообщает о действиях пользователя.
Как обёртка связывает виджет со страницей
Обёртка живёт рядом с дашбордом и переводит данные страницы в контракт. Она читает текущий ticketId из стейта страницы, конвертирует UI‑фильтры в range и маппит записи бэкенда в формат событий для виджета.
Когда виджет эмитит select, обёртка может открыть панель деталей или инициировать действие страницы. Когда эмитит changeFilters, обёртка обновляет фильтры страницы и перезагружает данные.
При регенерации дашборда виджет остаётся нетронутым, потому что он живёт вне сгенерированных файлов. Обычно вы трогаете только обёртку, если страница переименовала поле или изменила способ хранения фильтров.
Привычки тестирования и релиза, которые предотвращают сюрпризы
Кастомные компоненты обычно ломаются банально: изменилась форма prop, событие перестало эмититься или сгенерированная страница перерисовывает чаще, чем ожидал виджет. Несколько привычек ловят эти проблемы рано.
Локальное тестирование: ловите разрывы границы раньше
Относитесь к границе между сгенерированным UI и вашим виджетом как к API. Тестируйте виджет без полного приложения сначала, с хардкод‑props, соответствующими контракту.
Отрисуйте его с «счастливыми» данными и с отсутствующими значениями. Симулируйте ключевые события (save, cancel, select) и убедитесь, что родитель обрабатывает их. Тестируйте медленные ответы и узкие экраны. Проверьте, что виджет не пишет в глобальный стейт, если это не часть контракта.
Если вы строите на AppMaster Vue3 веб‑приложении, выполняйте эти проверки до регенерации. Так легче диагностировать проблему, когда вы не меняли сразу две вещи.
Регрессия после регенерации: что перепроверить в первую очередь
После каждой регенерации перепроверьте точки соприкосновения: передаются ли те же props и обрабатываются ли те же события? Именно там обычно проявляется ломка.
Держите включение предсказуемым. Избегайте хрупких импортов, зависящих от путей файлов, которые могут поменяться. Используйте один стабильный вход для ваших кастомных компонентов.
Для продакшена добавьте лёгкое логирование и захват ошибок внутри виджета:
- монтирование с ключевыми props (с аннотацией)
- нарушения контракта (отсутствующий требуемый prop, неправильный тип)
- неудачные вызовы API с коротким кодом ошибки
- неожиданные пустые состояния
Когда что‑то ломается, вы должны быстро понять: регенерация изменила входы или виджет сам стал виновником.
Следующие шаги: сделать паттерн повторяемым по всему приложению
Когда первый виджет заработал, настоящая выгода — сделать паттерн повторяемым, чтобы следующий виджет не был «костылём».
Оформите небольшой внутренний стандарт для контрактов виджетов и положите его туда, где команда хранит заметки по приложению. Держите его простым: нейминг, обязательные и опциональные props, маленький набор событий, поведение при ошибках и чёткая ответственность (что живёт в сгенерированном UI, а что в кастомной папке).
Также сформулируйте правила границы простым языком: не редактировать сгенерированные файлы, держать кастомный код изолированным и передавать данные только через props и события. Это предотвращает «быструю правку», которая превращается в постоянную налоговую нагрузку по поддержке.
Перед созданием второго виджета проведите небольшой эксперимент с регенерацией. Выпустите первый виджет, затем сделайте хотя бы две регенерации в обычных изменениях (изменение лейбла, смена лейаута, новое поле) и убедитесь, что ничего не ломается.
Если вы используете AppMaster, зачастую имеет смысл держать большую часть UI и логики в визуальных редакторах (UI builders, Business Process Editor и Data Designer). Оставляйте кастомные Vue‑компоненты для действительно уникальных виджетов, которые редакторы выразить не могут: специализированные таймлайны, интерактивные диаграммы или необычные контролы. Если хотите чистую отправную точку, AppMaster на appmaster.io проектирован вокруг регенерации, поэтому изоляция кастомных виджетов естественно вписывается в рабочий процесс.
Вопросы и ответы
Редактирование сгенерированных Vue‑файлов рисковано, потому что при регенерации их могут полностью перезаписать. Даже если изменение пройдет один раз, небольшая правка в визуальном редакторе может пересоздать шаблоны и стереть ваши правки.
Поместите весь вручную написанный Vue‑код в отдельную папку, которую вы контролируете (например, custom или extensions), и подключайте его как зависимость. Считайте сгенерированные страницы выходными файлами только для чтения и связывайтесь с вашими компонентами через небольшой и стабильный интерфейс.
Обёртка — это тонкий компонент, который вы контролируете и который находится между сгенерированной страницей и вашим виджетом. Он преобразует форму данных страницы в чистые props и переводит события виджета в действия страницы. Тогда при смене формы данных обычно нужно менять только обёртку.
Держите контракт минимальным: несколько props для данных, которые виджету действительно нужны, и несколько событий для сообщений о действиях пользователя. Отдавайте предпочтение простым типам и контролируемым объектам, задавайте значения по умолчанию, проверяйте входящие данные и при ошибках показывайте пустое состояние, а не краш.
update:modelValue подходит, когда компонент ведёт себя как элемент формы и должен поддерживать v-model. change лучше использовать для подтверждённых действий, например, когда пользователь нажимает Сохранить или завершил выбор, чтобы родитель не обрабатывал каждое нажатие клавиши.
Сделайте стили виджета scoped и используйте понятный префикс классов, чтобы сгенерированные страницы случайно не переопределяли ваш CSS. Если нужно соответствие теме приложения, передавайте токены темы как props (цвета, отступы, размер шрифта) вместо импорта стилей страницы.
По умолчанию — вне виджета. Пусть страница или backend решают про права, правила валидации и сохранение, а виджет фокусируется на визуале и взаимодействии и эмитит события вроде select, retry или approve.
Избегайте зависимостей от нестабильных деталей: путей файлов, структуры DOM родителя или формы объектов состояния. Если такие зависимости нужны, спрячьте их в обёртке, чтобы переименование поля, например user.name → user.profile.name, не вынуждало переписывать виджет.
Тестируйте виджет в изоляции с хардкод‑props, которые соответствуют контракту, включая отсутствующие и неверные значения. Затем регенерируйте и первым делом проверьте: те же ли props передаются и те же ли события обрабатываются страницей.
Если визуальный конструктор не может выразить требуемое поведение, стоит делать кастомный виджет: сложные графики, выбор мест на карте, поля для подписи или drag‑and‑drop‑планировщики — примеры, где кастомный код оправдан. Если задача решается настройкой сгенерированного UI, это обычно проще поддерживать.


