Micro-Frontend Plugin System на React + Vite: опыт и подводные камни

#react#vite#microfrontend#plugin-system

Когда пишешь продукт с плагинами — особенно если это AI-инструмент вроде Scrum-доски — рано или поздно упираешься в необходимость изолировать сторонний код. В Paca решили эту проблему через микрофронтенды на Vite, и вот что из этого вышло.

Почему не iframe и не monorepo

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

Решение на Vite Module Federation выглядит логичным компромиссом: плагины грузятся асинхронно как отдельные бандлы, но работают в том же DOM-контексте.

Как устроена изоляция плагинов

Авторы Paca используют три ключевых механизма:

  1. Vite Remotes для динамической загрузки:
// host-vite.config.js
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

export default defineConfig({
  plugins: [
    react(),
    federation({
      name: 'host',
      remotes: {
        plugin1: 'http://localhost:5001/assets/plugin1.js',
      },
      shared: ['react', 'react-dom']
    })
  ]
})
  1. Sandboxed React Trees через кастомный Provider:
const PluginSandbox = ({ pluginUrl, children }) => {
  const [scope] = useState(() => {
    const scope = new WeakMap()
    scope.set(React, await import(pluginUrl + '/react'))
    return scope
  })

  return (
    <React.Provider value={scope}>
      <ErrorBoundary>
        {children}
      </ErrorBoundary>
    </React.Provider>
  )
}
  1. Event Bridge вместо прямого доступа к состоянию:
interface PluginEvent {
  type: 'ai/response' | 'ui/update';
  payload: unknown;
  meta: {
    pluginId: string;
    timestamp: number;
  };
}

const usePluginBus = () => {
  const bus = useContext(EventBusContext)
  
  const emit = (event: PluginEvent) => {
    if (!event.meta.pluginId) throw new Error('Plugin ID required')
    bus.publish(event)
  }

  return { emit }
}

На практике это означает, что плагин может сломать только свою часть интерфейса, но не положит всё приложение.

Где архитектура даёт течь

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

  1. Shared Dependencies: Даже с явным указанием shared: ['react'] в federation-конфиге иногда подгружались две копии React. Лечится явным version check в host-приложении.

  2. CSS-коллизии: Плагины со стилями в JS (CSS-in-JS) иногда пробивали изоляцию. Пришлось добавить postcss-плагин для автоматического префиксирования классов.

  3. AI-специфика: Когда плагин генерирует промпты для LLM, event bus может стать бутылочным горлышком. Для AI-ивентов добавили отдельный канал с prioritization.

Что бы я сделал иначе

  1. Runtime Config вместо Build-Time Federation: Динамическое подключение remote-модулей через import() дало бы больше гибкости.

  2. Contract Testing: Не доверял бы плагинам даже изолированным контекстом без проверки типов событий через Zod или аналоги.

  3. Lazy SDK: Текущая реализация загружает весь SDK плагина сразу. Для тяжелых AI-плагинов лучше динамически импортировать только используемые методы.

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


Источник: https://www.reddit.com/r/reactjs/comments/1u78a0u/built_a_microfrontend_plugin_system_with_react/