Nuxt 3: SSR และการสร้างเพจแบบ Static คู่มือฉบับสมบูรณ์

เชี่ยวชาญ SSR และการสร้างเพจแบบ Static ด้วย Nuxt 3 ตั้งแต่ useFetch ไปจนถึง route rules เรียนรู้วิธีเพิ่มประสิทธิภาพแอปพลิเคชัน Vue.js

ภาพประกอบแสดงการเรนเดอร์ฝั่งเซิร์ฟเวอร์และการสร้างเพจแบบ Static ด้วย Nuxt 3 และ Vue.js

Nuxt 3 ปรับเปลี่ยนวิธีการสร้างแอปพลิเคชัน Vue.js โดยเสนอโหมดการเรนเดอร์หลายแบบที่เหมาะสมกับกรณีการใช้งานที่แตกต่างกัน ตั้งแต่ Server-Side Rendering (SSR) ไปจนถึงการสร้างเพจแบบ Static และการเรนเดอร์แบบไฮบริด เฟรมเวิร์กนี้ให้ความยืดหยุ่นที่โดดเด่นในการเพิ่มประสิทธิภาพและ SEO

ข้อกำหนดเบื้องต้น

บทช่วยสอนนี้สมมุติว่ามีความรู้พื้นฐานเกี่ยวกับ Vue 3 และ Composition API ความคุ้นเคยกับแนวคิดการเรนเดอร์ฝั่งเซิร์ฟเวอร์มีประโยชน์แต่ไม่จำเป็น เพราะแนวคิดพื้นฐานจะถูกอธิบายตลอดทั้งคู่มือ

ทำความเข้าใจโหมดการเรนเดอร์ของ Nuxt 3

ก่อนที่จะลงลึกในโค้ด สิ่งสำคัญคือต้องเข้าใจความแตกต่างระหว่างโหมดการเรนเดอร์ที่มี โหมดแต่ละแบบตอบสนองความต้องการเฉพาะในด้านประสิทธิภาพ SEO และประสบการณ์ผู้ใช้

SSR (Server-Side Rendering) สร้าง HTML บนเซิร์ฟเวอร์ในทุกคำขอ การสร้างเพจแบบ Static (SSG) สร้างทุกหน้าไว้ล่วงหน้าตอน build โหมดไฮบริดอนุญาตให้รวมแนวทางเหล่านี้เข้าด้วยกันแบบหน้าต่อหน้า

nuxt.config.tstypescript
// การกำหนดค่าโหมดการเรนเดอร์ต่าง ๆ
export default defineNuxtConfig({
  // SSR เปิดใช้งานโดยค่าเริ่มต้น (แนะนำสำหรับ SEO)
  ssr: true,

  // การสร้างเพจแบบ Static: เรนเดอร์ทุกหน้าไว้ล่วงหน้า
  // ใช้ 'npm run generate' เพื่อ build
  // target: 'static', // ไวยากรณ์ Nuxt 2

  // โหมดไฮบริด: ปรับแต่งได้ต่อ route
  routeRules: {
    // หน้าแรก: เรนเดอร์ล่วงหน้าและแคชไว้
    '/': { prerender: true },
    // บล็อก: การสร้างเพจแบบ Static
    '/blog/**': { prerender: true },
    // Dashboard: เรนเดอร์เฉพาะฝั่ง client
    '/dashboard/**': { ssr: false },
    // API: ไม่มี pre-rendering
    '/api/**': { prerender: false }
  }
})

การกำหนดค่านี้แสดงให้เห็นพลังของโหมดไฮบริด: แต่ละส่วนของแอปพลิเคชันใช้โหมดการเรนเดอร์ที่เหมาะสมกับความต้องการของตนมากที่สุด

การดึงข้อมูลด้วย useFetch และ useAsyncData

Nuxt 3 มี composable หลักสองตัวสำหรับการดึงข้อมูลแบบ isomorphic composable เหล่านี้ทำงานได้ทั้งฝั่งเซิร์ฟเวอร์และฝั่ง client พร้อมจัดการ hydration อัตโนมัติ

