Главное - успейте вовремя остановиться на текущем пути) Если поверх сгенерированного по спеке апи-клиента еще прикручивать автоматические валидаторы типа Zod, то одна из проблем формально решится - то, что бэк реально отдает, начнет валидироваться. Но добавятся новые - фронт будет логировать ошибки или падать при изменении неиспользуемых полей. По факту фронт становится "валидатором, что бэк правильно сгенерировал спеку" - это вообще путь не туда. Я потратил пару лет на этот путь, и продвинулся довольно далеко в энтерпрайзах - но проблемы будут множиться, инфраструктура становиться все сложнее, а по времени и ресурсам бизнесу это все куда дороже, чем даже вручную править.
потратил полдня на то, чтобы понять, почему фронт падает после обновления бэка
Решение этого - не в проверке соответствия бэкенда им же написанной спеке. Просто предостерегаю от излишнего погружения в такую схему - лучше не делать дефолтной для проектов, есть подходы эффективнее.
Идея генерировать апи-клиент с типизацией из openapi стара, и ее недостатки достаточно изучены, чтобы сказать - это скорее нишевая схема, в большинстве проектов добавляет больше неразберихи и проблем, чем пользы.
Часть проблем вы уже нашли сами, с другой частью еще столкнетесь:
схема в реальности может расходиться с фактически присланными данными (по моему опыту - частая история), то есть гарантии мы не получаем
проблемы с версионированием - если скажем 10 разработчиков бэк+фронт работают над разными ручками в рамках своих задач, нужна сложная инфраструктура чтобы удобно разрабатывать. Это отдельное пространство для проблем, которые можно "героически решать", например публикуя с yaml или с готовым апи-клиентом пакеты во внутренние репозитории, генерируя в день сотни "@api-spec": "123.3.1877-feature-123", синхронизируя со стендами и деплойными циклами, занимаясь сложными слияниями в dev. Ну либо перекидываться в рабочих чатах yaml файлами, а потом на dev стенде после слияния нескольких фич ловить что или фронт или бэк вмерджили что-то не выложенное на стенд или ушли в глубокий конфликт.
фронту может быть нужен 1 параметр в конкретной ручке, а бэк присылает 100 (возможно - легаси, или для разных приложений-потребителей). Сгенерированный апи-клиент не очистит лишнее, ему все кажется важным и нужным.
в ряде кейсов теряется параллельность разработки бэка и фронта. В задаче согласовали переименование параметра в ручке - фронт не может у себя поменять, проверить, доработать типы и связанный код. Он должен дождаться пока бэк либо сделает задачу, либо сделает заглушки -> генерируется openapi -> генерируется апи-клиент -> можно начинать работу. Либо костылить и реплейсить вручную части сгенерированного апи-клиента.
большинство cli-утилит генерации заточены под единый огромный инстанс апи-клиента. Сложно будет сделать lazy loading, разбиение на чанки, модульность (чтобы апи лежало ближе к местам использования), не потеряв единый процесс фетчинга и не наплодив дубляж.
велик риск что в апи-клиент попадут неиспользуемые ручки, и в целом анализ по проекту "какие именно ручки и какие именно поля фронт использует" будет затруднен. Бандл неизбежно раздувается, на Хабр летят статьи "почему вкладки браузера тормозят и едят столько памяти".
логирование несовпавших ожиданий все равно требуется - то есть необходимо где-то хранить "что именно нужно фронту для работы", и делать дифф со сгенерированным апи-клиентом.
Ну, о недостатках можно говорить очень долго, суть - "сгенерированный апи-клиент по openapi может гарантировать, что он соответствует openapi, но абсолютно не гарантирует что он соответствует конкретному фронту-потребителю".
Конечно, есть кейсы, где это все подходит на каком-то из этапов развития проекта (особенно если количество фронтендеров === 1 и проект маленький), но он скорее тупиковый.
Как не сталкиваться с этими проблемами? Воткнуть генерацию в правильное место - не в то, чтобы создавать js-файл "что примерно отдает бэк", а в то, чтобы "производилась валидация ожиданий между тем, что нужно фронту и что реально отдает бэк". Написать руками DTO для ручек только с теми полями, которые нужны конкретному фронту-потребителю, а дальше уже подключать генерацию, как примерно описывал тут.
Концепт автооборачивания действительно улучшает DX, но конкретно этот пакет тянет babel/core и его 15 зависимостей на мегабайты, что на мой взгляд довольно значительная цена. Более легковесных альтернатив с AST трансформацией не видел, но для многих проектов могут подойти трансформации кода регулярками в бандлере - не так надежно, зато 15 строк вместо гигантского дерева зависимостей.
Реактивность в Mobx работает синхронно - при изменении реактивных данных сразу срабатывают реакции. Значит, если вы меняете последовательно 2 переменных store.counterOne++ и store.counterTwo++ , то реактовые компоненты перерендерятся после каждой операции. И если оба этих параметра читаются в одном компоненте - будет лишний ререндер.
Да, далеко не всегда это значительно влияет на производительность, но является очевидной точкой оптимизации, чтобы снизить количество ререндеров. Mobx дает для батчей runInAction и по дефолту пишет ворнинг в консоль, если эта оптимизация не используется - это скорее просто good practice, и вполне можно без этого писать приложения, отключая enforceActions. Я же предпочитаю всегда явно батчи делать и экономить ререндеры.
На Хабре было много дискуссий по этой теме, например в этой разбираются централизованные механизмы автобатчинга и их недостатки.
Я обычно все делаю без прибивания гвоздями к фреймворку, с возможностью SSR, и раз уж у вас Mobx - то логично было бы не использовать хуки. Довольно непросто бывает подружить иммутабельные механизмы Реакта с ручными deps и реактивность.
По селектам пример описывал здесь, но, конечно, вместо статичных options можно передавать и async getOptions , если нужно, чтобы именно компонент управлял асинхронным получением данных с бэка (хотя это далеко не всегда эффективно - если на странице много селектов, лучше подготовить данные заранее).
вот это точно - сложно оценивать области, в которых мало экспертизы. Я бэкенд писал не так много, поэтому наоборот думаю что сложность фронта выше. В него с каждым годом перетекает все больше - и "толстые клиенты" вообще по факту без традиционного бэкенда пишутся.
Firebase или похожие облачные БД с функциями и авторизацией + реалтаймовыми подписками позволяют довольно сложные проекты делать. Для эффективной работы оптимистичных апдейтов и параллельного редактирования - SQLite в wasm + Service Worker для кешей + синхронизация в бэкграунде. Роутинг, шаблонизация, реактивный стейт-менеджмент - это тоже раньше по большей части на бэке было, теперь - переехало на фронт в SPA. И оркестрация асинхронщины в JS далеко не такая простая область, если нужно управлять прерыванием или откатом длинных цепочек (например, при переходе на другие страницы), а каждая ошибка грозит "белым экраном" для пользователя. А уж сколько костылей нужно чтобы по правам пользователя включать-выключать или показывать разный функционал...
Кроссбраузерность сверху, чтобы все работало во всем многообразии устройств, а не в стабильном окружении сервера... После выкатки новых версий - бывает надо еще проапдейтить точечно код запущенный у всех клиентов во вкладках, да и синхронизировать процессы и данные между вкладками. Да и тулинг для фронта вообще не простой сейчас и в нем тоже приходится разбираться - парсинг разных форм AST, иногда что-то на Rust или Go.
Это все у меня в разных вариациях в каждом первом проекте, и просто описывать технологии можно на десятках страниц, еще и фреймворк-агностик стараюсь все делать чтобы в долгосроке не устаревало, и в архитектурном плане морочусь - схемы бывают в разы больше, чем у бэкенда из десятков микросервисов и баз (на фронте пару раз работал с "микрофронтами" от 50 штук).
А на бэке как что-то писал лет 10 назад, так оно и крутится, там все намного спокойнее и медленнее, и с каждым годом - все меньше функционала там требуется. Не говорю, что "бэк проще фронта", говорю лишь что у кого где экспертиза - там и видится "большая сложность")
В целом подход выглядит лучше, чем отдельные Enum, но не знаю, зачем вам понадобилось строго типизировать label. Если оставить его string (что в целом разумно - там может быть что угодно с учетом локализации и спецсимволов), то не придется обратно конвертировать в enum тип. Будет достаточно typeof enumConfig.Person.type[number]['value'] как string union, и в коде непосредственно писать строки вроде status: 'WORKING'. Типизация будет строгой, зато меньше нормализации и преобразований, когнитивной нагрузки в целом оберток.
Еще если вы уж выкладываете код на TypeScript, можно было бы побольше поработать над читаемостью. Сейчас читается сложно из-за разнородного нейминга - где-то сущность выносится вперед EnumConfig, где-то в середине UniqEnum, где-то есть явное определение что это тип с помощью суффикса PersonType. Дженерики в качестве динамических переменных тоже с разным неймингом: где-то однобуквенные T и K, где-то без префикса Seen, где-то с префиксом TValue.
TS-код это тоже код, и стабильные паттерны нейминга улучшают читаемость, как в js коде, где давно уже сложились best practice. В TS-коде они тоже есть - вот пример моего стиля, можно прогнать через LLM и спросить "почему именно так", ну либо сформировать свои паттерны. Такой вот лайт-код-ревью, надеюсь будет полезным)
Применяю тот же подход, но приходил к нему со стороны фронта в нескольких компаниях. Причины внедрения BFF те же самые - для ускорения загрузки информации, нужной конкретной странице; для упрощения контроля выдачи по ролям, фиче-тогглам и данным конкретной сущности; из-за хаоса бэкенд-сервисов с разными форматами запроса-ответа для однотипных данных, и другого легаси и ошибок проектирования.
Еще немаловажные фичи BFF - пререндеринг html, гибкое управление CSP и возможность более удобного тестирования. В компаниях, в которых работал, часто ресурса бэкенд-разработчиков было катастрофически мало, поэтому BFF занимались фронтендеры на node.js - и в целом хранить в одном репо и этот сервер, и код фронта, очень удобно. Можно переиспользовать схемы АПИ, графы условий показа функционала и использовать автоматические механизмы синхронизации (добавилась страница - создался бойлерплейт сервисов BFF для ее обслуживания) и проверки неиспользуемого функционала. Так контролируется рост техдолга в долгосрочной перспективе.
В статье было интересно посмотреть, как к такой же схеме приходят со стороны бэкенда.
Исследование интересное, пока не приходилось измерять частоту вызова requestAnimationFrame - а выглядит достаточно полезно.
В целом приложение не выглядит сложным по данным, их преобразованиям, по интенсивности анимаций и запросов. Я бы в первую очередь подумал над стеком - если оборудование для запуска слабое, лучше начать с точечной работы с DOM и отказаться от иммутабельных подходов. Так, я бы выбрал для этого приложения Solid.js - и уже делал такое для зарубежного аналога Лавки (тоже работает в WebView на разных платформах, в SmartTV и в холодильниках). По лонг-таскам, плавности и размеру получилось намного лучше аналогов.
Если говорить о сложных приложениях вроде реалтайм трейдинга с сотнями вебсокет-подписок, обилием преобразований данных и графиками с высокой отзывчивостью - там да, полезно мерять FPS, но я ограничивался разбиением long task на асинхронные операции с пост-синхронизацией через computed. Для дебага в целом достаточно было профайлера и что таски теперь по 12-15мс, что создавало видимость очень плавной работы интерфейса. К сожалению, не удалось построить подробный сбор метрик, только вот классическую базу собирал в Prometheus + перфоманс по большей части асинхронных операций.
Видно, что автор этого подхода действительно его выстрадал, поработав с десятком проектов на лапшекоде Redux и разочаровавшись в FSD. И в целом сделал логичный следующий шаг, решив несколько проблем легаси-проектов.
Осталась всего сотня-две таких шагов и получится что-то нормальное - без прибивания гвоздями к Реакту, хукам, с нормальной файлогенерацией и автовалидацией, со встроенным эффективным feedback loop для LLM, со стабильными SSR / Widget режимами, без 2-3мб бандла на старте и т.п. И пройдя это все и протестируя на десятках проектов с разного размера командами, окажется что Flat Architecture (я предпочитаю называть "классическая файловая структура") не так уж и плоха, если скомпоновать в формализованные слои.
Но как шаг для борьбы с легаси в условном 2018 году выглядит отлично. Как старт для новых проектов - очень сомнительно.
Первоначально идею я описывал в этой статье. Там просто пример болей в крупных проектах и как можно снизить количество ручной работы, используя простые скрипты и встраивая их в процесс бандлинга.
В этом личном репо (не OSS, просто собираю там промежуточные варианты библиотек, которые писал не под NDA) - более современная версия тех скриптов.
А в реальности использую намного более мощный генератор, с парсингом TS AST, построением resolved reversed dependencies, валидацией формата экспортов и нейминга, детальными логами и очень оптимизированной инкрементальной сборкой. У него уже другое позиционирование - не просто хелперы, помогающие не писать бойлерплейт, а концепт "ядро приложения - не бандлер или фреймворк, а файлогенератор, а все остальное легко подключается адаптерами". Невольно говорю как GPT, но оборот "не просто, а..." все-таки иногда реально подходит к мысли)
Постепенно готовлю его к OSS, но это часть большой стратегии вывода моих библиотек. Начал с Reactive Route роутера, как чистой реактивной стейт-машины для перехода в разные состояния рантайма (с сайд-эффектом в виде обновления URL для восстановления при перезагрузки страницы). Сейчас работаю над выводом конвертера TS моделей в реалтаймовые валидаторы, потом уже сам генератор выведу. Работа это огромная, нужно много примеров для разных архитектур и "папочных методологий", качественная документация. Роутер выводил полгода, так что думаю с генератором будет та же история... Но статью сделаю, думаю будет интересно.
Согласен с критикой FSD - максимально широко трактуемый нейминг и при этом жесткий догматизм в плане изоляции часто стреляют в ногу. И то, что держится на соглашениях в команде, плохо переносится из проекта в проект. Особенно странно что это называется "архитектура" и считается, что изоляция и связанность логики в пределах папки - это ключевая проблема, и ее решение уменьшит бардак. Уменьшит, но добавит новый - идеального в нашем мире нет.
Я пока пришел к тому, что архитектура - это сериализуемый контракт для рантайма, при этом можно его сделать на 100% соответствующим файловой структуре. Если в проекте нельзя автоматически вывести архитектуру - значит ее скорее нет, а есть устные соглашения и ситуативные практики, которые разваливаются в долгосроке.
У меня подход - нечто вроде смеси “классическая структура” + “горизонтальные слои” + “соответствие папок и названий файлов рантайму почти на 100%”. При этом рантайм склеивается файлогенератором, что дает и удобные переходы в IDE, и обеспечивает изоляцию и отсутствие размытия. Условно это
И нейминг и раскладка тут вообще не главное - можно и FSD, и DDD и т.п. формализовывать, главное - что это делается автоматически без участия человека, достаточно 1 раз настроить плагины и правила в файлогенераторе. Он же проверяет и нейминг (что каждый store экспортирует export class [fileName], каждый экшен - export function [fileName] и т.п.). Ну и сверху на это линтинг с проверкой import boundaries. А вот полный анализ графа импортов с резолвингом пока считаю бесполезным.
Так можно по PR diff понять, что изменилось в архитектуре, проверить дубляж, организовывать эффективнее слои и нейминг, зоны ответственности и т.п. - главное, что это материализовано и точно соответствует реальному рантайму. Уже много лет это использую, причем в разных неймингах и “папочных методологиях” - намного важнее прозрачность и управляемость, чем “что класть в features”.
Да, без TypeScript конечно тяжело проектам - получается неоткуда взять информацию о структурах, кроме ручных JSDoc. С TS AST конечно намного больше возможностей по автоматической генерации валидаторов
Entity Registry — плоский реестр сущностей, который ... обеспечивает точечный ре-рендер только тех компонентов, чьи данные действительно изменились
Судя по задаче - нужно было оптимизировать ререндер, а нормализация в маппер по id - это лишь способ решения для иммутабельных подходов. С Mobx это вообще излишне - можно класть в стор сырой ответ от апи и напрямую его перебирать вообще без "слоев нормализации".
Тут UserItem обновится точечно при изменении данных конкретного пользователя, и можно напрямую мутировать сущности (но лучше конечно через персистентное хранилище в виде БД бэка или LS, зависит от требований к функционалу).
Может быть я неправильно понял вопрос и не на то ответил - я говорил как “вернуть состояние назад” в beforeEnter, но возможно имелось в виду именно предотвращение в beforeLeave. Это пока не стабилизированный функционал, к сожалению:
браузер поменял URL, вызвался redirect+lifecycle
beforeLeave говорит preventRedirect(), state остается старым, URL в браузере - новым (что неправильно)
Сделаю доработку в библиотеке и добавлю тестов на эти кейсы, чтобы URL менялся обратно, вроде такого механизма
if (reason === ‘unmodified’) {
if (source === ‘popstate’ && actualUrl !== currentUrl) {
win?.history.replaceState(null, ‘’, currentUrl);
}
}
Подумаю над этим. Довольно мало внимания уделил синхронизации с браузерными асинхронными переходами, больше стабильности состояния и пайплайну мульти-редиректов. Причем в коммерческих версиях роутера это было, сам не знаю зачем удалил)
P.S. Скорее всего из-за ограниченного доступа к History браузера - что нет полной манипуляции стеком истории, а только push в конец и replace текущего. Поэтому стабильного preventRedirect с "пост-фактум popstate" не получалось, и оставил половинчатое решение на будущее.
К сожалению или к счастью, сайты не могут блокировать браузерные кнопки вперед-назад и popstate соответственно) Поэтому нельзя “предотвратить переход, если нажал Назад в браузере”. Вот как сделано в роутере:
То есть жмешь “Назад”, запускается стандартный redirect, то есть сработает beforeEnter соответствующего конфига. И в нем можно проверить - откуда пришел
dashboard: {
path: '/dashboard',
loader: () => import('./pages/dashboard'),
async beforeEnter({ redirect, currentState }) {
// Пользователь находился на странице авторизации,
// нажал кнопку "назад" и оказался на странице дашборда.
// запретить браузеру менять свой URL мы не можем,
// но можем явно проверить этот кейс и вернуть обратно на авторизацию
if (currentState?.name === 'login') {
return redirect(currentState);
}
}
}
Рассинхрон будет в любом случае, каким бы роутером ни пользовались - браузер меняет URL (из js это не достать), посылает событие popstate (js это уже может поймать, вытянуть location.href и запустить цикл валидации и редиректов), затем в данном случае роутер заредиректит обратно. Но на состоянии приложения это не скажется - оно будет консистентным и источником правды для рендеринга.
Если говорить другими словами - каждая вкладка браузера это безопасный sandbox без root-доступа к API браузера из js. И popstate для роутера - полноценный заход на сайт как если бы было по прямой ссылке, но с доступом к предыдущему состоянию currentState, которое можно восстановить (эмуляция доступа к истории браузера).
Да, там делаю очень замороченный TS AST парсер с поддержкой всего, что может быть в Open API, включая реэкспорты, алиасы, построение инверсированных зависимостей (дерева импортов). Конкретно для этого кейса будет такой валидатор
Zod - инструмент с широким назначением, может и формы валидировать, и проверять данные на email, длину, маски, использоваться на бэке и в пайплайнах нормализации.
Но на фронте задача другая - если использовать TS, то важна именно совместимость по типам - что пришла строка, а не число (и неважно - это email или isoDate), так обеспечивается работоспособность рантайма. А проверки на email в основном нужны бэку при сохранении данных (ну и для форм если надо - можно zod или аналог прикрутить).
При этом намного удобнее не полностью Swagger Open API переносить в проект, а детально заводить типы - если фронту в TypeUser нужно только 3 поля, а не 800 присылаемых бэком с relationships, ios / android / tv спецификой, то лучше так и завести. Это же потом и стабильность улучшит - не будет ломаться при изменении неиспользуемых полей или ошибок в них, фронтендер всегда может сказать "мы используем 3 поля, проверьте не устарела ли выдача из 800 полей", трафика меньше, тестировать проще, в сторы фронта не протекает лишнего и т.п. Думаю любой найдет много причин держать код и рантайм чистыми, а не "тащить все что дают".
Не понимаю вопрос - в конце первого комментария я описал схему работы и дал ссылку на пример кода.
Главное - успейте вовремя остановиться на текущем пути) Если поверх сгенерированного по спеке апи-клиента еще прикручивать автоматические валидаторы типа Zod, то одна из проблем формально решится - то, что бэк реально отдает, начнет валидироваться. Но добавятся новые - фронт будет логировать ошибки или падать при изменении неиспользуемых полей. По факту фронт становится "валидатором, что бэк правильно сгенерировал спеку" - это вообще путь не туда. Я потратил пару лет на этот путь, и продвинулся довольно далеко в энтерпрайзах - но проблемы будут множиться, инфраструктура становиться все сложнее, а по времени и ресурсам бизнесу это все куда дороже, чем даже вручную править.
Решение этого - не в проверке соответствия бэкенда им же написанной спеке. Просто предостерегаю от излишнего погружения в такую схему - лучше не делать дефолтной для проектов, есть подходы эффективнее.
Добро пожаловать на Хабр.
Идея генерировать апи-клиент с типизацией из openapi стара, и ее недостатки достаточно изучены, чтобы сказать - это скорее нишевая схема, в большинстве проектов добавляет больше неразберихи и проблем, чем пользы.
Часть проблем вы уже нашли сами, с другой частью еще столкнетесь:
схема в реальности может расходиться с фактически присланными данными (по моему опыту - частая история), то есть гарантии мы не получаем
проблемы с версионированием - если скажем 10 разработчиков бэк+фронт работают над разными ручками в рамках своих задач, нужна сложная инфраструктура чтобы удобно разрабатывать. Это отдельное пространство для проблем, которые можно "героически решать", например публикуя с yaml или с готовым апи-клиентом пакеты во внутренние репозитории, генерируя в день сотни
"@api-spec": "123.3.1877-feature-123", синхронизируя со стендами и деплойными циклами, занимаясь сложными слияниями в dev. Ну либо перекидываться в рабочих чатах yaml файлами, а потом на dev стенде после слияния нескольких фич ловить что или фронт или бэк вмерджили что-то не выложенное на стенд или ушли в глубокий конфликт.фронту может быть нужен 1 параметр в конкретной ручке, а бэк присылает 100 (возможно - легаси, или для разных приложений-потребителей). Сгенерированный апи-клиент не очистит лишнее, ему все кажется важным и нужным.
в ряде кейсов теряется параллельность разработки бэка и фронта. В задаче согласовали переименование параметра в ручке - фронт не может у себя поменять, проверить, доработать типы и связанный код. Он должен дождаться пока бэк либо сделает задачу, либо сделает заглушки -> генерируется openapi -> генерируется апи-клиент -> можно начинать работу. Либо костылить и реплейсить вручную части сгенерированного апи-клиента.
большинство cli-утилит генерации заточены под единый огромный инстанс апи-клиента. Сложно будет сделать lazy loading, разбиение на чанки, модульность (чтобы апи лежало ближе к местам использования), не потеряв единый процесс фетчинга и не наплодив дубляж.
велик риск что в апи-клиент попадут неиспользуемые ручки, и в целом анализ по проекту "какие именно ручки и какие именно поля фронт использует" будет затруднен. Бандл неизбежно раздувается, на Хабр летят статьи "почему вкладки браузера тормозят и едят столько памяти".
логирование несовпавших ожиданий все равно требуется - то есть необходимо где-то хранить "что именно нужно фронту для работы", и делать дифф со сгенерированным апи-клиентом.
Ну, о недостатках можно говорить очень долго, суть - "сгенерированный апи-клиент по openapi может гарантировать, что он соответствует openapi, но абсолютно не гарантирует что он соответствует конкретному фронту-потребителю".
Конечно, есть кейсы, где это все подходит на каком-то из этапов развития проекта (особенно если количество фронтендеров === 1 и проект маленький), но он скорее тупиковый.
Как не сталкиваться с этими проблемами? Воткнуть генерацию в правильное место - не в то, чтобы создавать js-файл "что примерно отдает бэк", а в то, чтобы "производилась валидация ожиданий между тем, что нужно фронту и что реально отдает бэк". Написать руками DTO для ручек только с теми полями, которые нужны конкретному фронту-потребителю, а дальше уже подключать генерацию, как примерно описывал тут.
Концепт автооборачивания действительно улучшает DX, но конкретно этот пакет тянет babel/core и его 15 зависимостей на мегабайты, что на мой взгляд довольно значительная цена. Более легковесных альтернатив с AST трансформацией не видел, но для многих проектов могут подойти трансформации кода регулярками в бандлере - не так надежно, зато 15 строк вместо гигантского дерева зависимостей.
Реактивность в Mobx работает синхронно - при изменении реактивных данных сразу срабатывают реакции. Значит, если вы меняете последовательно 2 переменных store.counterOne++ и store.counterTwo++ , то реактовые компоненты перерендерятся после каждой операции. И если оба этих параметра читаются в одном компоненте - будет лишний ререндер.
Да, далеко не всегда это значительно влияет на производительность, но является очевидной точкой оптимизации, чтобы снизить количество ререндеров. Mobx дает для батчей runInAction и по дефолту пишет ворнинг в консоль, если эта оптимизация не используется - это скорее просто good practice, и вполне можно без этого писать приложения, отключая enforceActions. Я же предпочитаю всегда явно батчи делать и экономить ререндеры.
На Хабре было много дискуссий по этой теме, например в этой разбираются централизованные механизмы автобатчинга и их недостатки.
Я обычно все делаю без прибивания гвоздями к фреймворку, с возможностью SSR, и раз уж у вас Mobx - то логично было бы не использовать хуки. Довольно непросто бывает подружить иммутабельные механизмы Реакта с ручными deps и реактивность.
По селектам пример описывал здесь, но, конечно, вместо статичных options можно передавать и
async getOptions, если нужно, чтобы именно компонент управлял асинхронным получением данных с бэка (хотя это далеко не всегда эффективно - если на странице много селектов, лучше подготовить данные заранее).вот это точно - сложно оценивать области, в которых мало экспертизы. Я бэкенд писал не так много, поэтому наоборот думаю что сложность фронта выше. В него с каждым годом перетекает все больше - и "толстые клиенты" вообще по факту без традиционного бэкенда пишутся.
Firebase или похожие облачные БД с функциями и авторизацией + реалтаймовыми подписками позволяют довольно сложные проекты делать. Для эффективной работы оптимистичных апдейтов и параллельного редактирования - SQLite в wasm + Service Worker для кешей + синхронизация в бэкграунде. Роутинг, шаблонизация, реактивный стейт-менеджмент - это тоже раньше по большей части на бэке было, теперь - переехало на фронт в SPA. И оркестрация асинхронщины в JS далеко не такая простая область, если нужно управлять прерыванием или откатом длинных цепочек (например, при переходе на другие страницы), а каждая ошибка грозит "белым экраном" для пользователя. А уж сколько костылей нужно чтобы по правам пользователя включать-выключать или показывать разный функционал...
Кроссбраузерность сверху, чтобы все работало во всем многообразии устройств, а не в стабильном окружении сервера... После выкатки новых версий - бывает надо еще проапдейтить точечно код запущенный у всех клиентов во вкладках, да и синхронизировать процессы и данные между вкладками. Да и тулинг для фронта вообще не простой сейчас и в нем тоже приходится разбираться - парсинг разных форм AST, иногда что-то на Rust или Go.
Это все у меня в разных вариациях в каждом первом проекте, и просто описывать технологии можно на десятках страниц, еще и фреймворк-агностик стараюсь все делать чтобы в долгосроке не устаревало, и в архитектурном плане морочусь - схемы бывают в разы больше, чем у бэкенда из десятков микросервисов и баз (на фронте пару раз работал с "микрофронтами" от 50 штук).
А на бэке как что-то писал лет 10 назад, так оно и крутится, там все намного спокойнее и медленнее, и с каждым годом - все меньше функционала там требуется. Не говорю, что "бэк проще фронта", говорю лишь что у кого где экспертиза - там и видится "большая сложность")
В целом подход выглядит лучше, чем отдельные Enum, но не знаю, зачем вам понадобилось строго типизировать label. Если оставить его string (что в целом разумно - там может быть что угодно с учетом локализации и спецсимволов), то не придется обратно конвертировать в enum тип. Будет достаточно
typeof enumConfig.Person.type[number]['value']как string union, и в коде непосредственно писать строки вродеstatus: 'WORKING'. Типизация будет строгой, зато меньше нормализации и преобразований, когнитивной нагрузки в целом оберток.Еще если вы уж выкладываете код на TypeScript, можно было бы побольше поработать над читаемостью. Сейчас читается сложно из-за разнородного нейминга - где-то сущность выносится вперед EnumConfig, где-то в середине UniqEnum, где-то есть явное определение что это тип с помощью суффикса PersonType. Дженерики в качестве динамических переменных тоже с разным неймингом: где-то однобуквенные T и K, где-то без префикса Seen, где-то с префиксом TValue.
TS-код это тоже код, и стабильные паттерны нейминга улучшают читаемость, как в js коде, где давно уже сложились best practice. В TS-коде они тоже есть - вот пример моего стиля, можно прогнать через LLM и спросить "почему именно так", ну либо сформировать свои паттерны. Такой вот лайт-код-ревью, надеюсь будет полезным)
Применяю тот же подход, но приходил к нему со стороны фронта в нескольких компаниях. Причины внедрения BFF те же самые - для ускорения загрузки информации, нужной конкретной странице; для упрощения контроля выдачи по ролям, фиче-тогглам и данным конкретной сущности; из-за хаоса бэкенд-сервисов с разными форматами запроса-ответа для однотипных данных, и другого легаси и ошибок проектирования.
Еще немаловажные фичи BFF - пререндеринг html, гибкое управление CSP и возможность более удобного тестирования. В компаниях, в которых работал, часто ресурса бэкенд-разработчиков было катастрофически мало, поэтому BFF занимались фронтендеры на node.js - и в целом хранить в одном репо и этот сервер, и код фронта, очень удобно. Можно переиспользовать схемы АПИ, графы условий показа функционала и использовать автоматические механизмы синхронизации (добавилась страница - создался бойлерплейт сервисов BFF для ее обслуживания) и проверки неиспользуемого функционала. Так контролируется рост техдолга в долгосрочной перспективе.
В статье было интересно посмотреть, как к такой же схеме приходят со стороны бэкенда.
Исследование интересное, пока не приходилось измерять частоту вызова requestAnimationFrame - а выглядит достаточно полезно.
В целом приложение не выглядит сложным по данным, их преобразованиям, по интенсивности анимаций и запросов. Я бы в первую очередь подумал над стеком - если оборудование для запуска слабое, лучше начать с точечной работы с DOM и отказаться от иммутабельных подходов. Так, я бы выбрал для этого приложения Solid.js - и уже делал такое для зарубежного аналога Лавки (тоже работает в WebView на разных платформах, в SmartTV и в холодильниках). По лонг-таскам, плавности и размеру получилось намного лучше аналогов.
Если говорить о сложных приложениях вроде реалтайм трейдинга с сотнями вебсокет-подписок, обилием преобразований данных и графиками с высокой отзывчивостью - там да, полезно мерять FPS, но я ограничивался разбиением long task на асинхронные операции с пост-синхронизацией через computed. Для дебага в целом достаточно было профайлера и что таски теперь по 12-15мс, что создавало видимость очень плавной работы интерфейса. К сожалению, не удалось построить подробный сбор метрик, только вот классическую базу собирал в Prometheus + перфоманс по большей части асинхронных операций.
Видно, что автор этого подхода действительно его выстрадал, поработав с десятком проектов на лапшекоде Redux и разочаровавшись в FSD. И в целом сделал логичный следующий шаг, решив несколько проблем легаси-проектов.
Осталась всего сотня-две таких шагов и получится что-то нормальное - без прибивания гвоздями к Реакту, хукам, с нормальной файлогенерацией и автовалидацией, со встроенным эффективным feedback loop для LLM, со стабильными SSR / Widget режимами, без 2-3мб бандла на старте и т.п. И пройдя это все и протестируя на десятках проектов с разного размера командами, окажется что Flat Architecture (я предпочитаю называть "классическая файловая структура") не так уж и плоха, если скомпоновать в формализованные слои.
Но как шаг для борьбы с легаси в условном 2018 году выглядит отлично. Как старт для новых проектов - очень сомнительно.
Первоначально идею я описывал в этой статье. Там просто пример болей в крупных проектах и как можно снизить количество ручной работы, используя простые скрипты и встраивая их в процесс бандлинга.
В этом личном репо (не OSS, просто собираю там промежуточные варианты библиотек, которые писал не под NDA) - более современная версия тех скриптов.
А в реальности использую намного более мощный генератор, с парсингом TS AST, построением resolved reversed dependencies, валидацией формата экспортов и нейминга, детальными логами и очень оптимизированной инкрементальной сборкой. У него уже другое позиционирование - не просто хелперы, помогающие не писать бойлерплейт, а концепт "ядро приложения - не бандлер или фреймворк, а файлогенератор, а все остальное легко подключается адаптерами". Невольно говорю как GPT, но оборот "не просто, а..." все-таки иногда реально подходит к мысли)
Постепенно готовлю его к OSS, но это часть большой стратегии вывода моих библиотек. Начал с Reactive Route роутера, как чистой реактивной стейт-машины для перехода в разные состояния рантайма (с сайд-эффектом в виде обновления URL для восстановления при перезагрузки страницы). Сейчас работаю над выводом конвертера TS моделей в реалтаймовые валидаторы, потом уже сам генератор выведу. Работа это огромная, нужно много примеров для разных архитектур и "папочных методологий", качественная документация. Роутер выводил полгода, так что думаю с генератором будет та же история... Но статью сделаю, думаю будет интересно.
Согласен с критикой FSD - максимально широко трактуемый нейминг и при этом жесткий догматизм в плане изоляции часто стреляют в ногу. И то, что держится на соглашениях в команде, плохо переносится из проекта в проект. Особенно странно что это называется "архитектура" и считается, что изоляция и связанность логики в пределах папки - это ключевая проблема, и ее решение уменьшит бардак. Уменьшит, но добавит новый - идеального в нашем мире нет.
Я пока пришел к тому, что архитектура - это сериализуемый контракт для рантайма, при этом можно его сделать на 100% соответствующим файловой структуре. Если в проекте нельзя автоматически вывести архитектуру - значит ее скорее нет, а есть устные соглашения и ситуативные практики, которые разваливаются в долгосроке.
У меня подход - нечто вроде смеси “классическая структура” + “горизонтальные слои” + “соответствие папок и названий файлов рантайму почти на 100%”. При этом рантайм склеивается файлогенератором, что дает и удобные переходы в IDE, и обеспечивает изоляцию и отсутствие размытия. Условно это
И нейминг и раскладка тут вообще не главное - можно и FSD, и DDD и т.п. формализовывать, главное - что это делается автоматически без участия человека, достаточно 1 раз настроить плагины и правила в файлогенераторе. Он же проверяет и нейминг (что каждый store экспортирует export class [fileName], каждый экшен - export function [fileName] и т.п.). Ну и сверху на это линтинг с проверкой import boundaries. А вот полный анализ графа импортов с резолвингом пока считаю бесполезным.
Так можно по PR diff понять, что изменилось в архитектуре, проверить дубляж, организовывать эффективнее слои и нейминг, зоны ответственности и т.п. - главное, что это материализовано и точно соответствует реальному рантайму. Уже много лет это использую, причем в разных неймингах и “папочных методологиях” - намного важнее прозрачность и управляемость, чем “что класть в features”.
Посмотрел репо - там все основано на схемах, а не на исходных типах. Для фронтенда такое избыточно при работе по апи
Да, без TypeScript конечно тяжело проектам - получается неоткуда взять информацию о структурах, кроме ручных JSDoc. С TS AST конечно намного больше возможностей по автоматической генерации валидаторов
Судя по задаче - нужно было оптимизировать ререндер, а нормализация в маппер по id - это лишь способ решения для иммутабельных подходов. С Mobx это вообще излишне - можно класть в стор сырой ответ от апи и напрямую его перебирать вообще без "слоев нормализации".
Тут UserItem обновится точечно при изменении данных конкретного пользователя, и можно напрямую мутировать сущности (но лучше конечно через персистентное хранилище в виде БД бэка или LS, зависит от требований к функционалу).
Может быть я неправильно понял вопрос и не на то ответил - я говорил как “вернуть состояние назад” в beforeEnter, но возможно имелось в виду именно предотвращение в beforeLeave. Это пока не стабилизированный функционал, к сожалению:
браузер поменял URL, вызвался redirect+lifecycle
beforeLeave говорит preventRedirect(), state остается старым, URL в браузере - новым (что неправильно)
Сделаю доработку в библиотеке и добавлю тестов на эти кейсы, чтобы URL менялся обратно, вроде такого механизма
Подумаю над этим. Довольно мало внимания уделил синхронизации с браузерными асинхронными переходами, больше стабильности состояния и пайплайну мульти-редиректов. Причем в коммерческих версиях роутера это было, сам не знаю зачем удалил)
P.S. Скорее всего из-за ограниченного доступа к History браузера - что нет полной манипуляции стеком истории, а только push в конец и replace текущего. Поэтому стабильного preventRedirect с "пост-фактум popstate" не получалось, и оставил половинчатое решение на будущее.
К сожалению или к счастью, сайты не могут блокировать браузерные кнопки вперед-назад и popstate соответственно) Поэтому нельзя “предотвратить переход, если нажал Назад в браузере”. Вот как сделано в роутере:
То есть жмешь “Назад”, запускается стандартный redirect, то есть сработает beforeEnter соответствующего конфига. И в нем можно проверить - откуда пришел
Рассинхрон будет в любом случае, каким бы роутером ни пользовались - браузер меняет URL (из js это не достать), посылает событие popstate (js это уже может поймать, вытянуть location.href и запустить цикл валидации и редиректов), затем в данном случае роутер заредиректит обратно. Но на состоянии приложения это не скажется - оно будет консистентным и источником правды для рендеринга.
Если говорить другими словами - каждая вкладка браузера это безопасный sandbox без root-доступа к API браузера из js. И popstate для роутера - полноценный заход на сайт как если бы было по прямой ссылке, но с доступом к предыдущему состоянию currentState, которое можно восстановить (эмуляция доступа к истории браузера).
Да, там делаю очень замороченный TS AST парсер с поддержкой всего, что может быть в Open API, включая реэкспорты, алиасы, построение инверсированных зависимостей (дерева импортов). Конкретно для этого кейса будет такой валидатор
И если бэк пришлет
{ gender: 'M', salary: '100' }то будет ошибка response.user is none of 2 types. Для дебага должно быть достаточно.Ну и в любом случае это лучше, чем ловить где-то при открытии модалки undefined has no method toFixed in user.salary.toFixed()
Zod - инструмент с широким назначением, может и формы валидировать, и проверять данные на email, длину, маски, использоваться на бэке и в пайплайнах нормализации.
Но на фронте задача другая - если использовать TS, то важна именно совместимость по типам - что пришла строка, а не число (и неважно - это email или isoDate), так обеспечивается работоспособность рантайма. А проверки на email в основном нужны бэку при сохранении данных (ну и для форм если надо - можно zod или аналог прикрутить).
При этом намного удобнее не полностью Swagger Open API переносить в проект, а детально заводить типы - если фронту в TypeUser нужно только 3 поля, а не 800 присылаемых бэком с relationships, ios / android / tv спецификой, то лучше так и завести. Это же потом и стабильность улучшит - не будет ломаться при изменении неиспользуемых полей или ошибок в них, фронтендер всегда может сказать "мы используем 3 поля, проверьте не устарела ли выдача из 800 полей", трафика меньше, тестировать проще, в сторы фронта не протекает лишнего и т.п. Думаю любой найдет много причин держать код и рантайм чистыми, а не "тащить все что дают".