Цикл продуктовой разработки часто напоминает весы: с одной стороны, системное проектирование, подбор основополагающих инструментов, масштабные рефакторинги. С другой — совокупность локальных решений, принимаемых для точечных улучшений в системе. И самое сложное тут: соблюдать баланс. Как понять, когда имеет смысл вмешаться «хирургически», а когда — предпочесть вместо конкретной проблемы решить (или предотвратить) целый класс проблем?  

Иногда нащупать границу между «масштабом» и «целесообразностью» получается почти что случайно. Однажды мы в Сравни подступились к переделке чата в нашем мобильном приложении, и на старте расценивали задачу как «ещё один рядовой продуктовый кейс». Но планы по модификации фичи быстро переросли в создание универсального инструмента: конструктора сценариев на базе Backend Driven UI.

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

Подробности о нашем сценарном BDUI-движке — этапах его создания, вариантах использования и нюансах технического устройства — читайте под катом.


Привет, Хабр! Меня зовут Денис, я архитектор в Сравни. Сегодня расскажу, как мы разработали для нашего мобильного приложения новый конструктор сценариев на базе nestjs и React. 

Началось всё, как уже упомянуто выше, с локальной потребности: заменить чат приложения Сравни более гибкой и удобной React-анкетой. Но при решении этой задачи нам удалось построить вполне себе универсальный BDUI-инструмент. С его помощью внесение любых изменений во фронтенд нашего приложения (например, добавление новых продуктов) происходит максимально быстро и безболезненно. Отвечает за это специальный сценарный движок на бэке — его мы нескромно, но справедливо прозвали «Flow-машиной». 

В статье я слегка затрону бизнесовую часть — зачем мы всё это затеяли и к каким результатам пришли. А затем углублюсь в технические подробности реализации: в первую очередь для нодеров и всех, кто интересуется принципами работы BDUI.

Зачем мы стали всё переделывать

Исторически оформление новых продуктов в мобильном приложении Сравни было сделано в виде чата: виртуальный помощник последовательно задает покупателю вопросы, проверяет и запрашивает дополнительную информацию, затем формирует анкету и отправляет заявку на продукт. 

Что конкретно не устраивало нас в «чатовой» реализации?

  • Чат морально устарел и был неудобен для клиента, его интерфейс не позволял реализовать механики, которые хотел бизнес. Формат чата ограничивал нас, как правило, одним достаточно простым полем ввода, небольшой вариативностью элементов управления и сложностью с представлением промежуточных данных в ходе заполнения продуктовой анкеты.

  • Редактирование чата было не слишком удобным, особенно когда переписки стали действительно огромными — JSON’ы на несколько тысяч строк. Хотя можно было декомпозировать и применять другие уловки, это была бы масштабная задача, в которой легко допустить ошибки.

  • В чате у нас был язык выражений для выбора пользовательского пути, подготовки данных перед отображением и шаблонизатор сообщений. Этот язык был достаточно похож на JS, но не поддерживал полезные функции типа Array.map, filter, reduce. Что порождало дополнительные ошибки.

Пример очень страшного JSON-а
{
      "_id": "reuse_passport",
      "type": "ViewCard",
      "router": {
        "routes": [
          {
            "endpoint": "correct_scan"
          }
        ]
      },
      "message": {
        "elements": [
          {
            "template": "ПАСПОРТ РФ",
            "type": "header"
          },
          {
            "template": "ФИО: ",
            "type": "labled-input",
            "elements": [
              {
                "template": "${fio}",
                "type": "input-value"
              }
            ]
          },
          {
            "template": "Серия и номер: ",
            "type": "labled-input",
            "elements": [
              {
                "template": "${pass_id}",
                "type": "input-value"
              }
            ]
          },
          {
            "template": "Дата выдачи: ",
            "type": "labled-input",
            "elements": [
              {
                "template": "${date_received}",
                "type": "input-value"
              }
            ]
          },
          {
            "template": "Дата рождения: ",
            "type": "labled-input",
            "elements": [
              {
                "template": "${birthday}",
                "type": "input-value"
              }
            ]
          }
        ],
        "modifiers": []
      },
      "name": "question-card",
      "modifiers": [],
      "displayType": "passport"
    },
 // и дальше за горизонт
}

