Home > Blog > tech

Vue.js และ Nuxt.js คืออะไร? สอนสร้าง Web App สมัยใหม่ด้วย Vue 3 + Composition API 2026

vuejs nuxtjs frontend guide
Vue.js Nuxt.js Frontend Guide 2026
2026-04-08 | tech | 3500 words

Vue.js เป็นหนึ่งใน JavaScript Framework ที่ได้รับความนิยมสูงสุดในโลก ด้วยความง่ายในการเรียนรู้ ความยืดหยุ่น และประสิทธิภาพสูง Vue.js ถูกใช้ตั้งแต่โปรเจกต์เล็กไปจนถึงแอปพลิเคชันระดับ Enterprise อย่าง Alibaba, GitLab และ Nintendo ในปี 2026 Vue 3 พร้อมกับ Composition API และ Nuxt 3 กลายเป็นตัวเลือกที่ทรงพลังสำหรับการสร้าง Web Application สมัยใหม่

บทความนี้จะสอน Vue.js 3 ตั้งแต่พื้นฐานจนถึงขั้นสูง ครอบคลุม Composition API, Vue Router, Pinia State Management, Composables และ Nuxt.js 3 สำหรับ SSR/SSG พร้อมตัวอย่างโค้ดที่ใช้งานได้จริง

Vue.js คืออะไร?

Vue.js (อ่านว่า "วิว") คือ Progressive JavaScript Framework สำหรับสร้าง User Interface สร้างโดย Evan You ในปี 2014 คำว่า "Progressive" หมายความว่าคุณสามารถเริ่มใช้ Vue.js แค่บางส่วนของเว็บไซต์ แล้วค่อยขยายขอบเขตไปเรื่อยๆ ไม่จำเป็นต้อง rewrite ทั้งหมดตั้งแต่ต้น

ทำไมเลือก Vue.js

Vue 2 vs Vue 3 — อะไรเปลี่ยนไป

คุณสมบัติVue 2Vue 3
API หลักOptions APIComposition API + Options API
ReactivityObject.definePropertyProxy (เร็วกว่า)
Performanceดีเร็วกว่า 2x, เล็กกว่า 50%
TypeScriptจำกัดFirst-class support
Fragmentต้องมี root element เดียวหลาย root elements ได้
Teleportไม่มีมี (render ส่วนอื่นของ DOM)
Suspenseไม่มีมี (async components)
สถานะ 2026End of Life (31 ธ.ค. 2023)Active LTS
สำคัญ: Vue 2 หมดอายุการสนับสนุนแล้วเมื่อ 31 ธันวาคม 2023 โปรเจกต์ใหม่ทั้งหมดในปี 2026 ต้องใช้ Vue 3 เท่านั้น ถ้ายังมี codebase Vue 2 ให้วางแผน migrate โดยเร็ว

เริ่มต้น Vue 3 ด้วย Vite

# สร้างโปรเจกต์ Vue 3 ด้วย Vite
npm create vue@latest my-vue-app

# เลือก options ที่ต้องการ:
# ✔ Add TypeScript? Yes
# ✔ Add JSX Support? No
# ✔ Add Vue Router? Yes
# ✔ Add Pinia? Yes
# ✔ Add Vitest for Unit Testing? Yes
# ✔ Add ESLint? Yes
# ✔ Add Prettier? Yes

cd my-vue-app
npm install
npm run dev        # เปิด dev server ที่ http://localhost:5173

โครงสร้างโปรเจกต์ที่ได้:

my-vue-app/
  src/
    assets/          # CSS, images
    components/      # Vue components
    composables/     # Custom composables (hooks)
    router/          # Vue Router config
    stores/          # Pinia stores
    views/           # Page components
    App.vue          # Root component
    main.ts          # Entry point
  index.html
  vite.config.ts
  tsconfig.json
  package.json

Options API vs Composition API

Vue 3 รองรับ 2 แบบในการเขียน component:

Options API (แบบเดิม)

