Обновить
53
Дмитрий Казаков@DmitryKazakov8

Front-end архитектор

45
Подписчики
Отправить сообщение

Не понимаю вопрос - в конце первого комментария я описал схему работы и дал ссылку на пример кода.

Главное - успейте вовремя остановиться на текущем пути) Если поверх сгенерированного по спеке апи-клиента еще прикручивать автоматические валидаторы типа 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, и обеспечивает изоляцию и отсутствие размытия. Условно это

type ArchitectureShape = {
   api;
   store;
   actions;
}

type AppRuntime implements ArchitectureShape = {
   api;
   store;
   actions;
   pages: { [pageName]: ArchitectureShape };
   router.state: { [routeName]: { name, params, query } }
}

type RouteNameToPageName = { [routeName]: pageName }; 

type PageNameToRouteName = { [pageName]: Array<routeName> }; 

И нейминг и раскладка тут вообще не главное - можно и FSD, и DDD и т.п. формализовывать, главное - что это делается автоматически без участия человека, достаточно 1 раз настроить плагины и правила в файлогенераторе. Он же проверяет и нейминг (что каждый store экспортирует export class [fileName], каждый экшен - export function [fileName] и т.п.). Ну и сверху на это линтинг с проверкой import boundaries. А вот полный анализ графа импортов с резолвингом пока считаю бесполезным.

Так можно по PR diff понять, что изменилось в архитектуре, проверить дубляж, организовывать эффективнее слои и нейминг, зоны ответственности и т.п. - главное, что это материализовано и точно соответствует реальному рантайму. Уже много лет это использую, причем в разных неймингах и “папочных методологиях” - намного важнее прозрачность и управляемость, чем “что класть в features”.

Посмотрел репо - там все основано на схемах, а не на исходных типах. Для фронтенда такое избыточно при работе по апи

Да, без TypeScript конечно тяжело проектам - получается неоткуда взять информацию о структурах, кроме ручных JSDoc. С TS AST конечно намного больше возможностей по автоматической генерации валидаторов

Entity Registry — плоский реестр сущностей, который ... обеспечивает точечный ре-рендер только тех компонентов, чьи данные действительно изменились

Судя по задаче - нужно было оптимизировать ререндер, а нормализация в маппер по id - это лишь способ решения для иммутабельных подходов. С Mobx это вообще излишне - можно класть в стор сырой ответ от апи и напрямую его перебирать вообще без "слоев нормализации".

const UserList = observer(({ userIds }: { userIds: Set<UserId> }) => {
  const filteredUsers = userStore.usersDTO
    .filter(({ id }) => userIds.has(id)); 

  return (
    <ul>
      {filteredUsers.map((user) => <UserItem key={user.id} user={user} />)}
    </ul>
  );
});

Тут 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 соответственно) Поэтому нельзя “предотвратить переход, если нажал Назад в браузере”. Вот как сделано в роутере:

listener() {
  void this.redirect({ ...this.urlToState(location.href), replace: true });
},
historySyncStart() {
  win?.addEventListener('popstate', this.listener);
},

То есть жмешь “Назад”, запускается стандартный 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, включая реэкспорты, алиасы, построение инверсированных зависимостей (дерева импортов). Конкретно для этого кейса будет такой валидатор

user: [
  'union',
  { gender: ['literal', 'F'], childrenCount: 'number' },
  { gender: ['literal', 'M'], salary: 'number' }
]

И если бэк пришлет { 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 полей", трафика меньше, тестировать проще, в сторы фронта не протекает лишнего и т.п. Думаю любой найдет много причин держать код и рантайм чистыми, а не "тащить все что дают".

1
23 ...

Информация

В рейтинге
6 076-й
Зарегистрирован
Активность