useFetch คือ wrapper รอบ useAsyncData ที่ทำให้การเรียก HTTP ง่ายขึ้น useAsyncData ให้การควบคุมที่มากขึ้นสำหรับกรณีการใช้งานขั้นสูง

vue
<script setup lang="ts">
// pages/blog/[slug].vue
// หน้ารายละเอียดบทความด้วย useFetch

// รับพารามิเตอร์ของ route
const route = useRoute()

// useFetch: ดึงข้อมูลอัตโนมัติ
// ข้อมูลจะถูกดึงที่ฝั่งเซิร์ฟเวอร์แล้ว hydrate ที่ client
const { data: article, pending, error } = await useFetch(
  `/api/articles/${route.params.slug}`,
  {
    // คีย์เฉพาะสำหรับแคชและการลดความซ้ำซ้อน
    key: `article-${route.params.slug}`,
    // แปลงข้อมูลเมื่อจำเป็น
    transform: (response) => response.data,
    // ตัวเลือกแคช
    getCachedData: (key) => {
      // ตรวจสอบว่าข้อมูลอยู่ในแคชหรือไม่
      const nuxtApp = useNuxtApp()
      return nuxtApp.payload.data[key]
    }
  }
)

// การจัดการข้อผิดพลาดด้วยการนำทาง
if (error.value) {
  throw createError({
    statusCode: 404,
    message: 'ไม่พบบทความ'
  })
}
</script>

<template>
  <div>
    <div v-if="pending" class="loading">
      กำลังโหลดบทความ...
    </div>
    <article v-else-if="article">
      <h1>{{ article.title }}</h1>
      <div v-html="article.content" />
    </article>
  </div>
</template>

สำหรับกรณีที่ต้องการการควบคุมมากขึ้น useAsyncData ช่วยให้สามารถเรียกใช้ฟังก์ชัน asynchronous ใด ๆ ได้

vue
<script setup lang="ts">
// pages/products/index.vue
// รายการสินค้าด้วย useAsyncData และตัวกรอง

const route = useRoute()

// useAsyncData: ควบคุมตรรกะการ fetching ได้อย่างเต็มที่
const { data: products, refresh } = await useAsyncData(
  'products-list',
  async () => {
    // ดึงข้อมูลจากหลายแหล่งหากจำเป็น
    const [productsResponse, categoriesResponse] = await Promise.all([
      $fetch('/api/products', {
        query: {
          category: route.query.category,
          sort: route.query.sort || 'date'
        }
      }),
      $fetch('/api/categories')
    ])

    // รวมและแปลงข้อมูล
    return {
      products: productsResponse.data,
      categories: categoriesResponse.data,
      total: productsResponse.meta.total
    }
  },
  {
    // รีเฟรชเมื่อ query params เปลี่ยน
    watch: [() => route.query]
  }
)

// ฟังก์ชันรีเฟรชด้วยตนเอง
const updateFilters = async (newCategory: string) => {
  await navigateTo({
    query: { ...route.query, category: newCategory }
  })
}
</script>

composable เหล่านี้ป้องกันการ fetch ซ้ำสองครั้ง: ข้อมูลที่ได้จากเซิร์ฟเวอร์จะถูก serialize ลงใน HTML payload และนำกลับมาใช้ระหว่างการ hydrate ที่ฝั่ง client

การปรับแต่ง SSR ด้วย server hooks

SSR ของ Nuxt 3 สามารถปรับแต่งได้ผ่าน server hooks hook เหล่านี้ช่วยให้สามารถเข้าแทรกแซงในขั้นตอนต่าง ๆ ของวงจรการเรนเดอร์เพื่อปรับเปลี่ยนพฤติกรรมเริ่มต้น

server/plugins/render-hooks.tstypescript
// ปลั๊กอินเซิร์ฟเวอร์เพื่อปรับแต่งการเรนเดอร์ SSR

