Один движок на React и Vue: как SkyGraph работает в двух фреймворках
Большинство UI-библиотек привязаны к одному фреймворку. MUI — React. Vuetify — Vue. Если команда мигрирует с React на Vue (или наоборот), библиотеку приходится менять. SkyGraph решает это иначе: один движок, два адаптера.
Архитектура
@skygraph/core — reactive runtime + engines (framework-agnostic)
@skygraph/react — React-обёртки
@skygraph/vue — Vue-обёртки
@skygraph/css — общие стили (CSS Variables)
Ядро (@skygraph/core) не знает ничего о React или Vue. Оно экспортирует движки: TableEngine, FormEngine, TreeEngine, CalendarEngine и другие. Каждый движок — это класс с reactive signals внутри.
Reactive signals
Вместо React state или Vue refs — собственные signals:
import { signal, computed, effect } from "@skygraph/core";
const rows = signal<Row[]>([]);
const sortedRows = computed(() => {
return [...rows()].sort(compareFn);
});
effect(() => {
console.log("Rows changed:", sortedRows().length);
});
Signal — это observable-значение. computed — derived-значение, пересчитывается при изменении зависимостей. effect — побочный эффект, выполняется при изменении.
React-адаптер
React-адаптер подписывается на signals через useSyncExternalStore:
function useSignal<T>(sig: Signal<T>): T {
return useSyncExternalStore(
sig.subscribe,
sig.get,
sig.get
);
}
export function SkyTable({ data, columns }: SkyTableProps) {
const engine = useMemo(() => new TableEngine({ data, columns }), []);
const sorted = useSignal(engine.sortedRows);
const selected = useSignal(engine.selectedIds);
return <table>...</table>;
}
Vue-адаптер
Vue-адаптер оборачивает signals в shallowRef + watchEffect:
function useSignal<T>(sig: Signal<T>): ShallowRef<T> {
const ref = shallowRef(sig.get());
watchEffect((onCleanup) => {
const unsub = sig.subscribe((val) => { ref.value = val; });
onCleanup(unsub);
});
return ref;
}
Компонент на Vue получает ту же таблицу, тот же API, те же features — но через привычный Composition API.
CSS без дублирования
Стили общие для обоих фреймворков — чистый CSS с CSS Variables:
[data-sky-theme="light"] {
--sky-bg: #ffffff;
--sky-border: #e2e8f0;
--sky-text: #1a202c;
}
[data-sky-theme="dark"] {
--sky-bg: #1a1a2e;
--sky-border: #2d3748;
--sky-text: #e2e8f0;
}
Переключение темы — один атрибут на <html>. Нет CSS-in-JS, нет runtime стилей, нет tree-shaking проблем.
Почему не Web Components
Альтернативный подход — Web Components (Lit, Stencil). Отвергнут по причинам:
- SSR — Web Components плохо работают с серверным рендерингом
- Типизация — пропсы через атрибуты (строки), нет type-safe API
- Стили — Shadow DOM усложняет кастомизацию
- DX — разработчики хотят использовать привычные паттерны фреймворка
Результат
- Стилизованные компоненты для React и Vue на одном ядре
- Один PR в ядро = фича в обоих фреймворках одновременно
- Тесты ядра — 85% coverage, без DOM
- Миграция между фреймворками: меняешь импорт, логика та же