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

  • เรียนรู้ง่าย: HTML template syntax ที่เข้าใจง่าย ไม่ซับซ้อนเหมือน JSX ของ React
  • Performance สูง: Virtual DOM ที่ optimize มาดี รองรับ Tree-shaking ขนาดเล็ก
  • Ecosystem ครบ: Vue Router, Pinia, Nuxt.js, Vuetify, Vite — ทุกอย่างทำงานร่วมกันได้ดี
  • TypeScript Support: Vue 3 เขียนด้วย TypeScript ทั้งหมด รองรับ type inference ได้ดีมาก
  • Two-way Data Binding: v-model ทำให้ form handling ง่ายมาก
  • ชุมชนใหญ่: มีผู้ใช้ทั่วโลก โดยเฉพาะในเอเชีย (จีน, ญี่ปุ่น, เกาหลี, ไทย)

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

  • Vuetify 3: Material Design components ครบทุกอย่าง เหมาะกับ Enterprise apps
  • PrimeVue: 90+ components รองรับหลาย themes เหมาะกับ business applications
  • Naive UI: TypeScript-first สวย เบา เหมาะกับโปรเจกต์ที่ต้องการ customization สูง
  • Nuxt UI: Official UI library ของ Nuxt สร้างบน Tailwind CSS และ Headless UI
  • Element Plus: นิยมมากในจีน components ครบ documentation ดี
  • Radix Vue: Headless components สำหรับ accessibility-first design

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

  • Lazy loading components: ใช้ defineAsyncComponent หรือ dynamic imports
  • Virtual scrolling: ใช้ vue-virtual-scroller สำหรับ list ใหญ่ (หลาย 1000 items)
  • Image optimization: ใช้ @nuxt/image หรือ v-lazy directive
  • Code splitting: Vite ทำ automatic code splitting ตาม route
  • Memoize: ใช้ computed แทนการคำนวณใน template ตรงๆ
  • v-once: ใช้กับ static content ที่ไม่เปลี่ยนแปลง
  • shallowRef: ใช้แทน ref สำหรับ object ใหญ่ที่ไม่ต้อง deep reactivity
<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 ในที่เดียว