Astro поддерживает не только React. Vue 3 и Svelte — полноценные граждане в экосистеме Astro. Вы можете использовать их как острова интерактивности, смешивать в одном проекте и даже комбинировать с React-островами. Разбираем, как это работает.
Философия: Острова любого фреймворка
Astro — фреймворк-агностик. Он не навязывает React. Вы выбираете инструмент под задачу:
---
import ReactDropdown from './Dropdown.tsx'; // React
import VueCounter from './Counter.vue'; // Vue 3
import SvelteSearch from './Search.svelte'; // Svelte
---
<!-- Все три острова работают независимо на одной странице -->
<ReactDropdown client:load />
<VueCounter initialValue={5} client:visible />
<SvelteSearch client:idle /> Часть 1: Astro + Vue 3
Установка
npx astro add vue Автоматически установит @astrojs/vue, vue и обновит astro.config.mjs.
// astro.config.mjs (после установки)
import vue from '@astrojs/vue';
export default defineConfig({
integrations: [
vue({
appEntrypoint: '/src/pages/_app', // Необязательно: глобальные плагины
}),
],
}); Vue SFC компонент
<!-- src/components/VueCounter.vue -->
<template>
<div class="counter">
<button @click="decrement" :disabled="count <= 0">−</button>
<span class="counter__value">{{ count }}</span>
<button @click="increment">+</button>
<p>{{ doubleCount }} (двойное)</p>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue';
interface Props {
initialValue?: number;
}
const props = withDefaults(defineProps<Props>(), {
initialValue: 0,
});
const count = ref(props.initialValue);
const doubleCount = computed(() => count.value * 2);
function increment() {
count.value++;
}
function decrement() {
count.value--;
}
</script>
<style scoped>
.counter {
display: flex;
align-items: center;
gap: 16px;
}
.counter__value {
font-size: 2rem;
font-weight: bold;
min-width: 3ch;
text-align: center;
}
</style> Использование в Astro:
---
import VueCounter from '../components/VueCounter.vue';
---
<VueCounter initialValue={10} client:visible /> Vue Router в Astro?
Не рекомендуется. Astro управляет маршрутизацией. Vue Router создаст конфликт. Для навигации используйте файловую маршрутизацию Astro. Vue — только для изолированных компонентов.
Pinia store для Vue-островов
Для состояния между несколькими Vue-островами используйте Pinia:
npm install pinia // src/lib/piniaPlugin.ts — инициализация Pinia
import { createPinia } from 'pinia';
export const pinia = createPinia(); // astro.config.mjs
vue({
appEntrypoint: '/src/pages/_app',
})
// src/pages/_app.ts
import type { App } from 'vue';
import { pinia } from '../lib/piniaPlugin';
export default (app: App) => {
app.use(pinia);
}; // src/stores/cart.ts (Pinia)
import { defineStore } from 'pinia';
export const useCartStore = defineStore('cart', {
state: () => ({ items: [] as { id: string; qty: number }[] }),
getters: {
total: (state) => state.items.length,
},
actions: {
add(id: string) {
const existing = this.items.find((i) => i.id === id);
if (existing) existing.qty++;
else this.items.push({ id, qty: 1 });
},
},
}); Часть 2: Astro + Svelte
Установка
npx astro add svelte // astro.config.mjs
import svelte from '@astrojs/svelte';
export default defineConfig({ integrations: [svelte()] }); Svelte компонент
<!-- src/components/SvelteSearch.svelte -->
<script lang="ts">
export let placeholder: string = 'Поиск...';
export let items: { title: string; url: string }[] = [];
let query = '';
$: results = query.trim()
? items.filter(i => i.title.toLowerCase().includes(query.toLowerCase()))
: [];
</script>
<div class="search">
<input
type="search"
bind:value={query}
{placeholder}
class="search__input"
/>
{#if results.length > 0}
<ul class="search__results">
{#each results as result (result.url)}
<li>
<a href={result.url}>{result.title}</a>
</li>
{/each}
</ul>
{:else if query.trim()}
<p class="search__empty">Ничего не найдено по запросу «{query}»</p>
{/if}
</div>
<style>
.search {
position: relative;
width: 100%;
max-width: 600px;
}
.search__input {
width: 100%;
padding: 12px 16px;
border-radius: 8px;
border: 1px solid var(--color-border, #333);
}
.search__results {
position: absolute;
top: calc(100% + 4px);
left: 0;
right: 0;
background: var(--color-surface-container, #1a1a2e);
border-radius: 8px;
list-style: none;
padding: 8px 0;
box-shadow: 0 4px 24px rgb(0 0 0 / 30%);
z-index: 100;
}
.search__results a {
display: block;
padding: 8px 16px;
color: inherit;
text-decoration: none;
}
.search__results a:hover {
background: var(--color-surface, #111);
}
</style> В Astro:
---
import { getCollection } from 'astro:content';
import SvelteSearch from '../components/SvelteSearch.svelte';
const articles = await getCollection('articles');
const searchItems = articles.map((a) => ({
title: a.data.title,
url: `/articles/${a.id.replace('.mdx', '')}/`,
}));
---
<SvelteSearch items={searchItems} client:visible /> Svelte stores между островами
Для синхронизации нескольких Svelte-островов — используйте Svelte Writable stores:
// src/stores/ui.ts
import { writable, derived } from 'svelte/store';
export const isMenuOpen = writable(false);
export const theme = writable<'light' | 'dark'>('dark');
export const cartCount = writable(0);
// Производный стор
export const themeClass = derived(theme, ($theme) => `theme-${$theme}`); <!-- Header.svelte — может изменять стор -->
<script>
import { isMenuOpen } from '../stores/ui';
</script>
<button on:click={() => $isMenuOpen = !$isMenuOpen}>Меню</button> <!-- MobileNav.svelte — читает тот же стор -->
<script>
import { isMenuOpen } from '../stores/ui';
</script>
{#if $isMenuOpen}
<nav class="mobile-nav">...</nav>
{/if} ⚠️ Svelte stores работают в пределах одной загрузки страницы. Для общего состояния между React и Svelte островами используйте nanostores.
Vue + Svelte вместе: nanostores как мост
npm install nanostores @nanostores/vue @nanostores/svelte // src/stores/shared.ts (nanostores)
import { atom } from 'nanostores';
export const globalTheme = atom<'light' | 'dark'>('dark'); <!-- ThemeToggleVue.vue -->
<script setup>
import { useStore } from '@nanostores/vue';
import { globalTheme } from '../stores/shared';
const theme = useStore(globalTheme);
</script>
<button @click="globalTheme.set(theme === 'dark' ? 'light' : 'dark')">
Тема: {{ theme }}
</button> <!-- ThemeBadgeSvelte.svelte -->
<script>
import { useStore } from '@nanostores/svelte';
import { globalTheme } from '../stores/shared';
const theme = useStore(globalTheme);
</script>
<span>Текущая тема: {$theme}</span> Оба острова синхронизированы через одно хранилище nanostores!
Когда использовать Vue vs Svelte vs React в Astro
| Ситуация | Рекомендация |
|---|---|
| Команда знает React | @astrojs/react |
| Команда знает Vue | @astrojs/vue |
| Команда знает Svelte | @astrojs/svelte |
| Максимально лёгкие компоненты | @astrojs/svelte или @astrojs/preact |
| Использование shadcn/ui | @astrojs/react |
| Использование Vuetify/Quasar | @astrojs/vue |
| Нет предпочтений, новый проект | @astrojs/preact (наименьший bundle) |
Итог
Astro + Vue или Svelte — отличная альтернатива React для команд, которые предпочитают эти фреймворки. Философия островов работает одинаково для всех: минимальный JavaScript на странице, изолированная гидратация только там, где нужна интерактивность. Наибольшей гибкости достигают проекты, использующие nanostores как общий слой состояния между островами разных фреймворков.