Memasang Vue sebagai island
Komponen Vue di Astro bukan aplikasi Vue. Ia potongan yang berdiri sendiri, tanpa router, dan dengan aturan props yang lebih ketat.
Intisari
pnpm astro add vuememasang dan mendaftarkan integrasinya sekaligus.- Tiap komponen ber-
client:*adalah aplikasi Vue tersendiri β bukan satu aplikasi besar. - Props dari Astro ke Vue harus bisa diserialisasi JSON. Tidak boleh fungsi, tidak boleh Date mentah.
- Slot Astro menjadi slot Vue, tapi isinya dirender Astro β bukan reaktif.
- Vue Router tidak dipakai. Navigasi tetap urusan Astro; island tidak boleh berpikir tentang rute.
Memasangnya
pnpm astro add vue
// astro.config.mjs
import vue from "@astrojs/vue";
export default defineConfig({
integrations: [
vue({
// Berkas .vue yang TIDAK perlu jadi island bisa dikecualikan
// supaya tidak ikut diproses.
devtools: false,
}),
],
});
Komponen pertama
// src/components/FilterKategori.vue
<script setup lang="ts">
import { ref, computed } from "vue";
const props = defineProps<{
kategori: { slug: string; nama: string; jumlah: number }[];
terpilih: string;
}>();
const cari = ref("");
const terlihat = computed(() =>
props.kategori.filter((k) =>
k.nama.toLowerCase().includes(cari.value.toLowerCase()),
),
);
</script>
<template>
<div class="filter">
<input v-model="cari" type="search" placeholder="Cari kategoriβ¦" />
<ul>
<li v-for="k in terlihat" :key="k.slug">
<a :href="`/berita/${k.slug}`" :aria-current="k.slug === terpilih ? 'page' : undefined">
{{ k.nama }} <span class="jml">{{ k.jumlah }}</span>
</a>
</li>
</ul>
</div>
</template>
---
import FilterKategori from "@/components/FilterKategori.vue";
import { ambilKategoriDenganJumlah } from "@/lib/kategori";
const kategori = await ambilKategoriDenganJumlah();
---
<FilterKategori client:visible kategori={kategori} terpilih={Astro.params.kategori ?? ""} />
Aturan props yang berbeda dari Vue biasa
Props dari Astro ke island diserialisasi jadi JSON dan ditanam di HTML, lalu di-parse ulang di browser. Konsekuensinya keras:
| Props | Boleh? | Catatan |
|---|---|---|
| string, number, boolean, null | Ya | β |
| array & objek biasa | Ya | β |
Date | Jadi string | Kirim ISO string, konversi di dalam komponen |
| Fungsi / callback | Tidak | Pakai event kustom atau nanostores |
| Komponen Astro | Tidak | Pakai slot |
| Objek besar | Secara teknis boleh | Ikut ke HTML tiap halaman β lihat di bawah |
Baris terakhir itu biaya nyata di portal berita. Props island ditanam di HTML sebagai JSON. Kalau kamu mengirim 200 artikel lengkap dengan isinya ke sebuah island tabel, HTML halamanmu bertambah ratusan kilobyte β dan karena halamanmu di-cache di edge, kamu membayar bandwidth itu untuk setiap pembaca. Kirim yang minimum, lalu ambil sisanya lewat endpoint saat dibutuhkan.
<!-- BURUK: 200 artikel lengkap ikut ke HTML -->
<TabelArtikel client:visible artikel={semuaArtikel} />
<!-- BAIK: 20 baris pertama untuk render awal, sisanya diambil island -->
<TabelArtikel client:visible awal={artikel.slice(0, 20)} total={total} />
Tiap island berdiri sendiri
<FilterKategori client:visible β¦ />
<TabelPerusahaan client:visible β¦ />
<MenuAkun client:load β¦ />
Ini tiga aplikasi Vue terpisah. Mereka tidak berbagi provide/inject, tidak
berbagi Pinia store, dan tidak bisa saling memanggil method. Kalau kamu datang dari Nuxt, ini pergeseran
mental terbesarnya.
Untuk state yang benar-benar harus dibagi, ada nanostores β dibahas di materi tersendiri di fase ini. Tapi pertanyaan pertama selalu: apakah ia benar-benar harus dibagi? Sebagian besar tidak.
Slot
<Akordeon client:visible>
<h3 slot="judul">Rincian keuangan</h3>
<p>Isi ini dirender Astro, bukan Vue.</p>
</Akordeon>
<template>
<details>
<summary><slot name="judul" /></summary>
<slot />
</details>
</template>
Isi slot dirender oleh Astro di server dan diserahkan sebagai HTML. Ia tidak reaktif β Vue tidak bisa mengubahnya, dan variabel Vue tidak bisa dipakai di dalamnya. Untuk isi statis di dalam komponen interaktif, ini justru ideal: teksnya ikut terindeks mesin pencari.
Yang tidak dipakai di Astro
| Fitur Vue | Di Astro |
|---|---|
| Vue Router | Tidak dipakai. Rute urusan Astro; island cukup pakai <a href> |
| Pinia lintas halaman | Tidak bertahan antar navigasi (MPA). Pakai nanostores + localStorage |
SSR Vue (@vue/server-renderer) | Astro yang mengurusnya. Kamu tidak menyentuhnya |
<Suspense> untuk data | Ambil data di Astro, kirim sebagai props β atau pakai server island |
| Layout Vue | Layout adalah komponen .astro |
Mengambil data dari dalam island
<script setup lang="ts">
import { ref, onMounted } from "vue";
const data = ref<Perusahaan[]>([]);
const memuat = ref(true);
const galat = ref<string | null>(null);
onMounted(async () => {
try {
const res = await fetch("/api/perusahaan.json?batas=50", {
signal: AbortSignal.timeout(5000),
});
if (!res.ok) throw new Error(`status ${res.status}`);
data.value = (await res.json()).data;
} catch (e) {
galat.value = "Gagal memuat data. Coba muat ulang halaman.";
} finally {
memuat.value = false;
}
});
</script>
Fetch dari island berjalan di browser, bukan di server. Artinya: ia tidak bisa memakai kredensial database, ia terlihat di Network tab pembaca, dan URL-nya harus bisa diakses publik. Jangan pernah memanggil API internal yang butuh kunci dari sini β kuncinya akan ikut ke bundel JavaScript dan terbaca siapa pun.
Latihan: buat FilterKategori.vue, panggil dengan client:visible, dan
buka DevTools β Network. Verifikasi: bundel Vue baru terunduh saat komponen masuk viewport, dan props
yang kamu kirim terlihat sebagai JSON di HTML. Lalu kirim seluruh objek artikel sebagai props dan
bandingkan ukuran HTML halamannya β angka itu yang akan kamu bayar per pembaca.
Rangkuman ini sengaja dipangkas ke bagian yang dipakai di roadmap. Buka sumber aslinya saat kamu butuh detail lengkap atau referensi parameter.