<script>
export default {
  data() {
    return {
      count: 0,
      name: 'Vue'
    }
  },
  computed: {
    doubleCount() {
      return this.count * 2
    }
  },
  methods: {
    increment() {
      this.count++
    }
  },
  mounted() {
    console.log('Component mounted!')
  }
}
</script>

<template>
  <div>
    <h1>{{ name }}</h1>
    <p>Count: {{ count }}, Double: {{ doubleCount }}</p>
    <button @click="increment">+1</button>
  </div>
</template>

Composition API (แนะนำ)

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

const count = ref(0)
const name = ref('Vue')

const doubleCount = computed(() => count.value * 2)

function increment() {
  count.value++
}

onMounted(() => {
  console.log('Component mounted!')
})
</script>

<template>
  <div>
    <h1>{{ name }}</h1>
    <p>Count: {{ count }}, Double: {{ doubleCount }}</p>
    <button @click="increment">+1</button>
  </div>
</template>
ทำไมเลือก Composition API: 1) จัดกลุ่ม logic ที่เกี่ยวข้องไว้ด้วยกัน (ไม่กระจายไปหลาย options) 2) Reuse logic ผ่าน composables ได้ง่าย 3) TypeScript support ดีกว่ามาก 4) เป็น recommended approach อย่างเป็นทางการของ Vue 3

Reactivity System — หัวใจของ Vue

ref — ค่า Primitive

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

// ref สำหรับค่า primitive (string, number, boolean)
const message = ref('Hello')
const count = ref(0)
const isVisible = ref(true)

// เข้าถึงค่าใน script ต้องใช้ .value
console.log(count.value)  // 0
count.value++             // 1

// ใน template ไม่ต้องใช้ .value (Vue unwrap ให้อัตโนมัติ)
// {{ count }} จะแสดง 1
</script>

reactive — Object/Array

<script setup lang="ts">
import { reactive } from 'vue'

// reactive สำหรับ object
const user = reactive({
  name: 'John',
  age: 25,
  address: {
    city: 'Bangkok',
    country: 'Thailand'
  }
})

// เข้าถึงค่าตรงๆ ไม่ต้อง .value
user.name = 'Jane'
user.address.city = 'Chiang Mai'

// reactive กับ array
const items = reactive(['Apple', 'Banana'])
items.push('Cherry')
</script>

computed — ค่าที่คำนวณจากค่าอื่น

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

const firstName = ref('สมชาย')
const lastName = ref('ใจดี')
const items = ref([
  { name: 'Item A', price: 100, qty: 2 },
  { name: 'Item B', price: 200, qty: 1 },
])

// computed อ่านอย่างเดียว
const fullName = computed(() => `${firstName.value} ${lastName.value}`)

// computed กับ getter/setter
const fullNameEditable = computed({
  get: () => `${firstName.value} ${lastName.value}`,
  set: (val: string) => {
    const [first, ...rest] = val.split(' ')
    firstName.value = first
    lastName.value = rest.join(' ')
  }
})

// computed จะ cache ค่าไว้ จะคำนวณใหม่เฉพาะเมื่อ dependencies เปลี่ยน
const totalPrice = computed(() =>
  items.value.reduce((sum, item) => sum + item.price * item.qty, 0)
)
</script>

watch และ watchEffect

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

const searchQuery = ref('')
const userId = ref(1)

// watch — ติดตาม source เฉพาะเจาะจง
watch(searchQuery, (newVal, oldVal) => {
  console.log(`Search changed: ${oldVal} -> ${newVal}`)
  // เรียก API ค้นหา
}, { debounce: 300 })  // Vue 3.4+ รองรับ debounce

// watch หลายค่า
watch([userId, searchQuery], ([newId, newQuery]) => {
  fetchData(newId, newQuery)
})

// watch deep object
watch(() => user.address, (newAddr) => {
  console.log('Address changed:', newAddr)
}, { deep: true })

// watchEffect — track dependencies อัตโนมัติ
watchEffect(async () => {
  // ทุก reactive value ที่อ่านจะถูก track อัตโนมัติ
  const response = await fetch(`/api/users/${userId.value}?q=${searchQuery.value}`)
  const data = await response.json()
  console.log(data)
})
</script>

