virtual-nesting / virtual-nesting

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

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

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

virtual.nesting 💊

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

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

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

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

Содержание

  1. Мотивация
  2. Быстрый старт
  3. Методология
  4. FAQ
  5. Поддержка IDE
  6. Сообщество
  7. Обратная связь
  8. Благодарности
  9. Поддержать автора

Мотивация

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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.

About

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

License:MIT License