Продумать структуру приложения на этапе создания очень важно, но часто в начале пути этому вопросу посвящают мало внимания. Предлагаю обсудить проблемы масштабирования современных веб-приложения с которыми сталкиваются разработчики.
Первое время разработка идет бодро, добавлять новые функции легко и приятно, приложение работает быстро, все просто и понятно. Но с ростом количество модулей, компонентов, сервисов скорость разработки падает, как и скорость работы приложения. Задач на рефакторинг появляется в спринте все больше. Кроме прочего, команда разработчиков может регулярно меняться (одни увольняются, другие приходят), что не добавляет порядка. Со временем может показаться, что проще все снести и написать заново.
Современные приложения предоставляют пользователям широкие возможности взаимодействия. Сложность логики интерфейса постоянно растет. Количество данных, которое мы загружаем на каждой странице, растет. Требования к производительности интерфейса растут. Количество разработчиков, которые работают над приложением, растет. В таких условиях рефакторинг структуры приложения становится проблемой, поэтому очень важно подумать о ней в самом начале.
Ниже представлены свойства, которые имеет идеальная, на мой взгляд, структура приложения:
-
Простая и понятная. Любой разработчик из команды не должен ни секунды думать, где разместить новый модуль, компонент, сервис, интерфейс или любую другую сущность.
-
Масштабируемая. В любом момент может появиться необходимость добавить в приложение новую функцию или новый раздел. Это не должно быть проблемой. При этом производительность не должна проседать с ростом приложения.
Принципы, которые необходимо соблюдать, чтобы достичь такой структуры приложения:
-
Независимые модули. Каждый раздел приложения должен быть представлен в виде отдельного модуля. Это ключевой принцип, он является основной для всех следующих. Он позволяет масштабировать приложение и комфортно работать над ним большому количеству разработчиков одновременно.
-
Lazy loading для каждого раздела. Мы должны загружать модуль раздела только тогда, когда пользователь переходит в него. При таком подходе при масштабировании приложения его производительность не будет страдать.
-
Provide in root только тогда, когда это действительно необходимо. Не раз встречал подход, когда тимлид говорит, что все сервисы должны быть внедрены в корневой модуль. Не знаю для чего это делать, может быть для того, чтобы избежать ошибки появление нескольких экземпляров одного и того же сервиса. Я считаю, если сервис используется только в одном модуле, то он должен быть внедрен в этот модуль и находиться рядом с ним в одной папке. Такие сервисы не должны жить все время работы приложения.
-
Вспомогательные сущности (интерфейсы, константы, перечисления и т.д.) должны соотноситься с модулями. Если сущность используется только в одном модуле, то она должна лежать в папке с этим модулем. Мы не должны все складывать по умолчанию в корневую папку приложения. Важно соблюдать порядок и отношения.
-
Общие модули. Когда мы имеем пайп/директиву/компонент, который используется только в нескольких разделах приложения (не во всех), то нет необходимости внедрять его в корневой модуль и загружать при старте приложения. Необходимо создать отдельный независимый модуль и импортировать его только в те разделы, где он используется. Таким образом, этот пайп/директива/компонент будет загружаться по требованию вместе с соответствующими разделами приложения.
-
Небольшая вложенность папок и однообразность структуры модулей. Чем меньше вложенность папок в проекте, тем удобнее с ним работать – мелочь, а приятно. Также однообразная структура модулей позволяет поддерживать порядок независимо от размера проекта.
Итак, перейдем собственно к самой структуре. Я представил ее на другом ресурсе для вашего удобства, чтобы вы имели возможность развернуть/свернуть каждую ветку проекта. Она выглядит так:
https://dynalist.io/d/iZZJgMzUewPX9Thji4xebcGa
Рассмотрим подробнее одну из веток – «system» модуль:
system -- core ---- services ---- interfaces ---- store -- transactions ---- core ------ services ------ interfaces ---- transactions.component.html ---- transactions.component.scss ---- transactions.component.ts ---- transactions.module.ts ---- transactions-routing.module.ts -- header ---- header.component.html ---- header.component.scss ---- header.component.ts -- system.component.html -- system.component.scss -- system.component.ts -- system.module.ts -- system-routing.module.ts
Я предлагаю в каждом модуле создавать папку «core». В нее мы будем складывать все сервисы, интерфейсы, константы и прочие сущности, которые относятся к модулю. Соответственно, все эти сущности должны быть внедрены именно в этот модуль.
Файлы модуля («system.module.ts», «system-routing.module.ts») и файлы главного компонента модуля («system.component.html», «system.component.scss», «system.component.ts») лежат в корне. Все остальные компоненты которые относятся к модулю (например, «header» компонент) мы также будем складывать в корень, но уже в папках. Еще в корень мы будем складывать все модули внутренних разделов (например, «transactions» модуль).
Все модули проекта будут выглядеть одинаково с точки зрения структуры. При этом мы можем масштабировать такую структуру сколько угодно – добавлять модули рядом или внутрь на любую глубину. Мы можем лениво загружать каждый раздел и подраздел приложения со всеми его зависимостями. Все однообразно, наглядно и, следовательно, очень просто.
И, напоследок, небольшой лафхак как сделать красивые короткие импорты, которые отображают принадлежность сущности к модулю. Попытаемся избавиться от таких конструкций:
import { TransactionsService } "../../../../../core/services/transactions.service"; import { UsersService } "../../../../../core/services/users.service";
В каждой папке, которые находятся в «core» создаем файл «index.ts» и экспортируем все содержимое папки в нем:
core -- services ---- transactions.service.ts ---- users.service.ts ---- index.ts // содержимое файла index.ts export * from './transactions.service'; export * from './users.service';
После направляемся в файл «tsconfig.json», ищем раздел «paths» и указываем в нем алиасы для каждого из модулей первого уровня (обычно этого достаточно, потому что зачастую только модули первого уровня являются зависимостями для остальных модулей):
"paths": { "@environments/*": ["src/environments/*"], "@app/*": ["src/app/*"], "@auth/*": ["src/app/auth/*"], "@system/*": ["src/app/system/*"], "@form/*": ["src/app/shared/form/*"] }
Кроме прочего, я создаю алиасы для каждого из общих модулей из папки «shared», а также для переменных окружения. Теперь мы имеем вот такие красивые импорты:
import { TransactionsService, UsersService } from '@app/core/services'; import { TransactionDto, UserDto } from '@app/core/interfaces'; import { AuthService } from '@auth/core/services'; import { Size, Color } from '@form/core/enums'; import { FieldOptions, ButtonOptions } from '@form/core/interfaces';
Описанная мной структура хорошо показала себя на практике. До сих пор в проектах, где я ее использовал, мне не приходилось ее менять в процессе роста проекта. Она проста и масштабируема. Над проектом с такой структурой комфортно работать командой. Все наглядно и всегда порядок.
Спасибо всем, кто прочитал статью! Буду рад любой обратной связи. Успехов в разработке приложений на Angular.
ссылка на оригинал статьи https://habr.com/ru/post/687062/
Добавить комментарий