Components — Props, Emits, Slots

Props — รับค่าจาก Parent

<!-- UserCard.vue -->
<script setup lang="ts">
// defineProps กับ TypeScript
interface Props {
  name: string
  age: number
  email?: string         // optional
  role?: 'admin' | 'user'  // union type
  isActive?: boolean
}

const props = withDefaults(defineProps<Props>(), {
  email: 'N/A',
  role: 'user',
  isActive: true
})

// เข้าถึงค่า props
console.log(props.name)
</script>

<template>
  <div class="user-card" :class="{ active: isActive }">
    <h3>{{ name }}</h3>
    <p>อายุ: {{ age }} ปี</p>
    <p>Email: {{ email }}</p>
    <span class="badge">{{ role }}</span>
  </div>
</template>

<!-- Parent component -->
<template>
  <UserCard name="สมชาย" :age="25" email="somchai@test.com" role="admin" />
</template>

Emits — ส่ง Event ไป Parent

<!-- SearchInput.vue -->
<script setup lang="ts">
import { ref } from 'vue'

// defineEmits กับ TypeScript
const emit = defineEmits<{
  (e: 'search', query: string): void
  (e: 'clear'): void
  (e: 'update:modelValue', value: string): void
}>()

const query = ref('')

function handleSearch() {
  emit('search', query.value)
}

function handleClear() {
  query.value = ''
  emit('clear')
  emit('update:modelValue', '')
}
</script>

<template>
  <div class="search-input">
    <input v-model="query" @keyup.enter="handleSearch" placeholder="ค้นหา..." />
    <button @click="handleSearch">ค้นหา</button>
    <button @click="handleClear">ล้าง</button>
  </div>
</template>

<!-- Parent -->
<template>
  <SearchInput @search="handleSearch" @clear="handleClear" v-model="searchText" />
</template>

Slots — Content Distribution

<!-- Card.vue -->
<template>
  <div class="card">
    <div class="card-header">
      <slot name="header">Default Header</slot>
    </div>
    <div class="card-body">
      <slot>Default Content</slot>
    </div>
    <div class="card-footer">
      <slot name="footer" :count="itemCount">
        Footer ({{ itemCount }} items)
      </slot>
    </div>
  </div>
</template>

<!-- Usage with named slots -->
<template>
  <Card>
    <template #header>
      <h2>รายการสินค้า</h2>
    </template>

    <p>เนื้อหาหลักของ Card</p>
    <ul>
      <li>สินค้า A</li>
      <li>สินค้า B</li>
    </ul>

    <template #footer="{ count }">
      <p>รวม {{ count }} รายการ</p>
    </template>
  </Card>
</template>

Lifecycle Hooks

<script setup lang="ts">
import {
  onBeforeMount,
  onMounted,
  onBeforeUpdate,
  onUpdated,
  onBeforeUnmount,
  onUnmounted,
  onErrorCaptured
} from 'vue'

onBeforeMount(() => {
  console.log('ก่อน mount — DOM ยังไม่พร้อม')
})

onMounted(() => {
  console.log('mount แล้ว — DOM พร้อมใช้งาน')
  // เหมาะสำหรับ: fetch data, init third-party library, add event listeners
})

onBeforeUpdate(() => {
  console.log('ก่อน update DOM')
})

onUpdated(() => {
  console.log('update DOM แล้ว')
})

onBeforeUnmount(() => {
  console.log('ก่อน unmount — cleanup ที่นี่')
  // เหมาะสำหรับ: remove event listeners, cancel subscriptions
})

onUnmounted(() => {
  console.log('unmount แล้ว')
})

onErrorCaptured((err, instance, info) => {
  console.error('Error captured:', err)
  return false  // ไม่ propagate ต่อ
})
</script>

Vue Router — Routing สำหรับ SPA

// router/index.ts
import { createRouter, createWebHistory } from 'vue-router'

