Архитектура front-end приложений — react, react native, angular. Обзор
Предисловие
Начнем цикл статей нашей компании легко и непринужденно, с темы из мира front-end. Надеюсь, что статья будет полезна тем, кто хоть как то связан с миром front-end. Предупреждаю, что backend разработчикам может быть скучновато из-за того, что у них давно все стандартизировано :-)
Когда я провожу собеседование на позицию React разработчика, обычно задаю общий вопрос об архитектуре — «как бы ты строил архитектуру своего приложения и почему?». После обсуждения, я начинаю рассказывать небольшую часть материала из этой статьи, 5% по содержанию, приводя тезисы, по которым можно будет собрать общую картину моего видения. И обычно получаю положительную обратку. Поэтому решил изложить этот материал более развернуто здесь. Буду отправлять ссылку тем, кому это может пригодиться :-)
В статье я рассмотрю ключевые, на мой взгляд, подходы в построении архитектуры front-end приложений, а так же к чему пришли мы в ходе заказной разработки. Мы называем свой подход модульным и одинаково понимаем этот термин внутри команды. Он прост, местами банален и очень хорошо показал себя при разработке как мобильных приложений на React Native, так и front-end на Angular, React и не вижу проблем применять его на Vue. И поэтому договоримся сразу, что все описанное в первую очередь нужно рассматривать через призму front-end разработки
Архитектура front-end«a. Что не так?
Бэкенд разработчики молодцы, давно стандартизировали уже свои подходы к архитектуре, масштабированию приложений на практике, написали умные книжки, а главное — их прочли. Нужно только терпение и желание, чтобы во все вникнуть и не писать больше фигню, а что написано «до» — никому не показывать. Фронтенд разработчики же относительно бэкенд не так давно узнали, что они оказывается разработчики и что им нужны фреймворки для ускорения избавления от страданий.
В итоге архитектура фронта зачастую скудна на практике, что-то более менее годное применяют IT гиганты, потому что деваться некуда, нужны микросервисы на фронте, сеньеры с испариной на лбу пишут код. А что делать остальным? Там пропасть — архитектура приложений React уровня tutorial для новичков: api, components, containers и т.п.
Надеюсь, ни у кого не подгорело и мы начнем.
Какими свойствами должна обладать хорошая архитектура?
Я выделил для себя 5 основных свойств хорошей архитектуры с описанием того, какой смысл я закладываю в каждое. Они выглядят очевидно, но их важность от этого становится только выше.
Простота
Подход должен описывать простые и понятные правила создания структуры. Их легко донести до коллег и обучить на практике. Правила трактуются однозначно, границы определены четко, поэтому споры с расположением файлов или отнесением их куда-то не туда сводятся к минимуму. Это сильно экономит время при разработке.
Масштабируемость кода
Правила и принципы, которые порождают структуру выдерживают рост проекта. Он остается понятным, с ним удобно работать, структуру не придется полностью рефакторить через год-два плотной разработки.
Масштабируемость команды
Структура позволяет участникам команды работать над разделами проекта с определенной долей изоляции друг от друга. Например, коллега работает с фичей оплат, другой с пользователями, а третий — с общим модулем форм на проекте.
Универсальность
Структура и правила не должны зависеть от бизнес логики проекта, его тематики. Архитектура должна позволять быстро начать и не обосраться через 4 месяца разработки.
Удобная и быстрая навигация
Если мне нужно найти компонент, я сразу пойму где его искать. При этом, размер проекта не будет играть определяющую роль.
Разберем примеры архитектур
Components, Store, Containers
Термин «Container components» ввел Дэн Абрамов, когда придумал Redux. Простой и понятный подход, с него начинали все, кто изучал Redux. Примерами такого подхода пестрят многие туториалы по ReactJS.
Суть подхода:
Проект делится на компоненты, за исключением компонентов, которые имеют связь с Redux. Например, компонент авторизации если у него есть понятные входные и выходные данные будет обычным переиспользуемым компонентом, но как только мы подключили его к Redux, то есть отправляем какие либо actions из компонента или достаем данные из стора и используем в нем — он стал контейнером. Если у нас этот компонент лежал доселе в components, то теперь его отправят в containers.
Минусы подхода:
Горизонтальный рост папок components, containers
Когда проект разрастается, растет и количество компонентов в каждой папке — components или containers. Есть решение — группировать компоненты по бизнес логикеВозможны лишние перемещения
Как только мы подключаем к компоненту Redux, его нужно переместить в соответствующую папку containers. Если проект пестрит такими перемещениями, то архитектурно он построен неправильно или недостаточно удобноЭлементы бизнес логики находятся в разрозненных местах
Например, у нас на проекте появился блог. Появляется api для загрузки, интерфейсы описывающие сущности, компоненты, страницы и навигация. Бизнес сущность одна, а мест где она представлена — много. Работать с этим сложнее, когда проект разрабатывается командой и ежегодно растет по кодовой базе.
Пример структуры проекта для подхода «Components, Containers»
Atomic Design
Методология разделения компонентов, которые создают что-то вроде матрешки. У каждой группы свое назначение и зона ответственности. Не будем углубляться, почитать можно здесь.
Суть подхода:
Для компонентов придумали правила для их группировки, не по бизнес логике, а по их сложности. Подход выделяет следующие группы компонентов:
Атомы
Базовые элементы, которые дальше не делятся: кнопки, ссылки, элементы текста.Молекулы
Это компоновка атомов. То есть молекулы переиспользуют атомы. Например, группа кнопок или кнопка + текст, которые можно переиспользовать дальше.Организмы
Компоновка молекул и атомов в более сложные интерфейсы или фичи. Например форма для расчета стоимости проекта, в которой есть кнопки или составные из молекул, элементы.Шаблоны
Это layout«ы, которые представляют собой правила расположения элементов внутри. Могут содержать любые элементы: атомы, молекулы, организмы.Страницы
Элемент навигации, который содержит шаблон по которому располагается контент внутри.
Минусы подхода:
Сложно
Заставить себя выучить всю эту историюБесконечные споры
О том, где проходит четкая граница между группами компонентовПеремещение компонентов
Компоненты все так же могут перемещаться между группами при изменении их степени сложности
Пример реализации подхода «Atomic Design» в вакууме
Feature Oriented
Отдельно упомяну, что в сообществе встречается так называемый Feature Oriented подход. Несложно догадаться, что он строится вокруг фич приложения. Можно нагуглить много примеров на эту тему, кто-то называет их модулями.
Суть подхода:
Основной для разделения проекта на компоненты является не их техническое происхождение, а бизнес логика. Что‑то это напоминает, неужели Domain Driven Design. Но мы уже создавали папки posts и users в самом первом подходе про Components и Containers, так в чем разница? Отличие как раз в основе разделения. То есть мы не группируем по папкам users и posts, когда компонентов в общей папке components стало слишком много, мы создаем отдельные модули или фичи users и posts, а внутри них будет находиться все что относится к этим модулям. Наглядно — на рисунке ниже.
Слева — Components, Containers. Справа — Feature Oriented
Минусы подхода:
Непонятно что внутри
С верхним уровнем более менее ясно, что творится внутри фичи — каждый сам себе художник и «лепит» как знает
И следом приложу пример структуры по папкам на таком проекте.
Пример реализации подхода «Feature Oriented»
Feature sliced design
Архитектурная методология для front-end проектов, разработка свежая — 2021 года. Собрались хорошие ребята, учли ошибки, которые уже давно научились избегать в архитектуре на бэкенде и придумали как их решать на фронте. Ссылка на проект здесь.
Суть подхода:
Доменный подход, деление на слои, сегменты, изолированные модули, правила по по работе между слоями (чтобы не возникали циклические зависимости), продуктовая составляющая (features) и многое другое. Документация постоянно дополняется. Проекту огромный респект.
Минусы подхода:
Порог вхождения
Новичкам сложно вкатываться, много правил и обсуждений идет на тему того что и куда положить в конкретном случае.
Наш опыт использования сводится к тому, что мы взяли на вооружение некоторые фишки подхода. В чистом виде не используем, но не исключено что постепенно к этому придем. Когда собеседуем фронтендеров, мало кто упоминает или слышал про этот подход, а новички (1–2 года опыта) — практически никогда.
Пример подхода «Feature Sliced Design»
Модульный подход или «Наш велосипед»
Глобально основывается на модулях и подходе Domain Driven Design.
Модули
Суть в уже знакомом делении проекта на модули / фичи. Создаем папку modules, если есть пользователи, посты, какой нибудь конструктор сценариев, то появляются модули users, posts, scenarios-constructor. Все просто. Конечно есть shared модули и конечно мы взяли некоторые фишки из Feature Sliced Design, например public api, но об этом ниже.
При разделении на модули все компоненты и логика находятся рядом друг с другом. С таким проектом проще работать: легче ориентироваться в структуре, удалять и рефакторить модули, проще увидеть общую картинку по функциональности, меньше вероятность помешать друг другу при разработке.
Функциональность не всегда означает бизнес логику. Существуют функциональные или инфраструктурные модули, такие как core, ud-ui (общие компоненты), не привязанные к бизнес логике.
Domain Driven Design
Внутри модуль состоит из простых уровней. Это подсмотрено у Эрика Эванса (Domain Driven Design): domain, store, ui.
Схема модульного подхода
У каждого слоя модуля есть ограничения, он не может иметь доступ и использовать код с уровня ниже (на минималках напоминает Feature Sliced Design).
Ограничения уровней внутри модуля
Пример модульного подхода в GetGain
Другие фишки
Public API
Например, у нас есть модуль для работы с файлами, а именно их загрузкой. В модуле создается файл index.ts, который экспортирует наружу все, что разрешено использовать. Таким образом модуль решает каким будет интерфейс его использования, можно изолированно работать с этим модулем и не ломать API, не натыкаться на side effects и лишние ошибки. Как это может выглядеть:
Папка с простым модулем
Файл index.ts
export * as fileDomain from './domain';
export { downloadByUrl } from './ui';
Пример использования
import { downloadByUrl, fileDomain } from 'modules/file';
Libs
Libs — это папка, содержащая утилиты, которые не имеют отношения не только к бизнес логике проекта, но и к какой то специфичной инфраструктуре проекта. То есть утилиты должны быть такие, чтобы эту папку можно было взять и смело перенести в другой проект, скопировав.
Выглядит как костыль, в чем же смысл? Мы занимаемся заказной разработкой, нам важна скорость и предсказуемость в повторяющихся кейсах, потому что мы ни раз уже потратили время на их разработку, тестирование и стабилизацию. Разработчик возьмет уже протестированный сборник утилит (libs) с другого проекта. Если предыдущий опыт не учел особенностей текущего проекта, или появились новые поводы для создания библиотек — допишет. Но он будет дописывать код, который уже создавался как кросс-проектный.
Через несколько таких итераций выделяются стабильные библиотеки, затем они дополнят общий npm пакет, который устанавливается на новых проектах.
Какое преимущество дает нам эта фишка? Нет overhead на то, чтобы выделять команду, целью которой будет поддержка этой библиотеки, но при этом легкое действие в виде создания и поддержания актуальной папки libs с промежуточными ретро между проектами позволяет хоть иногда об этом думать и даже выделять код в общий пакет.
Пример папки libs на проекте React Native
Заключение
Надеюсь несложно было читать много букв. Свой модульный подход в разработке мы используем уже более 4-х лет. Дополняем фишками, вносим корректировки. Он не зависит от фреймворка, прост в понимании и использовании. И есть много других подходов, наш не панацея, но пока что сейчас — нам удобно.