Продумать структуру приложения на этапе создания очень важно, но часто в начале пути этому вопросу посвящают мало внимания. Предлагаю обсудить проблемы масштабирования современных веб-приложения с которыми сталкиваются разработчики.
Первое время разработка идет бодро, добавлять новые функции легко и приятно, приложение работает быстро, все просто и понятно. Но с ростом количество модулей, компонентов, сервисов скорость разработки падает, как и скорость работы приложения. Задач на рефакторинг появляется в спринте все больше. Кроме прочего, команда разработчиков может регулярно меняться (одни увольняются, другие приходят), что не добавляет порядка. Со временем может показаться, что проще все снести и написать заново.
Современные приложения предоставляют пользователям широкие возможности взаимодействия. Сложность логики интерфейса постоянно растет. Количество данных, которое мы загружаем на каждой странице, растет. Требования к производительности интерфейса растут. Количество разработчиков, которые работают над приложением, растет. В таких условиях рефакторинг структуры приложения становится проблемой, поэтому очень важно подумать о ней в самом начале.
Ниже представлены свойства, которые имеет идеальная, на мой взгляд, структура приложения:
Простая и понятная. Любой разработчик из команды не должен ни секунды думать, где разместить новый модуль, компонент, сервис, интерфейс или любую другую сущность.
Масштабируемая. В любом момент может появиться необходимость добавить в приложение новую функцию или новый раздел. Это не должно быть проблемой. При этом производительность не должна проседать с ростом приложения.
Принципы, которые необходимо соблюдать, чтобы достичь такой структуры приложения:
Независимые модули. Каждый раздел приложения должен быть представлен в виде отдельного модуля. Это ключевой принцип, он является основной для всех следующих. Он позволяет масштабировать приложение и комфортно работать над ним большому количеству разработчиков одновременно.
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.