Если вы не используете монорепозиторий, то взаимодействие между клиентом и сервером с общей моделью данных будет проблемой. Без обслуживания дублирование кода приведет к рассинхронизации.

Если модель данных изменяется на серверной части - убедитесь, что клиентский код подхватит эти изменения. Иначе клиентский код сильно пострадает при получении несовместимых данных, либо из-за игнорирования дополнений. А это не то, чего вы хотите.

Давайте рассмотрим стратегии для синхронизации клиентского и серверного кода.

Извлечение общих моделей в сторонние зависимости

Предположим, вы работаете с несколькими репозиториями. После определения модели данных (набора моделей данных), которую использует сервер и его клиенты (пользовательские приложения или другие сервисы), хорошей идеей будет извлечь модели в общую библиотеку.

Таким способом вы гарантируете, что сервер и клиент импортируют и используют одну и ту же модель данных.

Какие в этом плюсы? Сервер и клиент будут точно знать, что отправляют друг другу. И будут одновременно в курсе любых изменений. По сути вы выровняли игровую площадку для обеих сторон.

Замечательно.

Тогда что не так?

Предположим, что мы работаем над приложением JavaScript-Node.js. Скорее всего, придётся превратить новый репозиторий в NPM-пакет. Это не сложно, но займёт дополнительное время. И самое главное, вы создадите новый репозиторий. Но кто за него отвечает? Backend? Если требуется внести изменение в модель по одному из клиентских проектов, то как лучше это сделать? При работе в многопроектной среде с несколькими командами (распространенный сценарий), ваши действия могут стать причиной организационного кошмара.

Это решение подойдет для тех сценариев, когда одна команда работает над всем проектом и владеет всей кодовой базой.

Предупреждение: убедитесь, что выносите в сторонний модуль DTO (Data Transfer Object), который содержит только поля, используемые для передачи данных между сущностями. Поскольку DTO может быть классом, то появляется соблазн поделиться методами, которые включают приватную бизнес логику. Это недопустимо, поэтому будьте внимательны.

Одно дело - поделиться несколькими полями, а другое - поделиться бизнес логикой, которая потенциально является конфиденциальной для компании.

Совместное использование типов с помощью TypeScript и Bit

https://bit.dev/deleteman/quotes-lister

Если используете TypeScript - убедитесь, что делитесь не только «формой» данных, но и типами.

В этом примере мы будем использовать Bit вместо создания нового репозитория и превращения его в NPM-пакет.

Bit - это инструмент с открытым исходным кодом (со встроенной интеграцией и платформой удаленного хостинга Bit Cloud). Bit помогает создавать и совместно использовать независимые компоненты. Эти компоненты (модули) разрабатываются независимо, версионируются и используются совместно.

Создавайте новые независимые компоненты с нуля, либо извлекайте их постепенно из уже существующей кодовой базы (это как раз наш случай).

Этот подход похож на NPM, но есть отличия:

  • Для версионирования не нужно извлекать код вручную, делиться и работать над ним по отдельности. Можно «экспортировать» компонент из репозитория. Bit идентифицирует часть кода, как компонент и обрабатывает его независимо. Это упрощает процесс совместного использования, не требует создания отдельного репозитория и убирает необходимость переделывать способ импорта этих файлов в ваш проект.

  • Люди, которые «импортируют» ваши компоненты (а не устанавливают их), могут совместно работать с ними, изменять и экспортировать обратно в свою «удаленную область» (удаленный хостинг компонентов).

Этот подход очень эффективен, потому что даёт возможность совместного использования одного инструмента в разных командах внутри одной организации. При этом вам не требуется создавать отдельный проект.

При импорте компонента Bit код загружается и копируется в рабочий каталог. Одновременно он создает соответствующий пакет в каталоге node_modules. При изменении исходного кода (в рабочем каталоге) пакет пересоздаётся. Таким образом, вы используете этот компонент с помощью абсолютного пути. Это работает в любом контексте, и будет работать даже при установке пакета компонента без импорта.