Чтобы заменить чат, можно было действовать «в лоб»: написать под каждый продукт свой сервис, собрать на фронтенде кучу отдельных экранчиков и быстро выпустить приложение. Но мы решили разработать универсальный инструмент. По задумке он позволил бы нам не просто переделать чат, но и в целом упростить работу с приложением: быстрее реализовывать фичи, выводить новые продукты и легко поддерживать старые.

Почему пошли в эту сторону? Дело в нашей мультипродуктовости. В Сравни мы работаем с финансовыми, страховыми, образовательными продуктами, и «локальный подход» не выглядел оптимальным: для каждого направления требовалось бы так или иначе писать кучу шаблонного кода, поддерживать инфраструктуру, сохранять версионирование — а это потеря времени и расфокусировка внимания. Единожды решить такую задачу было бы несложно (хотя и скучно), но на дистанции нескольких лет поддержка этого зоопарка стала бы камнем на шее у любой команды. Нужен был универсальный инструмент — с перспективой долгосрочной поддержки всей нашей продуктовой линейки, мощный, при этом понятный разработчикам.

В целом мы намеревались закрывать одну большую задачу: получить возможность гибко управлять фронтендом приложения. Понятно, что сделать это сложнее, чем управлять несколькими системами — по сути нужно создать инструмент для метапрограммирования! Зато выхлоп от реализации был бы очевиден. 

Забегая вперед, посмотрим, что у нас получилось — пока на примере одной конкретной задачи с переделкой чата под React-анкету.

Первоначально было вполне стандартное чатовое приложение:

Сейчас наша анкета выглядит вот так, с красивыми кнопочками и разворачивающимися экранчиками:

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

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

Получилось не только красиво, но и эффективно с продуктовой точки зрения: конверсия выросла на 10-15%. В настоящий момент на конструкторе сценариев работают 13 больших и 8 маленьких анкет.

Как мы создавали нашу анкету: обращаемся к BDUI

Определив требования к анкете, мы зафиксировали основные моменты, которые требовалось учитывать при разработке решения: 

  • Анкеты могут содержать как небольшие экраны с одним-двумя полями ввода, так и большие — со множеством информации и интерактивных элементов. 

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

  • Анкеты интенсивно взаимодействуют с внешними сервисам: забирают данные из сервиса пользовательских данных, формируют составные подсказки, рассчитывают предварительные суммы и запрашивают список предложений.

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

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

  • Анкеты содержат сложные композитные компоненты.

Исходя из этого остановились на следующих обязательных шагах:

  • Использовать Backend-Driven UI

  • Реализовать полноценную поддержку JS

  • Обеспечить независимую разработку, хранение и раскатку новых сценариев

  • Предоставить удобные способы для использования сторонних API внутри сценариев

  • Реализовать легкий способ управления памятью сценария

  • Обеспечить понятный и немногословный способ разработки фронтенда

  • Максимально снизить вероятность ошибки при разработки фронтовой и бэковой части

Что за зверь Backend Driven UI — и зачем он нужен нам?

В каком-то смысле наш старый чат уже был построен на принципе BDUI. Разница лишь в том, что чат отображает новое сообщение, а анкета — большие и сложные экраны. 

Все перемещения по экранам и решения о том, что именно отображать, принимаются бэкендом. Так мы имеем полный контроль над тем, куда пользователь пойдет, и не перегружаем фронтенд дополнительными знаниями о том, как исполнять сценарий. Задача фронтенда — нарисовать красивую картинку и беззаботно отправлять нам пользовательские действия через сокет.

Сценарий включает не только представление и роутинг, но и операции, выполняемые на бэкенде. И таких операций на самом деле может быть немало: 

  • валидация входящих данных

  • форматирование входящих и исходящих данных 

  • получение дополнительной информации из внутренних сервисов

  • выполнение бизнес-операций.

  • работа с памятью диалога с пользователем

  • обработка разнообразных ошибок

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

