Astrocalendar_today12 февр. 2026 г.schedule2 мин

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

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

Фото Дмитрий Соколов
Дмитрий Соколов
технический редактор и консультант по быстрым сайтам
Astro + Vue + Svelte — мультифреймворк интеграция

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 как общий слой состояния между островами разных фреймворков.

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

Frontend Engineer и Astro-консультант

Помогает бизнесу запускать быстрые сайты на Astro, переносить проекты с CMS и готовить страницы к SEO/GEO-продвижению.

Комментарии

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

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

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

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