virtual-nesting icon indicating copy to clipboard operation
virtual-nesting copied to clipboard

Подход к организации файловой структуры проектов

Життя переможе смерть, а світ – темряву.

virtual.nesting 💊

Подход к организации файловой структуры проектов.

Подходит для JS-проектов любой сложности. Предлагает простую систему терминов и правил, призванных снизить сложность работы над проектами.

Преимущества

  • 🚀 подходит для проектов любой сложности
  • 🍰 легко понять, простые термины и правила
  • 🧩 формирует информативную и предсказуемую файловую структуру проекта, чем облегчает построение его ментальной модели
  • 📦 группирует файлы одновременно по реализуемой функциональности и по типу, чем облегчает анализ случаев использования отдельных файлов и поиск связанных файлов
  • 📌 обеспечивает фиксированную максимальную вложенность директорий во всём проекте — 3 уровня
  • 🚚 разгружает пространство имён файлов, облегчает именование

Содержание

  1. Мотивация
    • Неудачно-организованная файловая структура проекта — это серьёзная проблема
    • У текущих подходов есть существенные недостатки
      • Folders by type
      • Folders by feature
    • Вложенность решает проблемы, но создаёт новые
  2. Быстрый старт
    • Краткий обзор
    • Применяем шаг за шагом
    • Примеры проектов
  3. Методология
    • Категория
      • Размещение категорий
      • Подкатегории
      • Ограничения категорий
      • Мета-категории
    • Группа
      • Размещение групп
      • Именованные группы
      • Ограничения групп
      • Мета-группы
    • Элемент
      • Размещение элементов
      • Варианты использования виртуальной вложенности в элементах
      • Ограничения элементов
    • Ресурс
    • Рекомендации
    • Совместимость с архитектурными шаблонами
  4. FAQ
  5. Поддержка IDE
  6. Сообщество
  7. Обратная связь
  8. Благодарности
  9. Поддержать автора

Мотивация

Неудачно-организованная файловая структура проекта — это серьёзная проблема

В сфере разработки ПО принято вести работу над ПО в рамках проектов. Под проектом ПО подразумевается набор файлов, содержащих код и другие ресурсы, необходимые для реализации ПО.

По мере развития, проекты становятся сложнее, а вместе с этим становится сложнее работа над ними.

То, как будет расти сложность работы, во многом зависит от подхода к принятию решений, которого придерживается разработчик.

Если принимать решения ситуативно, сложность работы над проектом будет расти стремительно. И напротив, если разработать оптимальный системный подход, рост сложности можно замедлить.

Одна из самых сложных задач, которую приходится выполнять разработчику во время работы, — построение ментальной модели проекта.

Под ментальной моделью подразумевается представление о том, какую задачу решает ПО, какие особенности имеет и как устроен его проект — какой фрагмент кода за что отвечает и как связан с другими фрагментами.

Если говорить про код, упростить построение ментальной модели призваны оптимальные архитектура и соглашения, соотносящиеся с лучшими практиками и индивидуальным опытом.

Но код не может существовать сам по себе. Для того, чтобы стать частью проекта, он должен быть помещён в файл.

Таким образом, построение ментальной модели начинается с изучения файловой структуры проекта.

В файловой структуре имя файла и принадлежность к директории позволяют сделать выводы о содержимом файла и связи с другими файлами.

В случае, если файловая структура организована неудачно (ситуативно, неоптимально), её изучение становится затруднительным. В свою очередь, это затрудняет построение ментальной модели. А поскольку это — одна из самых сложных задач при работе с проектами, это серьёзная проблема.

Среди разработчиков проблема часто оказывается недооценённой, хотя стоит наряду с неудачными архитектурой и соглашениями или их отсутствием.

У текущих подходов есть существенные недостатки

Можно выделить два популярных подхода к организации файловой структуры:

  • folders by type
  • folders by feature

Folders by type

Подход предусматривает группировку файлов по типу содержимого.

Для примера, файловая структура 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")
└── ...
  • Проблемы с пространством имён

    Чем больше файлов содержится в группе, тем выше становится нагрузка на пространство имён. Чтобы избежать коллизий, к именам новых файлов требуется добавлять префиксы, что усложняет имена и сам процесс именования.

  • Проблемы анализа

    Чем больше файлов содержится в группе, тем сложнее её анализировать, поскольку длинный несгруппированный список файлов несёт мало информации.

    Рассматривая отдельный файл, невозможно быстро получить представление о том, в реализации какой функциональности он участвует и с какими другими файлами может быть связан. Чтобы ответить на эти вопросы, потребуется провести мини-исследование, — и так с каждым файлом.