const router = createRouter({
  history: createWebHistory(),
  routes: [
    {
      path: '/',
      name: 'home',
      component: () => import('../views/HomeView.vue')
    },
    {
      path: '/about',
      name: 'about',
      component: () => import('../views/AboutView.vue')
    },
    {
      path: '/users/:id',
      name: 'user-detail',
      component: () => import('../views/UserDetail.vue'),
      props: true     // ส่ง route params เป็น props
    },
    {
      path: '/dashboard',
      component: () => import('../views/DashboardLayout.vue'),
      meta: { requiresAuth: true },
      children: [
        {
          path: '',
          name: 'dashboard',
          component: () => import('../views/DashboardHome.vue')
        },
        {
          path: 'settings',
          name: 'settings',
          component: () => import('../views/SettingsView.vue')
        }
      ]
    },
    {
      path: '/:pathMatch(.*)*',
      name: 'not-found',
      component: () => import('../views/NotFound.vue')
    }
  ]
})

// Navigation Guard — ตรวจสอบ auth
router.beforeEach((to, from) => {
  const isAuthenticated = checkAuth()
  if (to.meta.requiresAuth && !isAuthenticated) {
    return { name: 'login', query: { redirect: to.fullPath } }
  }
})

export default router
<!-- ใช้ Router ใน Template -->
<template>
  <nav>
    <RouterLink to="/">Home</RouterLink>
    <RouterLink :to="{ name: 'user-detail', params: { id: 123 } }">User 123</RouterLink>
  </nav>
  <RouterView />
</template>

<!-- ใช้ Router ใน Script -->
<script setup lang="ts">
import { useRouter, useRoute } from 'vue-router'

const router = useRouter()
const route = useRoute()

// อ่าน params
console.log(route.params.id)
console.log(route.query.search)

// Navigate
function goToUser(id: number) {
  router.push({ name: 'user-detail', params: { id } })
}

function goBack() {
  router.back()
}
</script>

Pinia — State Management (แทน Vuex)

Pinia เป็น official state management library ของ Vue 3 แทนที่ Vuex ด้วยความง่าย TypeScript support ที่ดีกว่า และไม่ต้องมี mutations อีกต่อไป

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

// Composition API style (แนะนำ)
export const useUserStore = defineStore('user', () => {
  // State
  const users = ref<User[]>([])
  const currentUser = ref<User | null>(null)
  const isLoading = ref(false)
  const error = ref<string | null>(null)

  // Getters (computed)
  const activeUsers = computed(() =>
    users.value.filter(u => u.isActive)
  )

  const userCount = computed(() => users.value.length)

  const isLoggedIn = computed(() => currentUser.value !== null)

  // Actions
  async function fetchUsers() {
    isLoading.value = true
    error.value = null
    try {
      const response = await fetch('/api/users')
      users.value = await response.json()
    } catch (e) {
      error.value = 'Failed to fetch users'
    } finally {
      isLoading.value = false
    }
  }

  async function login(email: string, password: string) {
    const response = await fetch('/api/login', {
      method: 'POST',
      body: JSON.stringify({ email, password })
    })
    currentUser.value = await response.json()
  }

  function logout() {
    currentUser.value = null
  }

  return {
    users, currentUser, isLoading, error,
    activeUsers, userCount, isLoggedIn,
    fetchUsers, login, logout
  }
})

// interface
interface User {
  id: number
  name: string
  email: string
  isActive: boolean
}
<!-- ใช้ Pinia Store ใน Component -->
<script setup lang="ts">
import { storeToRefs } from 'pinia'
import { useUserStore } from '@/stores/useUserStore'

const userStore = useUserStore()

// ใช้ storeToRefs เพื่อ destructure แบบ reactive
const { users, isLoading, activeUsers } = storeToRefs(userStore)

// Actions ไม่ต้อง storeToRefs
const { fetchUsers, login, logout } = userStore

// เรียก fetch เมื่อ mount
onMounted(() => {
  fetchUsers()
})
</script>

<template>
  <div v-if="isLoading">กำลังโหลด...</div>
  <div v-else>
    <p>ผู้ใช้ทั้งหมด: {{ users.length }} คน (Active: {{ activeUsers.length }})</p>
    <ul>
      <li v-for="user in activeUsers" :key="user.id">
        {{ user.name }} — {{ user.email }}
      </li>
    </ul>
  </div>