export default defineNitroPlugin((nitroApp) => {
  // hook ที่ทำงานก่อนการเรนเดอร์ของแต่ละหน้า
  nitroApp.hooks.hook('render:html', (html, { event }) => {
    // ฉีดสคริปต์หรือเมตาดาต้า
    html.head.push(`
      <script>
        // การวิเคราะห์หรือการตั้งค่าระดับโลก
        window.__APP_CONFIG__ = {
          environment: '${process.env.NODE_ENV}',
          apiUrl: '${process.env.API_URL}'
        }
      </script>
    `)
  })

  // hook สำหรับการจัดการแคชการเรนเดอร์
  nitroApp.hooks.hook('render:response', (response, { event }) => {
    // เพิ่ม header แคชแบบกำหนดเอง
    const path = event.path

    if (path.startsWith('/blog/')) {
      // แคชระยะยาวสำหรับบทความบล็อก
      response.headers['Cache-Control'] = 'public, max-age=3600, s-maxage=86400'
    } else if (path.startsWith('/api/')) {
      // ไม่มีแคชสำหรับ API
      response.headers['Cache-Control'] = 'no-store'
    }
  })
})
ประสิทธิภาพ SSR

hook render:response เหมาะอย่างยิ่งสำหรับการนำกลยุทธ์แคช HTTP มาใช้ การรวม SSR กับ CDN ที่เคารพ header Cache-Control ช่วยให้สามารถให้บริการหน้าที่เรนเดอร์ล่วงหน้าโดยยังคงรักษาความสามารถในการยกเลิกแคชได้

การสร้างเพจแบบ Static ด้วย nuxt generate

การสร้างเพจแบบ Static สร้างทุกหน้าไว้ล่วงหน้าตอน build วิธีนี้เหมาะสำหรับเว็บไซต์ที่มีเนื้อหาคงที่ เช่น บล็อก เอกสาร หรือเว็บไซต์การตลาด

สำหรับ route แบบไดนามิก Nuxt ต้องรู้ทุก URL ที่จะสร้าง hook prerender:routes ช่วยให้กำหนด route เหล่านี้ในเชิงโปรแกรมได้

nuxt.config.tstypescript
// การกำหนดค่าครบถ้วนสำหรับการสร้างเพจแบบ Static

export default defineNuxtConfig({
  // เปิดใช้การสร้างเพจแบบ Static
  nitro: {
    prerender: {
      // เปิดการ crawl ลิงก์อัตโนมัติ
      crawlLinks: true,
      // route ที่ต้องรวมไว้เสมอ
      routes: ['/', '/about', '/contact'],
      // เพิกเฉย route บางอย่าง
      ignore: ['/admin', '/api']
    }
  },

  hooks: {
    // hook สำหรับสร้าง route แบบไดนามิก
    async 'prerender:routes'(ctx) {
      // ดึงบทความจาก API หรือ DB
      const articles = await fetch('https://api.example.com/articles')
        .then(res => res.json())

      // เพิ่ม route บทความ
      for (const article of articles) {
        ctx.routes.add(`/blog/${article.slug}`)
      }

      // ดึงหมวดหมู่
      const categories = await fetch('https://api.example.com/categories')
        .then(res => res.json())

      for (const category of categories) {
        ctx.routes.add(`/category/${category.slug}`)
      }
    }
  }
})

สำหรับโครงการที่มีหน้าจำนวนมาก crawler อัตโนมัติอาจไม่เพียงพอ นี่คือแนวทางที่ทนทานกว่าด้วยไฟล์การกำหนดค่าแยก

server/utils/generate-routes.tstypescript
// ยูทิลิตี้สำหรับสร้างรายการ route แบบไดนามิก

import { prisma } from './prisma'