Folders by feature

Подход предусматривает группировку файлов по функциональности, которую реализует их содержимое.

Для примера, файловая структура 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
    

Больше примеров можно найти здесь.

⬆️ К содержанию

Применяем шаг за шагом

Пошаговое руководство 📘

Давайте представим, что мы веб-разработчик и некая компания предложила нам работу над проектом новой социальной сети.

Проект на начальном этапе разработки и нам нужно выступить в роли ведущего разработчика фронтенда.

У нас есть требования, которые подготовил аналитик, и план работ, который составил менеджер.

Содержание

  1. Начинаем работу над домашней страницей
  2. Инициализируем проект
  3. Реализуем компонент, отвечающий за раскладку элементов страницы
  4. Реализуем UI-кит
  5. Начинаем работу над постом
  6. Реализуем компонент, отвечающий за визуализацию поста
  7. Реализуем компонент, отвечающий за просмотр изображений
  8. Организуем доступ к общим ресурсам в разных категориях
  9. Реорганизуем категорию
  10. Реализуем редактор постов
  11. Завершаем работу над домашней страницей
  12. Интегрируем домашнюю страницу в приложение

Начинаем работу над домашней страницей

Погружаемся в контекст. Первые примеры уже в следующей главе!

Наша первая (и единственная в рамках этого руководства) задача — реализовать домашнюю страницу.

Задача — комплексная и трудозатратная. Чтобы сократить возможные риски, необходимо её декомпозировать.

Сперва отметим, что перед тем, как к ней приступить, нам необходимо инициализировать проект.

Затем проанализируем требования и макеты:

  1. Лейаут страницы совпадает с лейаутом других страниц. Под лейаутом подразумевается раскладка элементов, а именно: хедера, навигации, контента, футера и т.д.

    Исходя из этого, необходимо реализовать переиспользуемый компонент, отвечающий за раскладку элементов страницы. Его можно будет использовать для реализации текущей страницы и следующих.

  2. На странице используются элементы, входящие в UI-кит. Необходимо спроектировать и подготовить минимальную реализацию UI-кита, которой будет достаточно для реализации страницы.

  3. На странице используются элементы, которые также будут использоваться на других страницах: посты и редактор постов. Необходимо реализовать соответствующие переиспользуемые компоненты.

Учитывая вышеперечисленные замечания, составим следующий список задач:

  1. инициализировать проект;

  2. реализовать переиспользуемый компонент, отвечающий за раскладку элементов странице;

  3. спроектировать и подготовить минимальную реализацию UI-кита;

  4. реализовать переиспользуемые компоненты для визуализации постов и редактора постов;

  5. реализовать страницу с использованием компонентов, разработанных ранее.

Приступим! 👩‍🔧

Инициализируем проект

Здесь мы создаём категорию, группу и элемент, знакомимся с их свойствами и ограничениями.

Первая задача в нашем списке — инициализировать проект. Задача включает в себя создание скелета и настройку сборки приложения.


Начнём с создания скелета. Он будет состоять из двух элементов:

  • root – в нём будет содержаться корневой компонент приложения,
  • init.js — в нём будет происходить инициализация приложения.

Сперва реализуем элемент root. Для этого:

  1. создадим категорию app,

    Категория — директория, применяемая для группировки элементов по реализуемой функциональности.

    — методология, раздел категории

  2. в ней создадим группу components,

    Группа — директория, применяемая для группировки элементов по типу.

    — методология, раздел группы

  3. в ней создадим элемент 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-кита.

Создадим новую категорию — 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';

Дело сделано! 🎉

Начинаем работу над постом

Наша следующая задача — реализовать переиспользуемый компонент, отвечающий за визуализацию поста.

Исходя из требований и макетов следует, что:

  1. пост может содержать текст, а также прикреплённые картинки или видео — и для их просмотра требуются соответствующие просмотрщики;

  2. у поста есть счётчик просмотров, также посту можно поставить лайк, сделать репост, написать комментарий;

  3. у поста есть меню действий, в котором можно подписаться на автора или заблокировать его, а так же пожаловаться на контент.

Как мы видим, полноценная реализация поста — задача комплексная и трудозатратная.

Поэтому будет разумно разбить его реализацию на итерации, сейчас же сосредоточиться только на следующих задачах:

  1. реализовать основную визуализацию поста,
  2. реализовать просмотрщик изображений.

Реализуем компонент, отвечающий за визуализацию поста

