Всем привет! Меня зовут Анатолий, я представляю команду Front-End разработки компании DD Planet.

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

Рождение

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

Проблемы

Из-за быстрой разработки сразу появились сложности:

  • Хаотичная структура проекта — файлы и компоненты разбросаны случайным образом, отсутствует единый подход к организации кода, сложно добавлять фичи, так как непонятно, где их правильно разместить, отсутствие единого стиля написания;

  • Высокая связность — правка в одном модуле неожиданно ломает другой, компоненты тесно переплетены между собой и не могут функционировать изолированно;

  • Проблемы с переиспользованием кода — похожие компоненты дублируются, а не выносятся в общие модули, одни и те же кнопки, формы и стили ведут себя по‑разному в разных частях приложения;

  • Трудности масштабирования — внедрение нового функционала занимает больше времени, так как разработчик вынужден разбираться в имеющемся хаотичном коде;

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

Подготовка

В первую очередь было решено актуализировать основные библиотеки. Так как в процессе разработки проекта они успели обзавестись новыми версиями. В качестве библиотеки компонентов мы использовали Mantine, обновление которой до 7 версии потребовало много усилий по поддержанию несовместимых изменений. В качестве фреймворка используется Next.js, в процессе обновления которого мы поддержали новую систему маршрутизации - App Router. После реализации этих обновлений пришло время определиться с архитектурой проекта.

Выбор архитектуры

Мы определили для себя ряд критериев, которым должна соответствовать новая архитектура:

  • Простота — понятные и однозначные правила организации кода;

  • Масштабируемость — возможность быстро добавлять новый функционал; 

  • Командная работа — возможность параллельной разработки разными специалистами, быстрая интеграция нового сотрудника;

  • Навигация — удобная система поиска компонентов и функциональности.

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

Components/Containers

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

Минусы подхода:

  • Сложность масштабирования — компоненты часто становятся монолитами, которые сложно комбинировать и переиспользовать. Вместо композиции небольших, компонентов, разработчики создают крупные, многофункциональные контейнеры;

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

Atomic Design

Основан на аналогии с химией и представляет интерфейс как иерархию компонентов. Методология включает пять уровней иерархии:

  • Атомы — базовые строительные блоки интерфейса (кнопки, шрифты, цвета, иконки, поля ввода). Это неделимые элементы, которые формируют основу системы;

  • Молекулы — комбинации атомов, работающие как единое целое (форма поиска, поле ввода с кнопкой);

  • Организмы — сложные блоки, состоящие из молекул и атомов (хедер, футер, карточки товаров);

  • Шаблоны — каркасы страниц с определенным расположением организмов;

  • Страницы — финальные версии с реальным контентом.

Минусы подхода:

  • Несоответствие реальным бизнес-требованиям - бизнес-логика часто не укладывается в чистую иерархию;

  • Проблемы с переиспользованием - компоненты становятся слишком абстрактными и обрастают десятками параметров для всех случаев.

Feature Sliced Design (FSD) 

Feature‑Sliced Design — это архитектурная методология для фронтенд‑приложений, созданная сообществом разработчиков. Она фокусируется на бизнес‑логике и обеспечивает масштабируемость, поддерживаемость. Ссылка на проект здесь.

Слои стандартизированы во всех проектах FSD:

  • App — все, благодаря чему приложение запускается — роутинг, точки входа, глобальные стили, провайдеры и так далее;

  • Pages (страницы) — полные страницы или большие части страницы при вложенном роутинге;

  • Widgets (виджеты) — большие самодостаточные куски функциональности или интерфейса, обычно реализующие целый пользовательский сценарий;

  • Features (фичи) — повторно используемые реализации целых фич продукта, то есть действий, приносящих бизнес‑ценность пользователю;

  • Entities (сущности) — бизнес‑сущности, с которыми работает проект, например user или product;

  • Shared — переиспользуемый код, отделенный от специфики проекта/бизнеса, хотя это не обязательно.

Минусы подхода:

  • Высокий порог входа и сложность обучения — FSD требует значительного времени на освоение всеми членами команды;

  • Субъективность и споры о принадлежности — разные интерпретации правил разными членами командами, споры о принадлежности компонента к тому или иному слою.

Внедрение

Ни одна из описанных архитектур в полной мере не удовлетворяла всем критериям, поэтому нам пришлось пойти своим путем. В качестве основы была выбрана методология Feature-Sliced Design (FSD) с ее идеей разбиения компонентов на слои и наличием подробной документации. При этом мы отказались от жесткой структуры чтобы получить возможность более гибкой настройки импортов.

Каждый из этих слоев, кроме shared, может в свою очередь тоже делиться на слои по необходимости. Здесь нами использовалась уже привычная структура из FSD, однако мы заменили features и entities на common, так как они и порождали больше всего споров, стали отталкиваться в первую очередь от структуры самих страниц. Фактически Feature-Sliced Design (FSD) превратился в Page-Sliced Design (PSD).

Слой domain был определен для бизнес логики, которая тоже может делиться на крупные блоки. На этом уровне уже появляются страницы pages.

Чтобы минимизировать споры о принадлежности к тому или иному слою, было решено применять принцип local first. Таким образом компоненты находятся максимально близко к месту использования и перемещаются на базовый уровень только в том случае, если начинают использоваться на нескольких слоях. Для организации самих компонентов была выбрана упрощенная структура, состоящая из components, shared и файла самого компонента. В случае с модальниками, или чем-то подобным, в корень добавляется еще и функция для взаимодействия.

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

Контроль

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

 module.exports = {

   extends: 'next/core-web-vitals',

   plugins: ['import'],

   ignorePatterns: ['**/.next/**'],

   rules: {

     'react/jsx-curly-brace-presence': 'error',

     'react-hooks/exhaustive-deps': 'off',

     'import/no-restricted-paths': [

       'error',

       {

         basePath,

         zones,

       },

     ],

   },

 };

Зоны вычисляются на основе иерархии

 const HIERARCHY = [

  { layer: 'app', level: 1 },

  { layer: 'domain', level: 2 },

  { layer: 'modules', level: 3 },

  { layer: 'common', level: 4 },

  { layer: 'core', level: 5 },

  { layer: 'shared', level: 6 },

 ];

 const zones = LAYER_PATHS.map((targetPath) => {

 const layer = HIERARCHY.find((w) => w.layer === targetPath);

 if (layer) {

    const restricted = HIERARCHY.filter((w) => w.level < layer.level);

    const restrictedRules = restricted.map((r) => ({

      target: targetPath,

      from: r.layer,

      message: 

   ${targetPath} layer cannot import from ${r.layer} layer,

    }));

    const modulesPaths = targetPath !== 'shared' 

  ? fs.readdirSync(${basePath}/${targetPath}) 

  : [];

    const modulesRules = modulesPaths.map((w) =>

      modulesPaths

        .filter((m) => m !== w)

        .map((m) => ({

          target: ${targetPath}/${w},

          from: ${targetPath}/${m},

          message: import between /${targetPath} are restricted,

        }))

    );

    return [...restrictedRules, ...modulesRules.flat()];

  }

})

  .flat()

  .filter((w) => !!w);

Результаты

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