Когда пишешь продукт с плагинами — особенно если это AI-инструмент вроде Scrum-доски — рано или поздно упираешься в необходимость изолировать сторонний код. В Paca решили эту проблему через микрофронтенды на Vite, и вот что из этого вышло.
Почему не iframe и не monorepo
Первое, что приходит в голову для изоляции плагинов — iframe или вынос их в отдельные пакеты monorepo. Но в случае с AI-ассистентами и динамически подгружаемыми модулями оба подхода дают сбой:
- Iframe убивает перформанс при частых коммуникациях между плагинами (а в AI-сценариях события летают постоянно)
- Monorepo требует пересборки всего приложения при добавлении нового плагина — не вариант для community-разработки
Решение на Vite Module Federation выглядит логичным компромиссом: плагины грузятся асинхронно как отдельные бандлы, но работают в том же DOM-контексте.
Как устроена изоляция плагинов
Авторы Paca используют три ключевых механизма:
- 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']
})
]
})
- 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>
)
}
- 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 }
}
На практике это означает, что плагин может сломать только свою часть интерфейса, но не положит всё приложение.
Где архитектура даёт течь
После недели тестов на реальных плагинах обнаружились неочевидные проблемы:
-
Shared Dependencies: Даже с явным указанием
shared: ['react']в federation-конфиге иногда подгружались две копии React. Лечится явным version check в host-приложении. -
CSS-коллизии: Плагины со стилями в JS (CSS-in-JS) иногда пробивали изоляцию. Пришлось добавить postcss-плагин для автоматического префиксирования классов.
-
AI-специфика: Когда плагин генерирует промпты для LLM, event bus может стать бутылочным горлышком. Для AI-ивентов добавили отдельный канал с prioritization.
Что бы я сделал иначе
-
Runtime Config вместо Build-Time Federation: Динамическое подключение remote-модулей через
import()дало бы больше гибкости. -
Contract Testing: Не доверял бы плагинам даже изолированным контекстом без проверки типов событий через Zod или аналоги.
-
Lazy SDK: Текущая реализация загружает весь SDK плагина сразу. Для тяжелых AI-плагинов лучше динамически импортировать только используемые методы.
Если вы сейчас выбираете архитектуру для плагинной системы — попробуйте этот подход, но сразу закладывайте время на борьбу с вендорными зависимостями. Особенно болезненными могут быть случаи, когда плагин тащит свою версию библиотеки, которая конфликтует с host-приложением.
Источник: https://www.reddit.com/r/reactjs/comments/1u78a0u/built_a_microfrontend_plugin_system_with_react/