Мы разделили наш сценарий на следующие элементы:

  • память диалога с пользователем (контекст)

  • основные и вспомогательные узлы

  • оркестратор, который управляет исполнением узлов 

Рассмотрим каждый из них подробнее.

1. Память диалога

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

При организации памяти диалога мы хотели добиться нескольких вещей:

  • быстрого доступа к памяти

  • гибкой структуры памяти и использования сложных типов данных

  • читабельности хранимой информации для разработчика

  • простого способа обращения к переменным внутри памяти

Управление памятью диалога реализовано через специализированный модуль NestJS, который обеспечивает абстракцию от конкретного способа хранения данных, упрощая взаимодействие с памятью на уровне бизнес-логики. 

В качестве основного хранилища используется MongoDB. Данные сохраняются в формате JSON, что предоставляет гибкость в структуре документов и упрощает их визуальное восприятие. Для повышения производительности применяется выборочная загрузка данных (через projection): при обработке конкретного узла извлекаются только необходимые поля, что снижает избыточность операций с базой данных и ускоряет доступ.

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

1.1. Расширение контекста

Иногда данные, получаемые из API, нужно сложно преобразовать, комбинировать и использовать повторно в нескольких узлах. Для этого нам на помощь приходит механика расширения контекста, которая помогает описать виртуальное поле как будто бы находящееся в памяти диалога, но в реальности оно будет рассчитано на бэкенде перед вызовом узла, в котором это поле используется.

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

// context/shorthands.ctx.ts
export const fullName = ({Api, context}) => {
  return `${context.firstName} ${context.secondName}`;
}

2. Узлы сценария

Мы решили не останавливаться на каком-то определенном списке (аппетит приходит во время еды!), но именно эти узлы станут базовыми строительными блоками для любого сценария:

Основные узлы:

  • Узлы исполнения бэкенд-операций и преобразования данных

  • Узлы отображения

Вспомогательные узлы:

  • Узлы роутинга

  • Узлы валидации

  • Узлы преобразования пользовательского ввода

2.2. Узел исполнения бэкенд-операций

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

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

Вопрос возник на этапе, когда мы задумались, как нам этот TS исполнять, не сохраняя его в нашем репозитории сервиса и не перезапуская сервис с появлением нового сценария. И тут нам на помощь приходит прекрасная особенность Node.js в виде возможности импортировать новые модули прямо во время исполнения. Так они автоматически кэшируются и оптимизируются со временем. 

Конечно, подход runtime require обычно не приветствуется и используется в основном в глубоко служебных библиотеках. Но мы оценили все плюсы того, что можем просто выложить скомпилированный файл сценария на s3, зарегистрировать его в хранилище, тут же скачать и импортировать. Изначально мы хотели оповещать все инстансы сервиса через очередь сообщений. Но бонусы от прогрева «кэша» сценариев оказались настолько незначительными, что мы оставили загрузку каждый раз, когда первый пользователь начинает этот сценарий. 

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

// get-user-node.api.ts
export default async ({userId, context, Api }) => {
  const userProfile = Api.Profile.getUserProfile(userId, [“name”]);
	
  return { userName: userProfile.name };
}

Дело было за малым: избежать необходимости работать напрямую с памятью сценария, при этом не загружая её при каждом исполнении узла. Но раз мы все равно используем сборщик, то просто написали плагин, который находит все обращения к полям context и собирает их для нас, чтоб мы заранее знали, какие поля нужно получить из памяти для этого запроса.

Например, если мы прогоним через bundler в такую функцию

// api-node.api.ts
export default async ({ context}) => {
  const sum = context.a + context.b + context.c;
	
  return { sum };
}

то после сборки увидим что-то похожее:

// api-node.api.js
export default { 
 fn: async ({ context }) => {
	   const sum = context.a + context.b + context.c;
       return { sum };
    },
 fields: [“a”,b”,”c”],
}
Да, выведение комплексных путей типа “context.a.b.c”, “context.a?.b?.c”, “context.a[name].c” мы тоже сделали.

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

