Skip to Content
VueАрхитектура

Архитектура

Уровень: middle.

Вопрос

Как разложить папки в крупном Vue-проекте и что отвечать про Feature-Sliced Design?

Короткий ответ

Раскладка только по типу файла (components/, views/, composables/) на маленьком проекте читается, на большом превращается в свалку: фича «корзина» размазана по пяти корням.

Для масштаба берут модули по предметной области или Feature-Sliced Design. Слои FSD сверху вниз:

СлойЧто лежит
appроутер, провайдеры, глобальные стили
pagesстраницы маршрутов
widgetsкрупные блоки страницы: шапка, колонка фильтров
featuresдействие пользователя: вход по почте, добавить в корзину
entitiesсущности: user, product, order
sharedUI-кит, утилиты, клиент API

Импорт идёт только вниз. features может брать entities и shared. shared не импортирует страницу и фичу. Так не появляется цикл «кнопка знает про заказ, заказ знает про кнопку».

Алиас @/ в Vite указывает на src, чтобы не считать ../../. Переменные окружения для клиента — только с префиксом, который Vite отдаёт в бандл (VITE_). Секрет без префикса в клиентский код не попадает, но и в браузере его нет: это не прятка.

Разбор

Слабый ответ на собеседовании: «у нас папка компонентов и папка страниц». Нормальный ответ называет границу фичи. Экран заказа собирает виджеты, виджет вызывает фичу, фича меняет сущность через стор или API, shared не знает слово «заказ».

Pinia-стор сущности живёт рядом с ней, не в общей свалке stores/. Плагин персиста и $subscribe — подробности хранилища, они не диктуют слои.

Composable фичи можно переиспользовать на двух страницах. Если его начинает импортировать shared/ui, границу перепутали: кнопка не должна знать про корзину.

VueUse закрывает повторяющиеся браузерные задачи (useDebounceFn, useLocalStorage, useIntersectionObserver). Свои копии пишут, когда поведение расходится с библиотекой, не «чтобы было своё».

На что смотрят на senior

FSD — договор о зависимостях, не религия имён папок. Команде из четырёх человек полный набор слоёв иногда тяжелее, чем три: app, modules/<domain>, shared. Важно уметь сказать, какой импорт запрещён и почему.

Публичный вход модуля (баррель index.ts) прячет внутренние файлы. Страница импортирует фичу из корня модуля, не из features/cart/model/store.ts соседа. Иначе рефакторинг внутри фичи ломает весь проект.

Общий UI-кит не импортирует роутер и Pinia. Как только кнопка тянет стор, её нельзя положить в другую зону и тяжело тестировать.

Типичные ошибки

  • Называть FSD и тут же импортировать страницу из сущности.
  • Класть бизнес-правило скидки в shared, потому что функцию захотели переиспользовать.
  • Путать алиас сборки с структурой слоёв: @/features/... не отменяет правило направления.
  • Отдавать в клиент ключ API через переменную без понимания, что бандл её содержит.

Задача

Кнопка из shared/ui импортирует стор корзины и сама вызывает addToCart. Какое правило слоёв это ломает и куда перенести вызов?

Показать ответ

shared не знает про фичу и сущность. Иначе кнопку нельзя переиспользовать и тестировать без корзины.

Вызов стора остаётся в features/add-to-cart или на странице. Кнопка принимает обычный onClick. Импорт сверху вниз: страница и фича могут взять shared, shared фичу — нет.

Last updated on