Начнём с компонента, отвечающего за визуализацию поста.

Создадим новую категорию — post. Разместим в ней соответствующие группы и элементы, а также создадим точку входа.

Примечание

Здесь и далее примеры файловой структуры будут упрощены

src
└── post
    ├── components
    │   ├── actions
    │   ├── block
    │   ├── content
    │   ├── follow
    │   ├── image
    │   ├── like
    │   ├── menu
    │   ├── post
    │   ├── reply
    │   ├── report
    │   ├── repost
    │   ├── share
    │   ├── text
    │   └── views
    ├── contexts
    │   └── post.js
    └── index.js

Реализуем компонент, отвечающий за просмотр изображений

Здесь мы создаём подкатегорию, знакомимся с её свойствами и ограничениями.

Перейдём к компоненту, отвечающему за просмотр прикреплённых к посту изображений.

Перед нами возникает вопрос — где разместить его код? Мы можем предположить, что:

  1. реализация просмотрщика потребует создания большого объёма файлов;
  2. просмотрщик не является самостоятельной функциональностью и всегда будет использоваться в паре с постом, будет полезно указать связь между ними;
  3. код просмотрщика не требует тесной связи с кодом поста, следовательно, код необходимо разграничить.

Исходя из этого, рассмотрим доступные варианты:

  1. Разместить код просмотрщика в той же категории — post.

    Однако, поскольку его реализация потребует создания большого объёма файлов, смешение файлов поста и просмотрщика:

    • затруднит анализ категории,
    • создаст дополнительную нагрузку на пространство имён.
  2. Разместить код просмотрщика в новой категории — image_viewer.

    Это облегчит анализ категории и создаст новое пространство имён.

    Однако, в таком случае не хватает указания связи между постом и просмотрщиком.

  3. Разместить код просмотрщика в подкатегории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.

В ней размещено довольно много файлов. Если провести мини-исследование, среди них можно выделить две подгруппы:

  • файлы, связанные с реализацией поста,
  • файлы, связанные с реализацией меню действий.

Отдельное мини-исследование — относительно лёгкая задача. Однако, чем больше их требуется, тем сложнее становится анализ проекта. В какой-то момент в проекте невозможно будет разобраться без блокнота и ручки.

Таким образом, лучше всего свести необходимость мини-исследований в проекте к минимуму.

В случае с файлами, которые мы рассматривали выше, будет полезно закрепить результаты мини-исследования и применить к файлам дополнительную группировку.

Перед нами возникает вопрос — как будет лучше это сделать? Рассмотрим доступные варианты:

  1. Разместить файлы в новых подкатегориях.

    Однако, подкатегории уместнее применять для группировки набора файлов с содержимым разного типа. В нашем же случае, файлы одного типа — components.

    Также это потребует соблюдения всех накладываемых на категории ограничений.

  2. Разместить файлы в именованных группах.

    Это позволит применить дополнительную группировку без создания новых категорий.

    Отличный вариант! 💯

Создадим именованные группы 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. Перейдём к реализации самой страницы.

Для этого:

  1. используя внешние компоненты Post и PostEditor, подготовленные ранее, реализуем комплексные компоненты PostFeed и PostEditor, отвечающие за полноценную реализацию и интеграцию ленты и редактора постов;

  2. реализуем компонент 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 в полной мере, вы также можете рассмотреть возможности применить отдельные идеи подхода.

FAQ

Можно ли использовать v.n с TypeScript?

Да!

Можно ли использовать v.n с CommonJS и другими системами модулей?

Да!

Можно ли использовать v.n с другими языками?

Полагаю, что да! Если у вас получится интерпретировать подход в проекте на другом языке, пожалуйста, поделитесь опытом.

Поддержка IDE

Visual Studio Code

Проблема с сортировкой файлов

По умолчанию для сортировки списка файлов используется алгоритм, приводящий в некоторых случаях к перемешиванию групп директорий:

В примере ниже категории 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

JetBrains

Нет известных проблем.

Сообщество

@virtual_nesting_community — сообщество в Telegram, где можно пообщаться и получить помощь.

Обратная связь

Буду рад любой обратной связи! Пожалуйста, пишите в issues или discussions.

Благодарности

Хочу поблагодарить мою жену @daryabratova, а также моих друзей @kindaro, @ruzovska и @a1ex-kaufmann.

Поддержать автора

Я потратил сотни часов, работая над этим проектом. Надеюсь, он сэкономит тысячи часов времени другим разработчикам.

Вы можете поддержать меня на Patreon или Boosty.