export async function getAllStaticRoutes(): Promise<string[]> {
  const routes: string[] = []

  // บทความบล็อก
  const articles = await prisma.article.findMany({
    where: { published: true },
    select: { slug: true, category: { select: { slug: true } } }
  })

  for (const article of articles) {
    routes.push(`/blog/${article.category.slug}/${article.slug}`)
  }

  // หน้าสินค้า
  const products = await prisma.product.findMany({
    where: { active: true },
    select: { slug: true }
  })

  for (const product of products) {
    routes.push(`/products/${product.slug}`)
  }

  // หน้าแท็ก
  const tags = await prisma.tag.findMany({
    select: { slug: true }
  })

  for (const tag of tags) {
    routes.push(`/tags/${tag.slug}`)
  }

  return routes
}

พร้อมที่จะพิชิตการสัมภาษณ์ Vue.js / Nuxt.js แล้วหรือยังครับ?

ฝึกฝนด้วยตัวจำลองแบบโต้ตอบ, flashcards และแบบทดสอบเทคนิคครับ

การเรนเดอร์แบบไฮบริดด้วย routeRules

การเรนเดอร์แบบไฮบริดเป็นคุณสมบัติเด่นของ Nuxt 3 ช่วยให้กำหนดกฎการเรนเดอร์ที่แตกต่างกันต่อ route ผสมจุดเด่นของ SSR และ SSG เข้าด้วยกัน

nuxt.config.tstypescript
// การกำหนดค่าขั้นสูงสำหรับการเรนเดอร์แบบไฮบริด

export default defineNuxtConfig({
  routeRules: {
    // หน้าการตลาด: เรนเดอร์ล่วงหน้าและแคชระยะยาว
    '/': { prerender: true },
    '/pricing': { prerender: true },
    '/features/**': { prerender: true },

    // บล็อก: ISR (Incremental Static Regeneration)
    // ตรวจสอบใหม่ทุกชั่วโมง
    '/blog/**': {
      isr: 3600,
      prerender: true
    },

    // เอกสาร: แคช CDN พร้อมการตรวจสอบใหม่
    '/docs/**': {
      swr: 86400, // Stale-while-revalidate
      prerender: true
    },

    // E-commerce: SSR ที่มีแคชระยะสั้น
    '/products/**': {
      ssr: true,
      cache: {
        maxAge: 60,
        staleMaxAge: 300
      }
    },

    // ตะกร้าและการชำระเงิน: เฉพาะฝั่ง client
    '/cart': { ssr: false },
    '/checkout/**': { ssr: false },

    // Dashboard: โหมด SPA
    '/dashboard/**': {
      ssr: false,
      // ปิดการ pre-rendering
      prerender: false
    },

    // route API: ไม่มีแคชโดยค่าเริ่มต้น
    '/api/**': {
      cors: true,
      headers: {
        'Access-Control-Allow-Methods': 'GET,POST,PUT,DELETE'
      }
    }
  }
})

การกำหนดค่านี้สะท้อนสถาปัตยกรรมทั่วไปของแอปพลิเคชันสมัยใหม่: หน้าสาธารณะถูกเพิ่มประสิทธิภาพด้วย SSG เพื่อ SEO ในขณะที่ส่วนที่มีปฏิสัมพันธ์ใช้การเรนเดอร์ฝั่ง client

การเพิ่มประสิทธิภาพด้วยการแคชข้อมูล

นอกเหนือจากการแคชหน้า Nuxt 3 ยังอนุญาตให้แคชข้อมูลที่ดึงมา กลยุทธ์นี้ช่วยลดภาระบน API และปรับปรุงเวลาตอบสนอง

server/api/articles/[slug].get.tstypescript
// API endpoint ที่มีการแคชข้อมูล

import { getArticleBySlug } from '~/server/utils/articles'

export default defineCachedEventHandler(
  async (event) => {
    const slug = getRouterParam(event, 'slug')

    if (!slug) {
      throw createError({
        statusCode: 400,
        message: 'ไม่มี slug'
      })
    }

    const article = await getArticleBySlug(slug)

    if (!article) {
      throw createError({
        statusCode: 404,
        message: 'ไม่พบบทความ'
      })
    }

    return article
  },
  {
    // คีย์แคชอ้างอิงจาก slug
    getKey: (event) => `article-${getRouterParam(event, 'slug')}`,
    // ระยะเวลาแคช: 1 ชั่วโมง
    maxAge: 3600,
    // Stale-while-revalidate: ให้บริการแคชเก่าระหว่างการอัปเดต
    staleMaxAge: 7200,
    // การยกเลิกแคชด้วย tag
    tags: ['articles']
  }
)

