Astro กับ Vue: ใช้ Vue 3 Components ใน Astro

#astro13 เม.ย. 2569

Astro กับ Vue: ใช้ Vue 3 Components ใน Astro

Vue 3 เป็นหนึ่งใน JavaScript framework ที่ได้รับความนิยมสูงสุด และ Astro รองรับการใช้งาน Vue components ได้อย่างสมบูรณ์แบบ บทความนี้จะแนะนำวิธีการผสาน Vue 3 เข้ากับ Astro project ตั้งแต่การติดตั้งจนถึงการใช้งานขั้นสูง

ทำไมต้องใช้ Vue กับ Astro?

Vue 3 มีคุณสมบัติที่น่าสนใจหลายอย่าง:

  • Composition API: เขียน logic ที่ reusable และ maintainable ได้ง่าย
  • TypeScript Support: รองรับ TypeScript อย่างสมบูรณ์
  • Reactivity System: ระบบ reactivity ที่ทรงพลังและเข้าใจง่าย
  • Ecosystem ขนาดใหญ่: มี library และ plugin มากมาย

การติดตั้ง Vue Integration

npx astro add vue

หรือติดตั้งแบบ manual:

npm install @astrojs/vue vue

แก้ไข astro.config.mjs:

import { defineConfig } from 'astro/config';
import vue from '@astrojs/vue';

export default defineConfig({
  integrations: [
    vue({
      appEntrypoint: '/src/pages/_app', // optional
    }),
  ],
});

สร้าง Vue Component ด้วย Composition API

สร้างไฟล์ src/components/TodoList.vue:

<template>
  <div class="todo-app">
    <h2>รายการสิ่งที่ต้องทำ</h2>
    
    <div class="input-group">
      <input
        v-model="newTodo"
        @keyup.enter="addTodo"
        placeholder="เพิ่มรายการใหม่..."
        type="text"
      />
      <button @click="addTodo">เพิ่ม</button>
    </div>

    <ul class="todo-list">
      <li
        v-for="todo in todos"
        :key="todo.id"
        :class="{ completed: todo.done }"
      >
        <input
          type="checkbox"
          v-model="todo.done"
        />
        <span>{{ todo.text }}</span>
        <button @click="removeTodo(todo.id)">ลบ</button>
      </li>
    </ul>

    <p class="summary">
      เสร็จแล้ว {{ completedCount }} / {{ todos.length }} รายการ
    </p>
  </div>
</template>

<script setup lang="ts">
import { ref, computed } from 'vue';

interface Todo {
  id: number;
  text: string;
  done: boolean;
}

const newTodo = ref('');
const todos = ref<Todo[]>([
  { id: 1, text: 'เรียน Astro', done: false },
  { id: 2, text: 'สร้าง Vue Component', done: false },
]);

const completedCount = computed(() =>
  todos.value.filter((t) => t.done).length
);

let nextId = 3;

function addTodo() {
  if (newTodo.value.trim()) {
    todos.value.push({
      id: nextId++,
      text: newTodo.value.trim(),
      done: false,
    });
    newTodo.value = '';
  }
}

function removeTodo(id: number) {
  todos.value = todos.value.filter((t) => t.id !== id);
}
</script>

<style scoped>
.todo-app {
  max-width: 500px;
  margin: 0 auto;
  padding: 1.5rem;
}

.input-group {
  display: flex;
  gap: 0.5rem;
  margin-bottom: 1rem;
}

input[type="text"] {
  flex: 1;
  padding: 0.5rem;
  border: 1px solid #ccc;
  border-radius: 4px;
}

.todo-list {
  list-style: none;
  padding: 0;
}

.todo-list li {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.5rem 0;
  border-bottom: 1px solid #eee;
}

.completed span {
  text-decoration: line-through;
  color: #999;
}

.summary {
  margin-top: 1rem;
  color: #666;
}
</style>

ใช้ Vue Component ใน Astro Page

---
import TodoList from '../components/TodoList.vue';
---

<html lang="th">
  <head>
    <title>Astro + Vue Demo</title>
  </head>
  <body>
    <h1>Vue 3 ใน Astro</h1>
    <TodoList client:load />
  </body>
</html>

Vue Composables กับ Astro

สร้าง composable สำหรับ fetch data:

// src/composables/useFetch.ts
import { ref, onMounted } from 'vue';

export function useFetch<T>(url: string) {
  const data = ref<T | null>(null);
  const loading = ref(true);
  const error = ref<string | null>(null);

  async function fetchData() {
    try {
      loading.value = true;
      const response = await fetch(url);
      if (!response.ok) throw new Error('Network error');
      data.value = await response.json();
    } catch (e) {
      error.value = e instanceof Error ? e.message : 'Unknown error';
    } finally {
      loading.value = false;
    }
  }

  onMounted(fetchData);

  return { data, loading, error, refetch: fetchData };
}

ใช้ composable ใน component:

<template>
  <div>
    <div v-if="loading">กำลังโหลด...</div>
    <div v-else-if="error">เกิดข้อผิดพลาด: {{ error }}</div>
    <ul v-else>
      <li v-for="post in data" :key="post.id">
        {{ post.title }}
      </li>
    </ul>
  </div>
</template>

<script setup lang="ts">
import { useFetch } from '../composables/useFetch';

interface Post {
  id: number;
  title: string;
}

const { data, loading, error } = useFetch<Post[]>('/api/posts');
</script>

Pinia Store กับ Astro

ติดตั้ง Pinia:

npm install pinia

สร้าง app entry point src/pages/_app.ts:

import { createPinia } from 'pinia';
import type { App } from 'vue';

export default (app: App) => {
  app.use(createPinia());
};

สร้าง store:

// src/stores/useUserStore.ts
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';

export const useUserStore = defineStore('user', () => {
  const name = ref('');
  const email = ref('');
  const isLoggedIn = computed(() => name.value !== '');

  function login(userName: string, userEmail: string) {
    name.value = userName;
    email.value = userEmail;
  }

  function logout() {
    name.value = '';
    email.value = '';
  }

  return { name, email, isLoggedIn, login, logout };
});

Vue Router กับ Astro

Astro จัดการ routing เอง แต่ถ้าต้องการ Vue Router สำหรับ SPA section:

// src/pages/_app.ts
import { createPinia } from 'pinia';
import { createRouter, createWebHistory } from 'vue-router';
import type { App } from 'vue';

const router = createRouter({
  history: createWebHistory(),
  routes: [
    { path: '/app', component: () => import('../views/Home.vue') },
    { path: '/app/profile', component: () => import('../views/Profile.vue') },
  ],
});

export default (app: App) => {
  app.use(createPinia());
  app.use(router);
};

สรุป

การใช้ Vue 3 กับ Astro ช่วยให้คุณได้ประโยชน์จากทั้งสองโลก Astro ดูแล static generation และ performance ในขณะที่ Vue 3 ด้วย Composition API ช่วยให้สร้าง interactive components ที่ซับซ้อนได้อย่างมีประสิทธิภาพ