Собирать данные пользователя может самая примитивная формочка. Основная ценность у нас — в интеграции со всеми нашими сервисами, которые дают нам огромное количество добавочных данных, фильтруют предложения и вот это всё. 

Мы постарались сделать интеграции максимально чисто. То есть внутрь функции, которая у нас формирует каждый шаг, мы поставляем некоторый набор уже готовых апишек. Естественно, можно просто запросить свой API, сделав банальный fetch-запрос. Этого никто не отнимет, но уже есть большое количество интеграций. В основном это API, которые оборачивают взаимодействие со сторонними сервисами и делают его чуть-чуть (а иногда и сильно) удобнее. 

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

По сути, разработчику, который пишет сценарий и получает эти данные, не нужно заботиться, как там передают access-токены, на какой URL ему сходить и так далее — для него это просто JavaScript-функции. Полученные данные он закидывает либо в контекст сценария, либо на их основе выполняет какой-то рутинг на тот или иной экран.

2.2. Узел отображения

Мы хотели добиться наиболее простого и безболезненного описания для узлов отображения, их одинакового поведения и внешнего вида. При этом были, мягко говоря, ограничены во времени. А так как даже самые дремучие бэкендеры знали, что существует React, и с помощью магии Webview его можно затащить и на iOS, и на Android, то было принято решение сделать пилотное приложение именно на нем. 

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

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

Типичный узел отображения:

//hello-screen.card.tsx
import type MobileKit from “@sravni/flow-mobile-kit”