Это наша цель - расшарить локальную настройку в окружение с несколькими репозиториями, не беспокоясь, что возникнут проблемы.

Я говорю «мульти-репозиторий» в кавычках, потому что создание и поддержка нового репозитория нас не волнует. Вы можете превратить часть базового кода в независимый компонент, версионировать его через Bit, и опубликовать в централизованном хранилище.

После этого новый компонент с общими типами можно импортировать куда угодно. Это значит, что любое клиентское приложение или микросервис будут импортировать тип как внешнюю зависимость. И нам для этого не требуется никаких манипуляций с NPM или Github.

И наконец, если вы публикуете только определения типов, то можно не беспокоиться о защите бизнес-логики.

Пример

Учитывая, насколько легко создать рабочий пример с Bit, я подготовил простой проект для демонстрации.

Проект называется «Quote Lister», можете увидеть его здесь или прямо на Github. Логика очень простая:

  • Сервис в Node.js, раздаёт список цитат, взятых из текстового файла.

  • Компонент React, запрашивает эти цитаты и выводит их на экран.

Как вы уже догадались, для описания структуры цитат, оба компонента могут использовать общий тип. Создаем новый компонент, который экспортирует только общие типы. У меня будет и служба и компонент реакции, импортирующий его.

Детали реализации проекта для Backend и Frontend фактически не имеют значения. Просто посмотрите на скриншот с самыми интересными частями клиента и сервера.

Frontend
Frontend
Backend
Backend

Здесь приведены компонент на React и серверная часть на Node.js. Это разные кодовые базы, но они устанавливают одну и ту же библиотеку: @deleteman/quotes-lister.shared-types

И вот что интересно: это не внешняя библиотека, но ее можно так использовать благодаря магии Bit.

Посмотрим на структуру проекта:

Структура проекта
Структура проекта

Все это локально и все компоненты управляются Bit, а ссылки на них формируются автоматически из папки node_modules под моим именем пользователя Bit (@deleteman). Таким образом проекты используют общую библиотеку, а мы не беспокоимся о публикации в NPM и поддержке другого репозитория. Если бы мы работали только с одним или двумя проектами и код был более общим, то вы могли бы просто установить их через Bit или NPM по отдельности.

Так выглядит созданный модуль на платформе Bit
Так выглядит созданный модуль на платформе Bit

Паттерн BFF (Backend for Frontend)

При синхронизации клиентского и серверного кода мы будем расшаривать этот код. Эту задачу можно упростить, используя паттерн BFF. Правда и у этого решения есть свои минусы.

Паттерн BFF - это способ создать общий интерфейс между сервером и клиентом без совместного использования кода между проектами. Как это сделать? Создайте прокси-сервис, который будет действовать как маппер между обеими моделями данных.

Так мы избежим проблем при совместном использовании внешних зависимостей или типов между клиентом и сервером. А для решения проблемы стандартизации можем добавить дополнительные сервисы.

Это хорошее решение? Все зависит от обстоятельств. Если у вас один клиент, то можно использовать этот паттерн, чтобы предоставить обеим сторонам больше гибкости в вопросе определения типов данных. 

С другой стороны если у вас несколько клиентов, то такой подход поможет изменить версии сервисов с минимальным влиянием на клиентов. Вы добавляете прокси и обновляете версию сервиса, сохраняя обратную совместимость. Прокси можно использовать для сопоставления изменений данных в новой версии. Сначала протестируйте новую версию сервиса и убедитесь, что она работает. Затем вы можете обратиться к клиентам, чтобы они обновили код.

С помощью паттерна BFF вы можете синхронизировать код API и код клиента, оставляя их не синхронизированными.

Риск - расшарить слишком много

Наконец совет относительно проблемы, которую мы пытаемся решить.