เพื่อยกเลิกแคชเมื่อเนื้อหาเปลี่ยนแปลง Nuxt มีระบบ tag

server/api/articles/[slug].put.tstypescript
// การอัปเดตบทความพร้อมยกเลิกแคช

import { updateArticle } from '~/server/utils/articles'

export default defineEventHandler(async (event) => {
  const slug = getRouterParam(event, 'slug')
  const body = await readBody(event)

  // อัปเดตบทความ
  const article = await updateArticle(slug, body)

  // ยกเลิกแคชสำหรับบทความนี้
  await useStorage('cache').removeItem(`nitro:handlers:article-${slug}`)

  // หรือยกเลิกแคชด้วย tag (บทความทั้งหมด)
  // await useStorage('cache').clear('articles')

  return article
})
แคชแบบกระจาย

ในระบบ production ที่มีหลาย instance แคชในหน่วยความจำไม่เพียงพอ แนะนำให้ตั้งค่า Redis หรือระบบกระจายอื่น ๆ ผ่านการตั้งค่า Nitro เพื่อรับประกันความสอดคล้องระหว่าง instance

การจัดการ SEO และเมตาดาต้า

SSR ช่วยให้ทำการเพิ่มประสิทธิภาพ SEO ได้โดยสร้างเมตาดาต้าฝั่งเซิร์ฟเวอร์ Nuxt 3 มีหลายวิธีในการจัดการ meta tag แบบไดนามิก

vue
<script setup lang="ts">
// pages/blog/[slug].vue
// หน้าบล็อกที่เพิ่มประสิทธิภาพ SEO

const route = useRoute()

const { data: article } = await useFetch(`/api/articles/${route.params.slug}`)

// การกำหนดค่า SEO แบบไดนามิกตามบทความ
useSeoMeta({
  title: article.value?.title,
  description: article.value?.excerpt,
  ogTitle: article.value?.title,
  ogDescription: article.value?.excerpt,
  ogImage: article.value?.coverImage,
  ogType: 'article',
  twitterCard: 'summary_large_image',
  twitterTitle: article.value?.title,
  twitterDescription: article.value?.excerpt,
  twitterImage: article.value?.coverImage
})

// ข้อมูลโครงสร้างสำหรับ Google
useHead({
  script: [
    {
      type: 'application/ld+json',
      innerHTML: JSON.stringify({
        '@context': 'https://schema.org',
        '@type': 'Article',
        headline: article.value?.title,
        description: article.value?.excerpt,
        image: article.value?.coverImage,
        datePublished: article.value?.publishedAt,
        dateModified: article.value?.updatedAt,
        author: {
          '@type': 'Organization',
          name: 'SharpSkill'
        }
      })
    }
  ]
})
</script>

สำหรับหน้าแบบ Static เมตาดาต้าสามารถกำหนดได้โดยตรงในคอมโพเนนต์

vue
<script setup lang="ts">
// pages/about.vue
// หน้าแบบ Static พร้อม SEO

definePageMeta({
  title: 'เกี่ยวกับ'
})

useSeoMeta({
  title: 'เกี่ยวกับ SharpSkill | การเตรียมตัวสัมภาษณ์ทางเทคนิค',
  description: 'ค้นพบ SharpSkill แพลตฟอร์มเตรียมตัวสัมภาษณ์ทางเทคนิค ภารกิจคือช่วยนักพัฒนาให้ประสบความสำเร็จในการสัมภาษณ์ทางเทคนิค',
  ogTitle: 'เกี่ยวกับ SharpSkill',
  ogDescription: 'แพลตฟอร์มเตรียมตัวสัมภาษณ์ทางเทคนิค',
  ogImage: '/images/og-about.webp'
})
</script>

