Привет, Хабр! На связи Александр Чебанов, технический директор компании Modus. Мы разрабатываем BI-платформу, которая собирает большие объемы данных из разных источников и представляет их в виде понятных дашбордов и отчетов для бизнеса.

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

Проблема: индивидуальные доработки для клиентов в общем продукте

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

Сначала мы пробовали вести отдельные git‑ветки под каждого клиента. На первых порах это работало, но с каждым обновлением ядра количество конфликтов при слиянии веток (merge‑конфликтов в коде) росло, а время на их исправление увеличивалось. Это замедляло развитие всей платформы и усложняло поддержку.

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

Архитектура: что есть что?

Мы четко определили зоны ответственности.

Ядро Modus BI — это весь наш проект, который:

  • предоставляет API для взаимодействия с плагинами;

  • умеет подключать и инициализировать плагины;

  • отвечает за их отображение в интерфейсе;

  • управляет данными и поддерживает безопасность.

Плагин — это, в нашем случае, кастомная визуализация для дашборда, которая:

  • общается с бекендом через выделенное API ядра;

  • обрабатывает и преобразует данные;

  • содержит собственные настройки;

  • умеет рендерить свой интерфейс в админке для конфигурации;

  • отображает уникальный виджет на дашборде.

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

Реализация: как это работает?

1. Загрузка плагинов на бекенде

Плагины загружаются в виде .tar.gz-архивов через админ-панель. Каждый архив содержит полный набор файлов: JS-бандл, CSS-стили, метаданные и обязательный манифест с описанием плагина.

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

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

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

Схематично процесс выглядит так:

2. Интеграция плагинов на фронтенде

Чтобы фронтенд понимал, что плагин загружен и готов к работе, мы используем заглушки. При сборке основного приложения резервируем места под будущие плагины в виде пустых React‑компонентов. Эти компоненты служат точками подключения, куда позже подставляется код плагина.

Вот как это работает технически:

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

Конфигурация Webpack:

const getCustomChartExternals = (indexRange) => {  

  const output = {};  

  for (let i = indexRange[0]; i <= indexRange[1]; i += 1) {  

    output[custom-chart-${i}] = CustomChart${i};  

  }  

  return output;  

};

webpackConfig.externals = [  

  {  

    // ... другие внешние зависимости

    ...getCustomChartExternals([0, 10]),  

  },  

];

Затем в index.html мы динамически подключаем все возможные плагины:

<!DOCTYPE html>  

<html lang='ru'>  

<head>

<!-- ... -->

<script type='text/javascript'>

  // ...

  const scriptPaths = [];

  for (let i = 0; i <= 40; i += 1) {  

    scriptPaths.push(${__base__}plugins/custom_chart_${i}.js);  

  }

  

  let scripts = '';

  scriptPaths.forEach((path) => {  

    scripts += <script src="${path}" defer></scr${''}ipt>;  

  });

  

  document.write(scripts);  

</script>

<!-- ... -->

</head>  

<body>  

  <div id='root'></div>

</body>  

</html>

Когда реальный плагин загружается на бекенд, его JavaScript-файл перезаписывает соответствующую заглушку. Фронтенд начинает использовать новый код плагина (его рабочую реализацию) без перезагрузки всего приложения.

3. API плагина и интеграция в ядро

Для взаимодействия с ядром платформы мы определили четкий API-контракт. Каждый плагин должен экспортировать четыре основных компонента:

  • CustomChart — корневой компонент для отображения виджета на дашборде;

  • CustomSettings — интерфейс настроек, который отображается в админке;

  • CustomReducers — Redux-экшен (функция, которая описывает, как изменяется состояние в Redux-хранилище) для интеграции с Redux;

  • CustomAxes — конфигурация для работы с данными в конструкторе.

Рассмотрим на примере интеграции компонента визуализации (CustomChart).

Сначала мы все плагины централизованно импортируем в ядро:

// charts/VisualComponents.js

export { CustomChart as CustomChart0 } from 'custom-chart-0';

// ...

export { CustomChart as CustomChart10 } from 'custom-chart-10';

Затем фабрика компонентов (ComponentFactory) определяет, какую именно визуализацию нужно отрендерить, и использует для этого соответствующий плагин.

import React, { Component } from 'react';

import PropTypes from 'prop-types';

import VisualComponents from 'charts/VisualComponents';

export default class ComponentFactory extends Component {

  static propTypes = {

    component: PropTypes.object,

    config: PropTypes.object.isRequired,  

    spec: PropTypes.object,

    datas: PropTypes.object,

    pluginImports: PropTypes.object,

    // ...

  };

  render() {

    const { component, config, datas, pluginImports } = this.props;

    if (component?.type) {

      const ComponentToRender = VisualComponents[component.type];

            if (!ComponentToRender) {

        // Обработка случая, когда компонент не найден

        return <div>Компонент не найден</div>;

      }

      return (

        <ComponentToRender

          config={config}

          datas={datas}

          pluginImports={pluginImports}

          // ... другие пропсы

        />

      );

    }

    return null;

  }

}

Ядро передает плагину все необходимые данные через пропсы: datas для работы с данными, config с настройками виджета и pluginImports с вспомогательными функциями для логирования, локализации и API-вызовов.

Аналогичным способом подключаются и остальные элементы плагина (CustomSettings, CustomReducers, CustomAxes), каждый в своей части приложения.

Общая схема взаимодействия выглядит так:

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

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

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

При активации плагина ядро динамически заменяет redux-экшены с заглушки на redux-экшены плагина, что предотвращает конфликты. При удалении плагина redux-экшен заменяется на экшен заглушки.

Результаты и выводы

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

  1. Разрабатывать кастомные визуализации отдельно, в независимых репозиториях.

  2. Быстро разворачивать виджеты на портале клиента без пересборки ядра и деплоя всего приложения.

  3. Передавать разработку плагинов клиентам, если у них есть необходимые навыки.

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

А как вы подходите к задачам кастомизации в ваших проектах? Сталкивались ли с подобными проблемами? Делитесь опытом в комментариях!

P.S. Присоединяйтесь к нашему BI-сообществу в Telegram и будьте в курсе последних новостей!

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