Архитектура фронтенда (доработки и SSR)

Почти год мне понадобился на то, чтобы доработать архитектуру, которую я описывал в предыдущей статье. Отчасти столько времени понадобилось на то, чтобы продумать проект, на котором я буду отлаживать и дорабатывать архитектуру. Отчасти на то, чтобы разобраться с другими делами как, например, бесплотные поиски работы и метания между попыткой написать книгу. Отчасти на то, чтобы побороть внутреннюю жабу, которая не давала мне купить подписку на нейронку, и решиться-таки пилить свой проект.
В общем, дней утекло достаточно. За это время я окончательно уверился в том, что рынок умер, а с ним и я как наёмный фронтенд-разработчик. Надежда найти работу если и осталась, то настолько призрачная, что её почти и незаметно.
А потому в этой статье я, пожалуй не стану себя одёргивать и буду самим собой, не опасаясь за будущую карьеру. То, что мертво, всё равно не убить.
Ну, да хватит ныть. В конце концов я готов-таки представить на суд сообщества свою статью и предложить архитектуру, которая подойдёт и SPA, и SSR, и с которой не придётся мучиться так же, как с FSD, которую по странной случайности тоже называют архитектурой.
Ну, приступим.
Об архитектуре
Скажу сразу: примеров кода тут не будет. Разговор исключительно об архитектуре. Я реализовал эту архитектуру на Nuxt. Если интересно, как именно — пишите в коменты, я напишу отдельную статью о конкретной реализации уже с примерами кода.
Понятийный аппарат я описал в предыдущей статье и дублировать его тут не стану. Повторю только одно:
Архитектура программного обеспечения (ПО) — это план или структура, описывающая, как система организована, как разные ее части (компоненты) взаимодействуют между собой и с внешними системами.
Как я уже говорил в предыдущей статье, опыт соприкосновения с FSD в своё время принесло мне довольно много боли. Методология расплывчатая и не конкретная, фичи часто пересекаются и из-за этого слои перемешиваются, из-за чего её трудно готовить и в каждой команде и даже в каждой голове лежит своя версия FSD.
При построении своей архитектуры я держал в уме две вещи:
Устранение болей, которые пыталась устранить FSD.
Устранение болей, которые привнесла FSD.
Первое, что необходимо было сделать — это обеспечить разделение слоёв и обеспечение уменьшение связанности между ними. Для этого каждый слой связан только с одним функциональным слоем и не больше. Всегда.
Не буду тянуть кота за тестикулы. Приветствуйте, схема:

Тут понадобятся пояснения. В первую очередь, функциональные слои. Их 5 (тут перечислю, подробнее опишу ниже):
API. В идеале генерируется автоматически из документации.
Домен.
Запросы и команды.
Composable слой. Те самые фичи.
Сторы
UI.
Подробно о слоях
Домен
Это ядро нашего приложения. Альфа и омега. Единственный источник истины для всех остальных модулей.
Перво-наперво домен должен быть независим. Он не должен знать ничего о том, как у вас там реализуется реактивность, кэширование и прочая прикладная фигня. Vue, React, Angular, Svelte тут быть не должно. Домен — храм, а храм должен быть чист от мирского. Без исключений. Никаких тяжёлых зависимостей.
Домен делает чистые запросы к api, маппит их и отдаёт дальше.
Api
Даже и не знаю, что тут сказать. Слой с прямыми запросами к источнику данных. В идеале генерируется из описаний контракта бэкенда. Openapi, proto... не важно. Главное тут автогенерация для гарантии актуальности контрактов. Думаю, понятно, почему.
Запросы, команды, сторы
Тут живёт управляющий запросами слой. Кэширование, дедупликация, ретрай, всё тут.
По задумке этот слой реализует CQRS паттерн. Не обязательно городить отдельные шины для запросов и команд, но хотя бы на уровне директорий и импортов разделить команды и запросы всё же стоит.
Команды и запросы ничего не знают ни об API, ни о серверных моделях данных. Тут всё работает только в парадигме фронтенда.
Фичи (composable)
Тут уже появляется зависимость от VDOM библиотеки.
На этом слое реализуется конкретная бизнес-логика (user-stories) и вспомогательные ui функции (toast, modal и т.д...). Разделение тут предельно простое:
└── 📁 composable
├── 📁 entety1
│ ├── 📄 index.ts
│ ├── 📄 useAction1.ts
│ └── 📄 useAction2.ts
├── 📁 entety2
│ ├── 📄 index.ts
│ ├── 📄 useAction1.ts
│ └── 📄 useAction2.ts
└── 📁 shared
├── 📄 function1.ts
├── 📄 function2.ts
└── 📄 index.tsПравило простое: импорты между entity запрещены. shared свободно импортируется по необходимости.
Composable слой ничего не знает ни об api, ни о репозиториях, ни от маппинге. Из домена он берёт только типы и константы.
Сторы
На самом деле это что-то вроде слоя 3.5
К одному и тому же стору могут обращаться разные composable функции, но при этом сторы не запрашивают данные сами и зависят от VDOM библиотеки. Сторы управляются только composable функциями и больше с ними никто не общается. Совсем.
Сделано это по двум причинам:
Чтобы не тащить VDOM зависимость в слой запросов/команд.
Чтобы не получить неожиданные данные из стора из-за неявного его обновления во время очередного запроса данных. Все обновления сторов явные и из composable функций.
UI
Собственно, слой UI. Не должен содержать описаний бизнес-логики, не должен сам запрашивать данные, не обращается к сторам и ничего в них не пишет. Никогда. В идеале импортирует функции из composable слоя и применяет в разметку.
Внутренняя организация ui компонентов на ваш вкус. Можно имплементировать atomic, можно wiget-component подход. Как нравится. Это мелочи и всегда можно будет не дорого переработать.
Плюсы и минусы
Минусы
Начнём с проблем.
В первую очередь это, конечно, сравнительно большое количество boiletplate кода, затрат на документацию архитектуры, линтинг и т.д.
Больше я минусов, прямо говоря, не вижу.
Плюсы
Чёткое разделение ответственности между слоями и, как следствие, простая и однозначная декомпозиция
Простая локализация ошибок. Чаще всего сразу понятно, на каком слое и в какой сущности её искать.
Лёгкое погружение в проект новых разработчиков.
Лёгкое и понятное декомпозирование задач:
Описать типы => описать маппинг => описать репозитории => описать запросы/команды => описать composable => описать uiУдобное ревью за счёт понятного и всегда однозначного паттерна реализации функционала
Простой контроль контекста нейросети.
Бизнес-документация прекрасно укладывается в корень domain области.
За счёт строгого потока зависимостей можно давать изолированные задачи типа «реализовать репозитории сущности A по требованиям `/domain/<entetyA>\readme.md`Простой контроль правил для нейросети.
Жёсткая фиксация отношений между слоями даёт нейросети прочную основу для самопроверки и понятные паттерны при ревью человеком.
Собственно, на этом всё. Добро пожаловать в комментарии. Как всегда.
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.