การ Deploy และข้อพิจารณาในระบบ Production

การเลือก deploy ขึ้นอยู่กับโหมดการเรนเดอร์ที่ใช้ ต่อไปนี้คือตัวเลือกหลักและการกำหนดค่า

nuxt.config.tstypescript
// การกำหนดค่าสำหรับสภาพแวดล้อมการ deploy ต่าง ๆ

export default defineNuxtConfig({
  nitro: {
    // Preset ตามแพลตฟอร์มเป้าหมาย
    // preset: 'vercel', // Vercel
    // preset: 'netlify', // Netlify
    // preset: 'cloudflare-pages', // Cloudflare
    // preset: 'node-server', // Node.js แบบดั้งเดิม

    // การกำหนดค่าสำหรับ Node.js ใน production
    preset: 'node-server',

    // การบีบอัด response
    compressPublicAssets: true,

    // การกำหนดค่าที่เก็บแคช
    storage: {
      cache: {
        driver: 'redis',
        url: process.env.REDIS_URL
      }
    }
  },

  // ตัวแปรสภาพแวดล้อมขณะรันไทม์
  runtimeConfig: {
    // ความลับ (ไม่เปิดเผยต่อ client)
    apiSecret: process.env.API_SECRET,
    // การกำหนดค่าสาธารณะ
    public: {
      apiBase: process.env.NUXT_PUBLIC_API_BASE || '/api'
    }
  }
})

สำหรับการ deploy แบบ Static คำสั่ง npm run generate จะสร้างโฟลเดอร์ .output/public ที่พร้อมจะ deploy ไปยังโฮสต์ไฟล์ Static ใด ๆ

bash
# การสร้างเพจแบบ Static
npm run generate

# เนื้อหาของ .output/public สามารถ deploy ได้ที่:
# - Vercel (ตรวจจับอัตโนมัติ)
# - Netlify (กำหนดค่าอัตโนมัติ)
# - GitHub Pages
# - S3 + CloudFront
# - CDN หรือเซิร์ฟเวอร์ไฟล์ Static ใด ๆ

เริ่มฝึกซ้อมเลย!

ทดสอบความรู้ของคุณด้วยตัวจำลองสัมภาษณ์และแบบทดสอบเทคนิคครับ

สรุป

Nuxt 3 มอบความยืดหยุ่นที่ยอดเยี่ยมในการเรนเดอร์แอปพลิเคชัน Vue.js การเลือกระหว่าง SSR, SSG และการเรนเดอร์แบบไฮบริดขึ้นอยู่กับความต้องการเฉพาะของแต่ละโครงการ

ประเด็นสำคัญ:

SSR: เหมาะสำหรับเนื้อหาแบบไดนามิกที่ต้องการ SEO ที่ดี (e-commerce เว็บไซต์ข่าว)

SSG: เหมาะสำหรับเนื้อหาที่คงที่ (บล็อก เอกสาร เว็บไซต์การตลาด)

ไฮบริด: แนวทางที่ดีที่สุดสำหรับแอปพลิเคชันที่ซับซ้อนและมีความต้องการหลากหลาย

useFetch/useAsyncData: hydration อัตโนมัติและการจัดการแคช

routeRules: การกำหนดค่าอย่างละเอียดสำหรับพฤติกรรมของแต่ละ route

การแคช: หลายกลยุทธ์เพื่อเพิ่มประสิทธิภาพในระบบ production

การรวมการเรนเดอร์แบบไฮบริดกับกลยุทธ์การแคชที่ออกแบบมาอย่างดีช่วยให้สามารถสร้างแอปพลิเคชันที่มีประสิทธิภาพสูงและเหมาะสมสำหรับ SEO โดยยังคงรักษาการตอบสนองของ Single Page Applications ไว้

แท็ก

#nuxt 3
#vue.js
#ssr
#การสร้างเพจแบบ static
#ประสิทธิภาพเว็บ

แชร์

บทความที่เกี่ยวข้อง