</template>

Composables — Reusable Logic (Custom Hooks)

Composables คือ functions ที่รวม reactive logic ไว้ด้วยกัน เหมือน Custom Hooks ใน React ช่วยให้ reuse logic ข้าม components ได้ง่าย

// composables/useFetch.ts
import { ref, watchEffect, type Ref } from 'vue'

interface UseFetchReturn<T> {
  data: Ref<T | null>
  error: Ref<string | null>
  isLoading: Ref<boolean>
  refresh: () => Promise<void>
}

export function useFetch<T>(url: Ref<string> | string): UseFetchReturn<T> {
  const data = ref<T | null>(null) as Ref<T | null>
  const error = ref<string | null>(null)
  const isLoading = ref(false)

  async function fetchData() {
    isLoading.value = true
    error.value = null
    try {
      const resolvedUrl = typeof url === 'string' ? url : url.value
      const response = await fetch(resolvedUrl)
      if (!response.ok) throw new Error(`HTTP ${response.status}`)
      data.value = await response.json()
    } catch (e: any) {
      error.value = e.message
    } finally {
      isLoading.value = false
    }
  }

  // ถ้า url เป็น ref ให้ watch แล้ว refetch อัตโนมัติ
  if (typeof url !== 'string') {
    watchEffect(() => {
      fetchData()
    })
  } else {
    fetchData()
  }

  return { data, error, isLoading, refresh: fetchData }
}
// composables/useLocalStorage.ts
import { ref, watch, type Ref } from 'vue'

export function useLocalStorage<T>(key: string, defaultValue: T): Ref<T> {
  const stored = localStorage.getItem(key)
  const data = ref<T>(stored ? JSON.parse(stored) : defaultValue) as Ref<T>

  watch(data, (newVal) => {
    localStorage.setItem(key, JSON.stringify(newVal))
  }, { deep: true })

  return data
}

// composables/useDebounce.ts
import { ref, watch, type Ref } from 'vue'

export function useDebounce<T>(value: Ref<T>, delay = 300): Ref<T> {
  const debounced = ref(value.value) as Ref<T>
  let timer: ReturnType<typeof setTimeout>

  watch(value, (newVal) => {
    clearTimeout(timer)
    timer = setTimeout(() => {
      debounced.value = newVal
    }, delay)
  })

  return debounced
}
<!-- ใช้ Composables ใน Component -->
<script setup lang="ts">
import { ref, watch } from 'vue'
import { useFetch } from '@/composables/useFetch'
import { useLocalStorage } from '@/composables/useLocalStorage'
import { useDebounce } from '@/composables/useDebounce'

// useFetch
const { data: users, isLoading, error } = useFetch<User[]>('/api/users')

// useLocalStorage — ค่าจะ persist ใน localStorage
const theme = useLocalStorage('theme', 'light')
const favorites = useLocalStorage<number[]>('favorites', [])

// useDebounce — debounce search input
const searchInput = ref('')
const debouncedSearch = useDebounce(searchInput, 500)

watch(debouncedSearch, (query) => {
  // จะ trigger เมื่อหยุดพิมพ์ 500ms
  fetchSearchResults(query)
})
</script>

Nuxt.js 3 — Framework สำหรับ Vue 3

Nuxt.js 3 คือ meta-framework ที่สร้างบน Vue 3 เพิ่มความสามารถ Server-Side Rendering (SSR), Static Site Generation (SSG), file-based routing, auto-imports และอีกมากมาย เหมาะสำหรับสร้างเว็บไซต์ที่ต้องการ SEO ดี หรือแอปพลิเคชันที่ต้องการ performance สูง

สร้างโปรเจกต์ Nuxt 3

# สร้างโปรเจกต์ Nuxt 3
npx nuxi@latest init my-nuxt-app
cd my-nuxt-app
npm install
npm run dev        # http://localhost:3000

File-Based Routing

Nuxt 3 สร้าง routes อัตโนมัติจากโครงสร้างไฟล์ใน pages/ ไม่ต้องเขียน router config เอง:

pages/
  index.vue         → /
  about.vue         → /about
  blog/
    index.vue       → /blog
    [slug].vue      → /blog/:slug (dynamic route)
  users/
    index.vue       → /users
    [id].vue        → /users/:id
    [...slug].vue   → /users/* (catch-all)

Auto-Imports

Nuxt 3 auto-import ทุกอย่าง ไม่ต้องเขียน import statement:

<!-- ไม่ต้อง import ref, computed, watch, useRouter ฯลฯ -->
<script setup lang="ts">
// Vue APIs — auto-imported
const count = ref(0)
const doubled = computed(() => count.value * 2)

// Nuxt composables — auto-imported
const route = useRoute()
const { data } = await useFetch('/api/users')

// Components ใน components/ — auto-imported
// Composables ใน composables/ — auto-imported
// Utils ใน utils/ — auto-imported
</script>

<template>
  <!-- NuxtLink, NuxtPage, NuxtLayout — auto-imported -->
  <NuxtLink to="/about">About</NuxtLink>
</template>

SSR, SSG, ISR — Rendering Modes

// nuxt.config.ts
export default defineNuxtConfig({
  // SSR (default) — render ฝั่ง server ทุก request
  ssr: true,

  // หรือ SPA — render ฝั่ง client เท่านั้น
  // ssr: false,

  // Route rules — กำหนด rendering per route
  routeRules: {
    '/': { prerender: true },              // SSG — generate ตอน build
    '/blog/**': { isr: 3600 },             // ISR — regenerate ทุก 1 ชม.
    '/dashboard/**': { ssr: false },        // SPA — client-only
    '/api/**': { cors: true },             // API routes
    '/admin/**': { redirect: '/login' }    // Redirect
  }
})

Server Directory — API Routes

// server/api/users.get.ts
export default defineEventHandler(async (event) => {
  // นี่คือ API endpoint: GET /api/users
  const users = await db.query('SELECT * FROM users')
  return users
})

// server/api/users/[id].get.ts
export default defineEventHandler(async (event) => {
  const id = getRouterParam(event, 'id')
  const user = await db.query('SELECT * FROM users WHERE id = ?', [id])
  if (!user) throw createError({ statusCode: 404, message: 'User not found' })
  return user
})

// server/api/users.post.ts
export default defineEventHandler(async (event) => {
  const body = await readBody(event)
  const user = await db.insert('users', body)
  return user
})

// server/middleware/auth.ts
export default defineEventHandler((event) => {
  const token = getHeader(event, 'Authorization')
  if (event.path.startsWith('/api/admin') && !token) {
    throw createError({ statusCode: 401, message: 'Unauthorized' })
  }
})

Nuxt Modules ยอดนิยม

Moduleหน้าที่
@nuxtjs/tailwindcssTailwind CSS integration
@pinia/nuxtPinia state management
@nuxt/imageImage optimization (lazy loading, resize)
@nuxt/contentCMS จาก Markdown/YAML files
@sidebase/nuxt-authAuthentication (OAuth, Credentials)
@nuxt/i18nInternationalization
@nuxt/uiOfficial UI component library
nuxt-securitySecurity headers, rate limiting

Vue vs React — เปรียบเทียบปี 2026

หัวข้อVue 3React 19
TemplateHTML template (SFC)JSX
Stateref/reactive (built-in)useState/useReducer
ReactivityFine-grained (Proxy)Re-render whole component
Two-way bindingv-model (built-in)ต้องเขียนเอง
StylingScoped CSS (built-in)CSS Modules/Styled Components
Learning curveง่ายกว่าปานกลาง
Meta-frameworkNuxt.jsNext.js
Bundle sizeเล็กกว่าใหญ่กว่าเล็กน้อย
Job market (global)น้อยกว่า Reactมากที่สุด
Job market (เอเชีย)นิยมมากนิยมมาก
เลือก Vue หรือ React: ทั้งสองตัวดีหมด ถ้าทำงานคนเดียวหรือทีมเล็ก Vue อาจง่ายกว่าในการเริ่มต้น ถ้าหา Job ต่างประเทศ React มี demand มากกว่า สิ่งสำคัญคือเลือกตัวหนึ่งแล้วเรียนรู้ให้ลึก

UI Libraries สำหรับ Vue 3

Testing Vue Components

// tests/UserCard.test.ts
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import UserCard from '@/components/UserCard.vue'

describe('UserCard', () => {
  it('renders user name', () => {
    const wrapper = mount(UserCard, {
      props: {
        name: 'สมชาย',
        age: 25
      }
    })
    expect(wrapper.text()).toContain('สมชาย')
    expect(wrapper.text()).toContain('25')
  })

  it('emits delete event when button clicked', async () => {
    const wrapper = mount(UserCard, {
      props: { name: 'Test', age: 20 }
    })
    await wrapper.find('.delete-btn').trigger('click')
    expect(wrapper.emitted('delete')).toBeTruthy()
  })

  it('applies active class when isActive is true', () => {
    const wrapper = mount(UserCard, {
      props: { name: 'Test', age: 20, isActive: true }
    })
    expect(wrapper.classes()).toContain('active')
  })
})

// tests/useCounter.test.ts — test composable
import { describe, it, expect } from 'vitest'
import { useCounter } from '@/composables/useCounter'

describe('useCounter', () => {
  it('starts with initial value', () => {
    const { count } = useCounter(10)
    expect(count.value).toBe(10)
  })

  it('increments and decrements', () => {
    const { count, increment, decrement } = useCounter(0)
    increment()
    expect(count.value).toBe(1)
    decrement()
    expect(count.value).toBe(0)
  })
})
# รัน tests
npx vitest              # watch mode
npx vitest run          # run once
npx vitest --coverage   # with coverage report

Deployment และ Performance Optimization

Build และ Deploy

# Vue 3 (Vite)
npm run build           # สร้าง dist/ folder
npm run preview         # preview production build

# Nuxt 3
npm run build           # SSR build → .output/
npm run generate        # SSG build → .output/public/

# Deploy options:
# - Vercel: zero-config deployment สำหรับ Nuxt
# - Netlify: เหมาะกับ SSG
# - Docker: สำหรับ SSR
# - Cloudflare Pages: edge rendering

Performance Tips

<script setup lang="ts">
import { defineAsyncComponent, shallowRef } from 'vue'

// Lazy load heavy component
const HeavyChart = defineAsyncComponent(() =>
  import('./components/HeavyChart.vue')
)

// shallowRef สำหรับ data ขนาดใหญ่
const bigData = shallowRef(fetchBigData())
</script>

<template>
  <!-- v-once — render ครั้งเดียว ไม่ update -->
  <footer v-once>
    <p>Copyright 2026 SiamCafe</p>
  </footer>

  <!-- Suspense สำหรับ async components -->
  <Suspense>
    <template #default>
      <HeavyChart :data="chartData" />
    </template>
    <template #fallback>
      <div>กำลังโหลด Chart...</div>
    </template>
  </Suspense>
</template>

สรุป

Vue.js 3 พร้อม Composition API และ Nuxt.js 3 เป็น stack ที่ทรงพลังสำหรับการสร้าง Web Application สมัยใหม่ในปี 2026 ด้วยความง่ายในการเรียนรู้ ระบบ reactivity ที่มีประสิทธิภาพ TypeScript support ที่ดีเยี่ยม และ ecosystem ที่ครบวงจร Vue.js เป็นตัวเลือกที่ยอดเยี่ยมสำหรับทั้ง beginner และ experienced developer

เริ่มต้นด้วย npm create vue@latest สร้างโปรเจกต์แรก เรียนรู้ Composition API กับ ref, computed, watch จากนั้นเพิ่ม Vue Router และ Pinia เมื่อพร้อมก้าวสู่ full-stack ให้ใช้ Nuxt 3 ที่ให้ทั้ง SSR, SSG, API routes และ auto-imports ในที่เดียว


Back to Blog | iCafe Forex | SiamLanCard | Siam2R