Если придерживаться принципа DRY, то при синхронизации кода клиента и сервера возникает две основные проблемы:

  1. Возможно, вы делитесь бóльшим количеством данными, чем следует. Мы это уже обсуждали: вы делитесь всем классом и его методами, вместо того, чтобы поделиться базовым определением данных (названия и типы полей). Расшаривать детали имеет смысл в случае, если логику методов можно использовать в обоих местах. Иначе вы раскрываете клиенту приватную бизнес-логику. А если клиент - внешняя сторона (а не другое приложение в вашей организации), вы передаете IP адрес своей компании. Мне нужно продолжать?

  2. Чем больше кода вы расшариваете, тем больше возникает взаимосвязей между системами. Это нелогично, но все усилия, которые прилагаете, чтобы поделиться кодом и удержаться от копирования и вставки файлов между проектами, заставляют связать сервер и реализацию клиента. Помните: после имплементации первой версии проекта в ту минуту, когда серверу требуется внести изменения в модель, вам нужно убедиться, что все клиенты обновлены.

Как избежать подобных проблем? Первый и простой способ: воздержитесь от совместного использования кода, как части моделей данных. Если есть общий код, которым нужно поделиться, сделайте это в другом месте.

Что касается второй проблемы - решения просто нет. Вопрос скорее в принятии: учитывая ситуацию, можно ли считать проблемой связь между клиентом и сервером? От вашего ответа зависит, что делать дальше.


Наличие одной модели данных между клиентом и сервером - распространенная ситуация. И нет правильного ответа, как к ней подойти. Как-то расшарить код, скопировать и вставить файлы или создать еще один сервис для их маппинга.

Выбор лучшего решения зависит только от вас и вашего контекста.


Для вас перевёл статью Никита Ульшин, Team Lead & JS-разработчик, веду блог ulshin.me и ТГ-канал @ulshinblog.

Комментарии, пожелания и конструктивная критика приветствуются :)

Комментарии (8)


  1. hwb
    06.11.2021 09:42
    +9

    Интересно, чем автору не угодил подход с использованием OpenAPI? Неужели настолько интереснее городить троллейбус из буханки, используя bit или что-то подобное? )

    И предложение использовать BFF для решения проблемы - тоже, мягко говоря, достаточно странное: BFF - он, как бы, вообще не про это. Да и решением это трудно назвать: имели одну проблему - синхронизацию типов api фронта и бэка, получили две проблемы - синхронизацию типов api фронтенд-bff и bff-бэкенд.


    1. nikitaulshin Автор
      08.11.2021 10:14
      +1

      Мне тоже такой подход кажется оверхедом. В микросервисах на NodeJS имхо намного проще пакет выпустить и поддерживать, чем тянуть стороннего провайдера.

      С другой стороны - было интересно рассмотреть альтернативный способ, пусть и немного наркоманский :)


    1. pinkskin
      08.11.2021 10:14
      +1

      Альтернативой может быть graphql


      1. nikitaulshin Автор
        08.11.2021 10:15

        Не во все проекты его можно воткнуть, но да


  1. noodles
    06.11.2021 17:54
    +2

    Иначе клиентский код сильно пострадает при получении несовместимых данных

    Если не доверять бекенду (как и пользовательскому вводу) - то не пострадает.

    Затем вы можете обратиться к клиентам, чтобы они обновили код.

    Этот шаг (шаг оповещения) присутствует всегда в том или ином виде, независимо от способов "шаринга типов/моделей". И также всегда присутствует последующий шаг - "поправить фронт согласно новой модели".

    Если придерживаться принципа DRY

    dry в теме статьи не работает, т.к. при обновлении типов\модели - общее количество телодвижений разработчиков меньше не становится.


    1. nikitaulshin Автор
      08.11.2021 10:17
      +1

      Имхо, проблема синхронизации типов идеального решения не имеет, всегда есть какие-то сайд-эффекты


  1. euhoo
    07.11.2021 09:28
    +3

    Статья проблему не раскрывает. Вот у нас есть фронт, есть промежуточный бэк на ноде и есть ядро на Java. Следуя статье, пока у нас нода - все ок. Но как только в игру вступает что-то другое(наша Java) - уже нет. Выше в комментарии написали про openApi и это хорошее решение. Мы именно так и пошли.


    1. nikitaulshin Автор
      08.11.2021 10:16

      Мне тоже OpenAPI пока больше всего нравится.