Архитектура
Вопрос
Как разложить папки в крупном Vue-проекте и что отвечать про Feature-Sliced Design?
Короткий ответ
Раскладка только по типу файла (components/, views/, composables/) на маленьком проекте читается, на большом превращается в свалку: фича «корзина» размазана по пяти корням.
Для масштаба берут модули по предметной области или Feature-Sliced Design. Слои FSD сверху вниз:
| Слой | Что лежит |
|---|---|
app | роутер, провайдеры, глобальные стили |
pages | страницы маршрутов |
widgets | крупные блоки страницы: шапка, колонка фильтров |
features | действие пользователя: вход по почте, добавить в корзину |
entities | сущности: user, product, order |
shared | UI-кит, утилиты, клиент 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 фичу — нет.