Astro calendar_today 23 апр. 2026 г. schedule 2 мин

Astro + Vue и Svelte: Острова без React

Как использовать Vue 3 и Svelte компоненты в Astro.js как острова интерактивности. Установка, директивы client:*, передача пропсов и Pinia/Svelte stores.

person
Журналист
Автор
Astro + Vue + Svelte — мультифреймворк интеграция

Astro поддерживает не только React. Vue 3 и Svelte — полноценные граждане в экосистеме Astro. Вы можете использовать их как острова интерактивности, смешивать в одном проекте и даже комбинировать с React-островами. Разбираем, как это работает.

Философия: Острова любого фреймворка

Astro — фреймворк-агностик. Он не навязывает React. Вы выбираете инструмент под задачу:

code
---
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

Установка

code
npx astro add vue

Автоматически установит @astrojs/vue, vue и обновит astro.config.mjs.

code
// astro.config.mjs (после установки)
import vue from '@astrojs/vue';

export default defineConfig({
  integrations: [
    vue({
      appEntrypoint: '/src/pages/_app', // Необязательно: глобальные плагины
    }),
  ],
});

Vue SFC компонент

code
<!-- 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:

code
---
import VueCounter from '../components/VueCounter.vue';
---

<VueCounter initialValue={10} client:visible />

Vue Router в Astro?

Не рекомендуется. Astro управляет маршрутизацией. Vue Router создаст конфликт. Для навигации используйте файловую маршрутизацию Astro. Vue — только для изолированных компонентов.

Pinia store для Vue-островов

Для состояния между несколькими Vue-островами используйте Pinia:

code
npm install pinia
code
// src/lib/piniaPlugin.ts — инициализация Pinia
import { createPinia } from 'pinia';
export const pinia = createPinia();
code
// 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);
};
code
// 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

Установка

code
npx astro add svelte
code
// astro.config.mjs
import svelte from '@astrojs/svelte';
export default defineConfig({ integrations: [svelte()] });

Svelte компонент

code
<!-- 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:

code
---
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:

code
// 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}`);
code
<!-- Header.svelte — может изменять стор -->
<script>
  import { isMenuOpen } from '../stores/ui';
</script>

<button on:click={() => $isMenuOpen = !$isMenuOpen}>Меню</button>
code
<!-- 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 как мост

code
npm install nanostores @nanostores/vue @nanostores/svelte
code
// src/stores/shared.ts (nanostores)
import { atom } from 'nanostores';
export const globalTheme = atom<'light' | 'dark'>('dark');
code
<!-- 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>
code
<!-- 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 как общий слой состояния между островами разных фреймворков.

Портрет автора Дмитрий Соколов

Senior Frontend Engineer / Tech Writer

Senior Frontend Engineer с 9-летним опытом. Специализируется на Astro.js и JAMstack.

Комментарии

Загрузка комментариев...

Оставить комментарий

Комментарии проходят модерацию перед публикацией. Правила

Рекомендуем к прочтению