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 2 | Vue 3 |
|---|---|---|
| API หลัก | Options API | Composition API + Options API |
| Reactivity | Object.defineProperty | Proxy (เร็วกว่า) |
| Performance | ดี | เร็วกว่า 2x, เล็กกว่า 50% |
| TypeScript | จำกัด | First-class support |
| Fragment | ต้องมี root element เดียว | หลาย root elements ได้ |
| Teleport | ไม่มี | มี (render ส่วนอื่นของ DOM) |
| Suspense | ไม่มี | มี (async components) |
| สถานะ 2026 | End of Life (31 ธ.ค. 2023) | Active LTS |
เริ่มต้น 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>
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/tailwindcss | Tailwind CSS integration |
| @pinia/nuxt | Pinia state management |
| @nuxt/image | Image optimization (lazy loading, resize) |
| @nuxt/content | CMS จาก Markdown/YAML files |
| @sidebase/nuxt-auth | Authentication (OAuth, Credentials) |
| @nuxt/i18n | Internationalization |
| @nuxt/ui | Official UI component library |
| nuxt-security | Security headers, rate limiting |
Vue vs React — เปรียบเทียบปี 2026
| หัวข้อ | Vue 3 | React 19 |
|---|---|---|
| Template | HTML template (SFC) | JSX |
| State | ref/reactive (built-in) | useState/useReducer |
| Reactivity | Fine-grained (Proxy) | Re-render whole component |
| Two-way binding | v-model (built-in) | ต้องเขียนเอง |
| Styling | Scoped CSS (built-in) | CSS Modules/Styled Components |
| Learning curve | ง่ายกว่า | ปานกลาง |
| Meta-framework | Nuxt.js | Next.js |
| Bundle size | เล็กกว่า | ใหญ่กว่าเล็กน้อย |
| Job market (global) | น้อยกว่า React | มากที่สุด |
| Job market (เอเชีย) | นิยมมาก | นิยมมาก |
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-lazydirective - 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 ในที่เดียว
