Життя переможе смерть, а світ – темряву.
Подход к организации файловой структуры проектов.
Подходит для JS-проектов любой сложности. Предлагает простую систему терминов и правил, призванных снизить сложность работы над проектами.
- 🚀 подходит для проектов любой сложности
- 🍰 легко понять, простые термины и правила
- 🧩 формирует информативную и предсказуемую файловую структуру проекта, чем облегчает построение его ментальной модели
- 📦 группирует файлы одновременно по реализуемой функциональности и по типу, чем облегчает анализ случаев использования отдельных файлов и поиск связанных файлов
- 📌 обеспечивает фиксированную максимальную вложенность директорий во всём проекте — 3 уровня
- 🚚 разгружает пространство имён файлов, облегчает именование
- Мотивация
- Быстрый старт
- Методология
- FAQ
- Поддержка IDE
- Сообщество
- Обратная связь
- Благодарности
- Поддержать автора
В сфере разработки ПО принято вести работу над ПО в рамках проектов. Под проектом ПО подразумевается набор файлов, содержащих код и другие ресурсы, необходимые для реализации ПО.
По мере развития, проекты становятся сложнее, а вместе с этим становится сложнее работа над ними.
То, как будет расти сложность работы, во многом зависит от подхода к принятию решений, которого придерживается разработчик.
Если принимать решения ситуативно, сложность работы над проектом будет расти стремительно. И напротив, если разработать оптимальный системный подход, рост сложности можно замедлить.
Одна из самых сложных задач, которую приходится выполнять разработчику во время работы, — построение ментальной модели проекта.
Под ментальной моделью подразумевается представление о том, какую задачу решает ПО, какие особенности имеет и как устроен его проект — какой фрагмент кода за что отвечает и как связан с другими фрагментами.
Если говорить про код, упростить построение ментальной модели призваны оптимальные архитектура и соглашения, соотносящиеся с лучшими практиками и индивидуальным опытом.
Но код не может существовать сам по себе. Для того, чтобы стать частью проекта, он должен быть помещён в файл.
Таким образом, построение ментальной модели начинается с изучения файловой структуры проекта.
В файловой структуре имя файла и принадлежность к директории позволяют сделать выводы о содержимом файла и связи с другими файлами.
В случае, если файловая структура организована неудачно (ситуативно, неоптимально), её изучение становится затруднительным. В свою очередь, это затрудняет построение ментальной модели. А поскольку это — одна из самых сложных задач при работе с проектами, это серьёзная проблема.
Среди разработчиков проблема часто оказывается недооценённой, хотя стоит наряду с неудачными архитектурой и соглашениями или их отсутствием.
Можно выделить два популярных подхода к организации файловой структуры:
Подход предусматривает группировку файлов по типу содержимого.
Для примера, файловая структура React-приложения может выглядеть так:
src ├── api ├── assets ├── components ├── constants ├── contexts ├── helpers └── hooks
Файлы с содержимым, относящимся к компонентам, размещаются в components
, к хелперам — в helpers
и т.д.
Подход достаточен в маленьких проектах, но в средних или больших группы могут содержать десятки или сотни файлов, из-за чего возникают проблемы:
components ├── (15 директорий, начинающихся с префикса "app") ├── (10 директорий, начинающихся с префикса "emoji") ├── (20 директорий, начинающихся с префикса "home") ├── (10 директорий, начинающихся с префикса "layout") ├── (30 директорий, начинающихся с префикса "post") ├── (20 директорий, начинающихся с префикса "post_editor") ├── (15 директорий, начинающихся с префикса "trends") └── ...
-
Проблемы с пространством имён
Чем больше файлов содержится в группе, тем выше становится нагрузка на пространство имён. Чтобы избежать коллизий, к именам новых файлов требуется добавлять префиксы, что усложняет имена и сам процесс именования.
-
Проблемы анализа
Чем больше файлов содержится в группе, тем сложнее её анализировать, поскольку длинный несгруппированный список файлов несёт мало информации.
Рассматривая отдельный файл, невозможно быстро получить представление о том, в реализации какой функциональности он участвует и с какими другими файлами может быть связан. Чтобы ответить на эти вопросы, потребуется провести мини-исследование, — и так с каждым файлом.
Подход предусматривает группировку файлов по функциональности, которую реализует их содержимое.
Для примера, файловая структура React-приложения может выглядеть так:
src ├── app ├── home ├── layout ├── posts └── trends
Файлы с содержимым, реализующим домашнюю страницу, размещаются в home
, посты — в posts
и т.д.
Подход решает основную проблему folders by type — вводит группировку по реализуемой функциональности. Группировка значительно упрощает анализ того, в реализации какой функциональности участвует файл и с какими другими файлами может быть связан.
Вместе с этим, подход лишён основного преимущества folders by type — группировки файлов по типу содержимого.
В больших проектах группы могут содержать десятки или сотни файлов, и нехватка дополнительной группировки становится проблемой:
src ├── post │ ├── actions_like.jsx │ ├── actions_like.styles.js │ ├── actions_reply.jsx │ ├── actions_reply.styles.js │ ├── actions_repost.jsx │ ├── actions_repost.styles.js │ ├── actions_share.jsx │ ├── actions_share.styles.js │ ├── content_image.jsx │ ├── content_image.styles.jsx │ ├── content_text.jsx │ ├── content_text.styles.js │ ├── content_video.jsx │ ├── content_video.styles.js │ ├── content.jsx │ ├── content.styles.js │ ├── image_viewer_backdrop.jsx │ ├── image_viewer_backdrop.styles.js │ ├── image_viewer_navigation.jsx │ ├── image_viewer_navigation.styles.js │ ├── image_viewer.jsx │ ├── image_viewer.styles.js │ ├── post.jsx │ ├── post.styles.js │ ├── views.jsx │ └── views.styles.js │ ├── post_editor │ ├── audience.jsx │ ├── audience.styles.js │ ├── editor.jsx │ ├── editor.styles.js │ ├── emoji.jsx │ ├── emoji.styles.js │ ├── media.jsx │ ├── media.styles.js │ ├── poll.jsx │ ├── poll.styles.js │ ├── post_limits.constants.js │ ├── settings.jsx │ ├── settings.styles.js │ ├── text_area.jsx │ └── text_area.styles.js │ └── ...
-
Проблемы анализа
Чем больше файлов содержится в группе, тем сложнее её анализировать, поскольку длинный несгруппированный список файлов несёт мало информации.
Рассматривая группу, невозможно быстро получить представление о том, из чего она состоит: есть ли файлы определённого типа, сколько их и т.д. Чтобы ответить на эти вопросы, потребуется провести мини-исследование, — и так с каждой группой.
Использование вложенности решает некоторые проблемы обоих подходов, но значительно затрудняет анализ проекта, поскольку часть файлов оказывается скрытой — до тех пор, пока не раскрыть все вложенные директории.
Изучим фичу
post
src └── post
Выглядит просто!
src └── post ├── actions ├── content ├── image_viewer ├── post.jsx ├── post.styles.js ├── views.jsx └── views.styles.js
Заглянем во вложенные директории
src └── post ├── actions │ ├── like.jsx │ ├── like.styles.js │ ├── reply.jsx │ ├── reply.styles.js │ ├── repost.jsx │ ├── repost.styles.js │ ├── share.jsx │ └── share.styles.js ├── content │ ├── content.jsx │ ├── content.styles.js │ ├── image.jsx │ ├── image.styles.jsx │ ├── text.jsx │ ├── text.styles.js │ ├── video.jsx │ └── video.styles.js ├── image_viewer │ ├── backdrop.jsx │ ├── backdrop.styles.js │ ├── image_viewer.jsx │ ├── image_viewer.styles.js │ ├── navigation.jsx │ └── navigation.styles.js ├── post.jsx ├── post.styles.js ├── views.jsx └── views.styles.js
Упс! Это займёт какое-то время 😳
⬆️ К содержанию
Файловая структура проекта, разработанная с применением подхода virtual.nesting
, выглядит так:
-
В директории с исходным кодом (по умолчанию
src
) размещаются категории. Они группируют файлы по функциональности, которую реализует их содержимое.Файлы, сгруппированные категорией, образуют функциональную единицу проекта.
В примере ниже
app
,home
,layout
и т.д. — категории.src ├── app │ └── ... ├── home │ └── ... ├── layout │ └── ... ├── post │ └── ... ├── post.image_viewer │ └── ... ├── post@shared │ └── ... ├── post_editor │ └── ... ├── trends │ └── ... └── ui_kit └── ...
В файловой структуре проекта категории образуют одноуровневый список, что достигается за счёт применения виртуальной вложенности — выражения вложенности директорий на уровне их имён.
Это позволяет иметь быстрый доступ к полному списку функциональных единиц проекта, без необходимости раскрытия неопределённого количества вложенных директорий.
Примечание
Категория
post.image_viewer
— пример применения виртуальной вложенности. -
В категориях размещаются группы, они группируют файлы по типу содержимого.
В примере ниже
components
иhelpers
— группы.src └── layout ├── components │ └── ... ├── helpers │ └── ... └── index.js
-
В группах размещаются элементы. Элемент может быть файлом или директорией с файлами.
В примере ниже
scroll.js
— элемент.src └── layout └── helpers └── scroll.js
В примере ниже
navigation
— элемент.src └── layout └── components └── navigation ├── component.jsx ├── index.js └── styles.js
Больше примеров можно найти здесь.
⬆️ К содержанию
Пошаговое руководство 📘
Давайте представим, что мы веб-разработчик и некая компания предложила нам работу над проектом новой социальной сети.
Проект на начальном этапе разработки и нам нужно выступить в роли ведущего разработчика фронтенда.
У нас есть требования, которые подготовил аналитик, и план работ, который составил менеджер.
- Начинаем работу над домашней страницей
- Инициализируем проект
- Реализуем компонент, отвечающий за раскладку элементов страницы
- Реализуем UI-кит
- Начинаем работу над постом
- Реализуем компонент, отвечающий за визуализацию поста
- Реализуем компонент, отвечающий за просмотр изображений
- Организуем доступ к общим ресурсам в разных категориях
- Реорганизуем категорию
- Реализуем редактор постов
- Завершаем работу над домашней страницей
- Интегрируем домашнюю страницу в приложение
Погружаемся в контекст. Первые примеры уже в следующей главе!
Наша первая (и единственная в рамках этого руководства) задача — реализовать домашнюю страницу.
Задача — комплексная и трудозатратная. Чтобы сократить возможные риски, необходимо её декомпозировать.
Сперва отметим, что перед тем, как к ней приступить, нам необходимо инициализировать проект.
Затем проанализируем требования и макеты:
-
Лейаут страницы совпадает с лейаутом других страниц. Под лейаутом подразумевается раскладка элементов, а именно: хедера, навигации, контента, футера и т.д.
Исходя из этого, необходимо реализовать переиспользуемый компонент, отвечающий за раскладку элементов страницы. Его можно будет использовать для реализации текущей страницы и следующих.
-
На странице используются элементы, входящие в UI-кит. Необходимо спроектировать и подготовить минимальную реализацию UI-кита, которой будет достаточно для реализации страницы.
-
На странице используются элементы, которые также будут использоваться на других страницах: посты и редактор постов. Необходимо реализовать соответствующие переиспользуемые компоненты.
Учитывая вышеперечисленные замечания, составим следующий список задач:
-
инициализировать проект;
-
реализовать переиспользуемый компонент, отвечающий за раскладку элементов странице;
-
спроектировать и подготовить минимальную реализацию UI-кита;
-
реализовать переиспользуемые компоненты для визуализации постов и редактора постов;
-
реализовать страницу с использованием компонентов, разработанных ранее.
Приступим! 👩🔧
Здесь мы создаём категорию, группу и элемент, знакомимся с их свойствами и ограничениями.
Первая задача в нашем списке — инициализировать проект. Задача включает в себя создание скелета и настройку сборки приложения.
Начнём с создания скелета. Он будет состоять из двух элементов:
root
– в нём будет содержаться корневой компонент приложения,init.js
— в нём будет происходить инициализация приложения.
Сперва реализуем элемент root
. Для этого:
-
создадим категорию
app
,Категория — директория, применяемая для группировки элементов по реализуемой функциональности.
— методология, раздел категории
-
в ней создадим группу
components
,Группа — директория, применяемая для группировки элементов по типу.
— методология, раздел группы
-
в ней создадим элемент
root
.Элемент — файл или директория с файлами.
— методология, раздел элементы
Обусловимся, что элементы группы components
будут директориями, поскольку чаще всего элементы этого типа являются комплексными и требуют декомпозиции на несколько модулей.
В директории элемента root
создадим два модуля:
-
component.jsx
— в нём разместим код компонента, -
index.js
— в нём реэкспортируем компонент из модуляcomponent.jsx
Почему?
Если элемент является директорией, внешний доступ к его внутренним ресурсам разрешён только через индексный модуль (
index.js
).Это ограничение обеспечивает сокрытие внутренних ресурсов элемента и формирует явные точки входа.
— методология, раздел ограничения элементов
В результате, получим следующую структуру:
src └── app └── components └── root ├── component.jsx └── index.js
Элемент root
готов!
Вслед за ним займёмся реализацией элемента init.js
. Он тоже относится к категории app
, но к группе init
.
src └── app ├── components │ └── root │ ├── component.jsx │ └── index.js └── init └── init.js
Элемент init.js
готов.
А с ним и скелет приложения!
Перейдём к сборке приложения.
Для этого необходимо указать путь к модулю с кодом инициализации init.js
в конфигурации сборщика. Нужный модуль расположен по пути src/app/init/init.js
.
Cоздадим в корне категории app
модуль init.js
, в котором импортируем модуль init/init.js
.
Почему?
Внешний доступ к внутренним ресурсам категорий разрешён только через модули, размещённые в корне категории.
Это ограничение обеспечивает сокрытие внутренних ресурсов категории и формирует явные точки входа.
— методология, раздел ограничения категорий
src └── app ├── components │ └── root │ ├── component.jsx │ └── index.js ├── init │ └── init.js └── init.js
Затем укажем путь к модулю src/app/init.js
в конфигурации сборщика.
Дело сделано! 🎉
Здесь мы создаём категорию и точку входа в неё.
Наша следующая задача — реализовать переиспользуемый компонент, отвечающий за раскладку элементов страницы.
Реализация компонента включает в себя реализацию нескольких дочерних компонентов и хелперов.
Создадим новую категорию — layout
. Разместим в ней соответствующие группы и элементы.
В результате, получим следующую структуру:
src └── layout └── components ├── about │ ├── component.jsx │ ├── index.js │ └── styles.js ├── layout │ ├── component.jsx │ ├── index.js │ └── styles.js ├── logo │ ├── component.jsx │ ├── index.js │ └── styles.js ├── navigation │ ├── component.jsx │ ├── index.js │ └── styles.js └── profile ├── component.jsx ├── index.js └── styles.js
Перейдём к созданию точки входа в категорию.
Создадим в корне категории индексный модуль (index.js
) и реэкспортируем в нём компонент Layout
из ./components/layout
.
Почему?
Имя категории (
layout
) и её экспортируемого элемента (layout
) совпадают. В этом случае предпочтительнее реэкспортировать элемент в индексном модуле (index.js
).Это позволит избежать дублирования имени при обращении к ресурсу.
// 🟡 имя `layout` дублируется import Layout from '../../../layout/layout'; // 🟢 ok import Layout from '../../../layout';
src └── layout ├── components │ ├── about │ │ ├── component.jsx │ │ ├── index.js │ │ └── styles.js │ ├── layout │ │ ├── component.jsx │ │ ├── index.js │ │ └── styles.js │ ├── logo │ │ ├── component.jsx │ │ ├── index.js │ │ └── styles.js │ ├── navigation │ │ ├── component.jsx │ │ ├── index.js │ │ └── styles.js │ └── profile │ ├── component.jsx │ ├── index.js │ └── styles.js └── index.js
Теперь к компоненту Layout
можно обратиться в других категориях:
import Layout from '../../../layout';
Дело сделано! 🎉
Здесь мы создаём категорию и несколько точек входа в неё.
Наша следующая задача — спроектировать и подготовить минимальную реализацию UI-кита.
Создадим новую категорию — ui_kit
. Разместим в ней соответствующие группы и элементы.
В результате, получим следующую структуру:
src └── ui_kit └── components ├── button │ ├── component.jsx │ ├── index.js │ └── styles.js ├── link │ ├── component.jsx │ ├── index.js │ └── styles.js ├── popover │ ├── component.jsx │ ├── index.js │ └── styles.js └── profile_pic ├── component.jsx ├── index.js └── styles.js
Перейдём к созданию точек входа в категорию.
Создадим в корне категории модули button.js
, link.js
, popover.js
, profile_pic.js
и реэкспортируем в них соответствующие компоненты.
src └── ui_kit ├── components │ ├── button │ │ ├── component.jsx │ │ ├── index.js │ │ └── styles.js │ ├── link │ │ ├── component.jsx │ │ ├── index.js │ │ └── styles.js │ ├── popover │ │ ├── component.jsx │ │ ├── index.js │ │ └── styles.js │ └── profile_pic │ ├── component.jsx │ ├── index.js │ └── styles.js ├── button.js ├── link.js ├── popover.js └── profile_pic.js
Теперь к компонентам можно обратиться в других категориях:
import ProfilePic from '../../../ui_kit/profile_pic';
Дело сделано! 🎉
Наша следующая задача — реализовать переиспользуемый компонент, отвечающий за визуализацию поста.
Исходя из требований и макетов следует, что:
-
пост может содержать текст, а также прикреплённые картинки или видео — и для их просмотра требуются соответствующие просмотрщики;
-
у поста есть счётчик просмотров, также посту можно поставить лайк, сделать репост, написать комментарий;
-
у поста есть меню действий, в котором можно подписаться на автора или заблокировать его, а так же пожаловаться на контент.
Как мы видим, полноценная реализация поста — задача комплексная и трудозатратная.
Поэтому будет разумно разбить его реализацию на итерации, сейчас же сосредоточиться только на следующих задачах:
- реализовать основную визуализацию поста,
- реализовать просмотрщик изображений.
Начнём с компонента, отвечающего за визуализацию поста.
Создадим новую категорию — post
. Разместим в ней соответствующие группы и элементы, а также создадим точку входа.
Примечание
Здесь и далее примеры файловой структуры будут упрощены
src └── post ├── components │ ├── actions │ ├── block │ ├── content │ ├── follow │ ├── image │ ├── like │ ├── menu │ ├── post │ ├── reply │ ├── report │ ├── repost │ ├── share │ ├── text │ └── views ├── contexts │ └── post.js └── index.js
Здесь мы создаём подкатегорию, знакомимся с её свойствами и ограничениями.
Перейдём к компоненту, отвечающему за просмотр прикреплённых к посту изображений.
Перед нами возникает вопрос — где разместить его код? Мы можем предположить, что:
- реализация просмотрщика потребует создания большого объёма файлов;
- просмотрщик не является самостоятельной функциональностью и всегда будет использоваться в паре с постом, будет полезно указать связь между ними;
- код просмотрщика не требует тесной связи с кодом поста, следовательно, код необходимо разграничить.
Исходя из этого, рассмотрим доступные варианты:
-
Разместить код просмотрщика в той же категории —
post
.Однако, поскольку его реализация потребует создания большого объёма файлов, смешение файлов поста и просмотрщика:
- затруднит анализ категории,
- создаст дополнительную нагрузку на пространство имён.
-
Разместить код просмотрщика в новой категории —
image_viewer
.Это облегчит анализ категории и создаст новое пространство имён.
Однако, в таком случае не хватает указания связи между постом и просмотрщиком.
-
Разместить код просмотрщика в подкатегории —
post.image_viewer
.Это облегчит анализ категории и создаст новое пространство имён, а также укажет на связь между категориями
post
иimage_viewer
.Отличный вариант! 💯
Создадим подкатегорию — post.image_viewer
.
Подкатегория — подвид категории, применяемый для декомпозиции комплексных категорий. Связана с другими категориями отношениями родитель-потомок.
Связь с родителями выражается через виртуальную вложенность — на уровне имени директории, перечислением списка родителей через точку.
— методология, раздел подкатегории
Разместим в ней необходимые группы и элементы, а также создадим точку входа.
src ├── post │ ├── components │ │ ├── actions │ │ ├── block │ │ ├── content │ │ ├── follow │ │ ├── image │ │ ├── like │ │ ├── menu │ │ ├── post │ │ ├── reply │ │ ├── report │ │ ├── repost │ │ ├── share │ │ ├── text │ │ └── views │ ├── contexts │ │ └── post.js │ └── index.js │ └── post.image_viewer ├── components │ ├── actions │ ├── navigation │ └── image_viewer └── index.js
В процессе реализации мы обращаем внимание на то, что в просмотрщике необходимо использовать ресурсы, уже реализованные в категории post
:
components/like
,components/repost
,components/share
,contexts/post.js
.
Но просто взять и обратиться к нужным компонентам из категории post
мы не можем.
Почему?
В подкатегориях запрещён доступ к ресурсам своих родителей.
Это ограничение структурирует связи между родителями и потомками и позволяет избежать циклических зависимостей.
— методология, раздел ограничения категорий
Что же делать? 😳
Здесь мы создаём мета-категорию, знакомимся с её свойствами и ограничениями.
В таком случае, необходимо разместить файлы в мета-категории @shared
.
Мета-категория — подвид категории. Отличается от обычной категории тем, что:
доступ к ресурсам мета-категории разрешён в базовой категории (без мета-тега) и всех её подкатегориях — и только в них,
обратный доступ, в мета-категории к ресурсам базовой категории и её подкатегорий, — запрещён,
мета-категория может переопределять ограничения обычных категорий.
Имя мета-категории имеет структуру:
<базовая категория?>@<мета-тег>
.Мета-категория
@shared
применяется для размещения элементов, к которым необходимо иметь доступ одновременно в категории и всех её подкатегориях.В ней не действует требование о создании точек входа для доступа к внутренним ресурсам (можно обращаться напрямую).
— методология, раздел мета-категории
Создадим мета-категорию — post@shared
и перенесём в неё необходимые элементы из категории post
:
src ├── post │ ├── components │ │ ├── actions │ │ ├── block │ │ ├── content │ │ ├── follow │ │ ├── image │ │ ├── menu │ │ ├── post │ │ ├── report │ │ ├── text │ │ └── views │ └── index.js │ ├── post.image_viewer │ ├── components │ │ ├── actions │ │ ├── image_viewer │ │ └── navigation │ └── index.js │ └── post@shared ├── components │ ├── like │ ├── reply │ ├── repost │ └── share └── contexts └── post.js
Теперь к нужным компонентам можно обратиться как в категории post
, так и в post.image_viewer
:
import Like from '../../../post@shared/components/like'
Примечание
Обратите внимание, в мета-категории
@shared
не действует требование о создании точек входа для доступа к внутренним ресурсам. К ним можно обращаться напрямую!
Это позволяет завершить реализацию просмотрщика.
Дело сделано! 🎉
Здесь мы создаём именованную группу, знакомимся с её свойствами и ограничениями.
Вернёмся к категории post
.
src └── post ├── components │ ├── actions │ ├── block │ ├── content │ ├── follow │ ├── image │ ├── menu │ ├── post │ ├── report │ ├── text │ └── views └── index.js
Давайте проанализируем группу components
.
В ней размещено довольно много файлов. Если провести мини-исследование, среди них можно выделить две подгруппы:
- файлы, связанные с реализацией поста,
- файлы, связанные с реализацией меню действий.
Отдельное мини-исследование — относительно лёгкая задача. Однако, чем больше их требуется, тем сложнее становится анализ проекта. В какой-то момент в проекте невозможно будет разобраться без блокнота и ручки.
Таким образом, лучше всего свести необходимость мини-исследований в проекте к минимуму.
В случае с файлами, которые мы рассматривали выше, будет полезно закрепить результаты мини-исследования и применить к файлам дополнительную группировку.
Перед нами возникает вопрос — как будет лучше это сделать? Рассмотрим доступные варианты:
-
Разместить файлы в новых подкатегориях.
Однако, подкатегории уместнее применять для группировки набора файлов с содержимым разного типа. В нашем же случае, файлы одного типа —
components
.Также это потребует соблюдения всех накладываемых на категории ограничений.
-
Разместить файлы в именованных группах.
Это позволит применить дополнительную группировку без создания новых категорий.
Отличный вариант! 💯
Создадим именованные группы components#content
и components#menu
.
Именованная группа — подвид группы, применяемый для дополнительной группировки элементов.
Имя именованной группы имеет структуру:
<базовая группа>#<имя>
.
Перенесём в них соответствующие элементы.
src └── post ├── components │ ├── actions │ └── post ├── components#content │ ├── content │ ├── image │ ├── text │ └── views ├── components#menu │ ├── block │ ├── follow │ ├── menu │ └── report └── index.js
Таким образом, анализ категории post
и группы components
больше не требует проведения мини-исследования.
Стоит отметить, что в именованных группах действуют некоторые ограничения:
В именованных группах запрещён доступ к ресурсам своей базовой группы.
Это ограничение структурирует связи между группами и позволяет избежать циклических зависимостей.
Если к ресурсу необходимо иметь доступ одновременно в базовой группе и всех именованных группах на её основе, необходимо разместить его в мета-группе
@shared
.— методология, раздел ограничения групп
Дело сделано! 🎉
Наша следующая задача — реализовать редактор постов.
Для этого создадим новую категорию — post_editor
. Разместим в ней соответствующие группы и элементы, а также создадим точку входа.
В редакторе постов есть пикер emoji, который будет использоваться и в других интерфейсах. Выделим его в отдельную категорию — emoji
.
src
├── emoji
│ ├── components
│ │ └── picker
│ ├── constants
│ │ └── emoji_list.js
│ └── picker.js
|
└── post_editor
├── components
│ ├── audience
│ ├── editor
│ ├── emoji
│ ├── media
│ ├── poll
│ ├── settings
│ └── text_area
├── contexts
│ └── post_editor.js
└── index.js
Дело сделано! 🎉
Наша следующая задача — реализовать домашнюю страницу.
Мы проделали большую работу и подготовили её основные компоненты: Layout
, Post
и PostEditor
. Перейдём к реализации самой страницы.
Для этого:
-
используя внешние компоненты
Post
иPostEditor
, подготовленные ранее, реализуем комплексные компонентыPostFeed
иPostEditor
, отвечающие за полноценную реализацию и интеграцию ленты и редактора постов; -
реализуем компонент
Home
, отвечающий за визуализацию страницы.
Создадим новую категорию — home
. Разместим в ней соответствующие группы и элементы, а также создадим точку входа.
src
└── home
├── components
│ ├── home
│ ├── post_editor
│ └── post_feed
└── index.js
Приближаемся к финишу! 🏁
Наша последняя задача — интегрировать страницу в приложение.
Для этого понадобится обновить код компонента Root
из категории app
: обратиться в нём к компоненту Home
из категории home
и вызвать его.
import Home from '../../../home'
const Root = () => {
return (
<Home />
)
}
export default Root
С этого момента, в нашем приложении доступна домашняя страница.
Задача выполнена! 🎉
Поздравляю, вы изучили подход virtual.nesting
на 95% и можете начать применять его в своих проектах!
Также вы можете:
- ознакомиться с методологией, чтобы структурировать и уточнить свои знания;
- присоединиться к сообществу.
Примеры проектов, в которых virtual.nesting
используется для организации файловой структуры, можно найти на здесь.
⬆️ К содержанию
Категория — директория, применяемая для группировки элементов по реализуемой функциональности.
Файлы, сгруппированные категорией, образуют функциональную единицу проекта.
Создаёт новое пространство имён.
В примере ниже
layout
— категорияsrc └── layout ├── components │ └── ... ├── helpers │ └── ... └── index.js
Категории размещаются в директории с исходным кодом (по умолчанию src
), и только в ней.
Подкатегория — подвид категории, применяемый для декомпозиции комплексных категорий. Связана с другими категориями отношениями родитель-потомок.
Связь с родителями выражается через виртуальную вложенность — на уровне имени директории, перечислением списка родителей через точку.
В подкатегориях действует дополнительное ограничение на доступ к ресурсам.
В примере ниже
post.image_viewer
— подкатегорияsrc ├── post │ ├── components │ │ └── ... │ ├── components#content │ │ └── ... │ └── index.js │ └── post.image_viewer ├── components │ └── ... └── index.js
-
Внешний доступ к внутренним ресурсам категорий разрешён только через модули, размещённые в корне категории.
Почему?
Это ограничение обеспечивает сокрытие внутренних ресурсов категории и формирует явные точки входа.
Пример
src/**post_editor**/components/emoji/component.jsx
:// ✅ ok import { Picker } from '../../../emoji'; // ✅ ok import Picker from '../../../emoji/picker'; // ❌ ошибка, прямой доступ к внутренним ресурсам категории запрещён import Picker from '../../../emoji/components/picker';
-
В подкатегориях запрещён доступ к ресурсам своих родителей.
Почему?
Это ограничение структурирует связи между родителями и потомками и позволяет избежать циклических зависимостей.
Примечание
Если к ресурсу необходимо иметь доступ одновременно в категории и всех её подкатегориях, необходимо разместить его в мета-категории @shared.
Пример
src/**post**/components/post/component.jsx
:// ✅ ok import ImageViewer from '../../../post.image_viewer';
src/**post.image_viewer**/components/viewer/component.jsx
:// ❌ ошибка, доступ к ресурсам родительской категории запрещён import Like from '../../../post/like'; // ✅ ok import Like from '../../../post@shared/components#actions/like';
Мета-категория — подвид категории. Отличается от обычной категории тем, что:
-
доступ к ресурсам мета-категории разрешён в базовой категории (без мета-тега) и всех её подкатегориях — и только в них,
-
обратный доступ, в мета-категории к ресурсам базовой категории и её подкатегорий, — запрещён,
-
мета-категория может переопределять ограничения обычных категорий.
Имя мета-категории имеет структуру: <базовая категория?>@<мета-тег>
.
Примечание
Если базовая категория не указана, доступ к ресурсам мета-категории разрешён во всём проекте.
Доступные мета-категории:
-
@shared
Применяется для размещения элементов, к которым необходимо иметь доступ одновременно в категории и всех её подкатегориях.
Переопределяет ограничения:
- не действует требование о создании точек входа для доступа к внутренним ресурсам (можно обращаться напрямую).
Примечание
Доступ к ресурсам мета-категории
@shared
(без указания базовой категории) разрешён во всём проекте.Пример
src/**post**/components/post/component.jsx
:// ✅ ok import Share from '../../../post@shared/components/share';
src/**post@shared**/components/share/component.jsx
:// ❌ ошибка, доступ к ресурсам базовой категории запрещён import shareOptions from '../../../post/constants/share_options';
src/**home**/components/post_feed/component.jsx
:// ❌ ошибка, доступ к ресурсам мета-категории `post@shared` разрешён только в её базовой категории и подкатегориях import Share from '../../../post@shared/components/share';
⬆️ К содержанию
Группа — директория, применяемая для группировки элементов по типу.
Создаёт новое пространство имён.
В примере ниже
components
— группаsrc └── layout └── components ├── about │ └── ... ├── layout │ └── ... ├── logo │ └── ... ├── navigation │ └── ... └── profile └── ...
Группы размещаются внутри категорий, и только в них.
Именованная группа — подвид группы, применяемый для дополнительной группировки элементов.
Имя именованной группы имеет структуру: <базовая группа>#<имя>
.
В именованных группах действует дополнительное ограничение на доступ к ресурсам.
В примере ниже
components#content
— именованная группаsrc └── post ├── components │ └── ... └── components#content └── ...
-
В именованных группах запрещён доступ к ресурсам своей базовой группы.
Почему?
Это ограничение структурирует связи между группами и позволяет избежать циклических зависимостей.
Примечание
Если к ресурсу необходимо иметь доступ одновременно в базовой группе и всех именованных группах на её основе, необходимо разместить его в мета-группе @shared.
Пример
src/post/**components**/post/component.jsx
:// ✅ ok import Content from '../components#content/content';
src/post/**components#content**/text/component.jsx
:// ❌ ошибка, доступ к ресурсам базовой группы запрещён import ProfilePopover from '../components/profile_popover'; // ✅ ok import ProfilePopover from '../components@shared/profile_popover';
Мета-группа — подвид группы. Отличается от обычной группы тем, что:
-
доступ к ресурсам мета-группы разрешён в базовой группе (без мета-тега) и всех именованных группах на её основе — и только в них,
-
обратный доступ, в мета-группе к ресурсам базовой группы и именованных групп на её основе, — запрещён,
-
мета-группа может переопределять ограничения обычных групп.
Имя мета-группы имеет структуру: <базовая группа>@<мета-тег>
.
Доступные мета-группы:
-
@shared
Применяется для размещения ресурсов, к которым необходимо иметь доступ одновременно в группе и всех именованных группах на её основе.
Пример
src/post/**helpers**/playback.js
:// ✅ ok import easings from '../../helpers@shared/easings';
src/post/**helpers@shared**/easings.js
:// ❌ ошибка, доступ к ресурсам базовой категории запрещён import math from '../../helpers/math';
src/post/**components**/audio/component.jsx
:// ❌ ошибка, доступ к ресурсам мета-группы `helpers@shared` разрешён только в её базовой группе и именованных группах на её основе import easings from '../../helpers@shared/easings';
⬆️ К содержанию
Элемент — файл или директория с файлами.
В примере ниже
scroll.js
— элементsrc └── layout └── helpers └── scroll.js
В примере ниже
navigation
— элементsrc └── layout └── components └── navigation ├── component.jsx ├── index.js └── styles.js
Элементы размещаются внутри групп, и только в них.
-
Если элемент является директорией, внешний доступ к его внутренним ресурсам разрешён только через индексный модуль (
index.js
).Почему?
Это ограничение обеспечивает сокрытие внутренних ресурсов элемента и формирует явные точки входа.
Пример
src/post_editor/components/**editor**/component.jsx
:// ✅ ok import Media from '../media'; // ❌ ошибка, прямой доступ к внутренним ресурсам элемента запрещён import mediaStyles from '../media/styles';
Если элемент является директорией, в нём можно использовать виртуальную вложенность, чтобы выразить связь файлов или организовать виртуальную директорию.
В примере ниже
файлstyles.actions.js
связан с файломstyles.js
src └── post_editor └── components └── editor ├── component.jsx ├── index.js ├── styles.js └── styles.actions.js
В примере ниже
организована виртуальная директорияassets
src └── post_editor └── components └── editor ├── assets.emoji_icon.svg ├── assets.media_icon.svg ├── assets.poll_icon.svg ├── component.jsx ├── index.js └── styles.js
⬆️ К содержанию
Ресурс — любое значение, которое участвует в экспорте/импорте.
В примере ниже
Page
,circleArea
иmeaningOfLife
— ресурсы// .../components/page/component.jsx export const Page = () => { return <h1>Hello, world!</h1>; }; // .../helpers/circle_area.js export const circleArea = (r) => { return Math.PI * r**2; }; // .../questions/meaning_of_life.js export const meaningOfLife = () => { return wait('7.5m years').then(() => 42); };
⬆️ К содержанию
- Используйте
snake_case
для именования файлов и директорий. - Используйте относительные пути для обращения к файлам в пределах проекта, не используйте сокращения путей.
- Избегайте циклических зависимостей.
⬆️ К содержанию
Подходы к организации файловой структуры и архитектурные шаблоны имеют общую цель — упростить построение метальной модели проекта.
Если подход к организации файловой структуры и архитектурный шаблон совместимы, их совместное применение может быть более эффективным в достижении этой цели.
Из этого следует, что вы можете применить в проекте virtual.nesting
и получить дополнительные преимущества, если выбранный вами архитектурный шаблон не имеет требований к организации файловой структуры проекта или эти требования совместимы с принципами virtual.nesting
.
Если требования архитектуры не позволяют вам применить в проекте virtual.nesting
в полной мере, вы также можете рассмотреть возможности применить отдельные идеи подхода.
Да!
Да!
Полагаю, что да! Если у вас получится интерпретировать подход в проекте на другом языке, пожалуйста, поделитесь опытом.
По умолчанию для сортировки списка файлов используется алгоритм, приводящий в некоторых случаях к перемешиванию групп директорий:
В примере ниже категории
post_editor
перемешались с категориямиpost
.src ├── post ├── post_editor ├── post_editor.preview ├── post.image-viewer ├── post.video-viewer └── post@shared
Это поведение можно изменить, указав другой алгоритм сортировки в настройках:
settings.json
{ "explorer.sortOrderLexicographicOptions": "unicode" }
После этого список файлов будет сортироваться корректно.
src ├── post ├── post.image-viewer ├── post.video-viewer ├── post@shared ├── post_editor └── post_editor.preview
Нет известных проблем.
@virtual_nesting_community — сообщество в Telegram, где можно пообщаться и получить помощь.
Буду рад любой обратной связи! Пожалуйста, пишите в issues или discussions.
Хочу поблагодарить мою жену @daryabratova, а также моих друзей @kindaro, @ruzovska и @a1ex-kaufmann.
Я потратил сотни часов, работая над этим проектом. Надеюсь, он сэкономит тысячи часов времени другим разработчикам.