export default ({context, Kit: MobileKit }) => {
	return (
<Kit.Screen>
	<Kit.Text> Привет, {context.name}! </Kit.Text>
	</Kit.Screent>
}

Для узлов отображения мы используем тот же самый плагин webpack для выделения обращений к полям context, чтобы знать те поля, которые нам нужно будет отправить вместе с именем узла на фронтенд, чтобы он смог отрисовать экран.

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

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

2.3. Узел роутинга

Самый базовый узел, который позволит нам выбирать, куда мы отправляем пользователя дальше. Его единственная задача — возвращать имя следующего узла, в который мы попадем.


// test-node.route.ts
export default ({ payload, Api }) => {
  // переводим на шаг “ok” или “not_ok” 
  return payload.age > Api.Age.minValue() ? “ok” : “not_ok”;
}

Или можно даже просто так, если переход не подразумевает вариативности:

// test-node.route.ts
export default “ok”;

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

2.4 Узел валидации

Тут тоже все просто: узел валидации возвращает либо список ошибок, либо пустое значение, если их не обнаружено. 

2.5. Узел преобразования пользовательского ввода

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


export default async ({ payload, Api }) => {
	const normalizedAddress = await Api.Location.getAddress(payload.address);

return { address: normalizedAddress };
}

2.6. Остальные узлы

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

Как мы храним описание сценариев

Очевидным решением было хранить каждый сценарий в своем репозитории, чем мы собственно и воспользовались. Это нам дает возможность легко отслеживания изменений в сценарии, понятного версионирования и всевозможные блага, которые дает CI\CD.

Для упрощения восприятия, где какой узел и возможности расширения, мы решили использовать суффиксы в стиле nestjs. Так узлы отображения стали .api.ts, роутинга.route.ts и т.д. Так же суффиксы помогают нам модифицировать билд-скрипты под специфические нужды узла.

//...
{
  test: /\.(card|route|input|api|validate|view|action).(ts|tsx)$/,
  use: [
    { loader: '@sravni/flow-scripts-utils/instruction-loader' }
  ],
},
//...

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

Для простых сценариев у нас дополнительно существуют скрипты, которые генерируют за разработчика все скучные index.ts файлы со сгруппированными импортами и формируют две точки входа для вебпака (одна для фронтовой части сценария, а вторая для бэка), в сложных случаях, конечно, придется написать их самостоятельно.

Отдельную головную боль создают особенности мобильного приложения, когда пользователи не обновляются, и не могут пользоваться свежей функциональностью. Для этого мы предусмотрели manifest-файлы, в которых описывается, для какой версии и платформы этот сценарий предназначен.

Все сценарии версионируются по semver. Например, если нам нужно исправить вид какого-то компонента отображения, что не влияет на сам пользовательский путь, мы можем это легко сделать. 

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

При старте сценария выбирается максимально свежая версия, опираясь на версию платформы и версию приложения.

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

Как устроен фронтенд

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

В мобильное приложение встроен WebView с React-приложением. В этом React-приложении находятся WebSocket и собственные утилитарные модули, включая модуль компонентов. Сам сценарий не определяет, как будет вести себя кнопка или как параметр Color: Red будет интерпретироваться кнопкой — всё это изолировано от самого сценария.

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

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

Архитектура и возможности приложения

Изначально наше приложение состояло из нескольких независимых сервисов. Во-первых, есть Socket-фронтенд, который общается с Flow-машиной. Flow-машина — это тот самый мозг, который принимает решения, обрабатывает данные, сохраняет данные. Во-вторых, есть сервис версионирования, хранилище сценариев и хранилище интерфейсов. 

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

Ещё это нам дало возможность синхронного тестирования — допустим, когда мы с тестировщиком одновременно открыли интерфейс с одинаковым идентификатором сценария, тестировщик что-то нажимает, что-то обновляет, а разработчик в своем браузере синхронно видит все действия и события, которые приходят на фронт. Хотя это не было нашей целью, но оказалось бесценно во время отладки и внедрения. 

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

Конструктор сценариев, по сути, является системой оркестрации. То есть на нем можно запускать чисто бэкэнд-процессы, проводить какие-то вычисления, ходить в aпишки. И это всё асинхронно и защищено сессиями. Теоретически, эту систему можно с определенными ограничениями использовать для создания сложных бизнес-процессов, как минималистичную замену bpmn-движка, или для е2е-тестирования сложных кейсов.

Конечно, пришлось потрудиться, чтобы реализовать всю эту сложную машину на фронтенде, зато сейчас с созданием той же конкретной анкеты у нас может справиться даже Junior-разработчик. А некоторые вопросы теперь могут закрывать сотрудники, которые вообще не являются разработчиками. 

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

Мы не раз расширяли функциональность самого движка сценариев, добавляли новые фичи, новые способы взаимодействия, расширяли инструментарий и сейчас переносим его на полноценный нативный BDUI. И все это сделано без боли: мы переписывали не так много нашего изначального кода, а только расширяли его.

***

Как один из разработчиков проекта скажу, что всё это, по сути, начиналось как шуточный набор самых странных подходов, которые вроде бы должны работать. Но никто не был готов ставить даже рубль, что все не развалится от первого же чиха и не разобьется о подводные камни. Мы собрали и webview, и runtime-импорты, смешали бэкендовую и фронтендовую часть в одном репозитории, добавили кодогенерацию на магии webpack-а, видели перед собой множество проблем, которые нам казались трудно преодолимыми. Но в итоге это сложилось в цельную картинку и выдержало проверку временем. 

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

Отсюда можно сделать вывод: создавайте первую версию настолько плохой, что все будут рады, когда вы её замените на что угодно не отбрасывайте идеи, если при первом рассмотрении они как будто бы не должны сработать или кажутся слишком нестандартными, сначала обоснуйте, почему так именно в вашем случае; не бойтесь сложностей, если вы сами не сталкивались с ними, а только слышали о них, проводите эксперименты. 

Возможно, это даст вам возможность создать интересное решение, которое позволит сэкономить кучу времени и сделать людей немного счастливее.

Надеюсь, мой рассказ оказался для кого-то полезным. Буду рад ответить на вопросы и комментарии. 


ТГ-канал инженерного сообщества Sravni Tech

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


  1. enoro
    30.05.2025 23:14

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


    1. Andrew0610
      30.05.2025 23:14

      Вот кстати да. Выглядит как фича ради фичи