Nuxt 生命週期詳解與 SEO 最佳實踐指南
什麼是 Nuxt?(What is Nuxt?)
Nuxt.js 是一個基於 Vue.js 的全端框架,提供了服務端渲染(SSR)、靜態站點生成(SSG)和單頁應用(SPA)等多種渲染模式。它通過預設的架構和豐富的模組生態系統,讓開發者能夠快速構建現代化的 Web 應用。
Nuxt 的核心優勢(Core Advantages)
- 服務端渲染(SSR):提升首屏加載速度和 SEO 表現
- 靜態站點生成(SSG):預渲染頁面,極致的性能表現
- 自動路由:基於文件系統的路由配置
- 模組化架構:豐富的官方和社區模組
- 開發體驗:熱重載、TypeScript 支持等
Nuxt 生命週期詳解(Nuxt Lifecycle Deep Dive)
1. 應用初始化階段(Application Initialization)
nuxt.config.ts
配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| export default defineNuxtConfig({ app: { head: { title: 'My Nuxt App', meta: [ { name: 'description', content: 'My amazing site.' } ] } }, modules: ['@nuxtjs/tailwindcss'], runtimeConfig: { apiSecret: process.env.API_SECRET, public: { apiBase: process.env.API_BASE } } })
|
插件執行順序
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| export default defineNuxtPlugin(() => { console.log('Client-only plugin') })
export default defineNuxtPlugin(() => { console.log('Server-only plugin') })
export default defineNuxtPlugin(() => { console.log('Universal plugin') })
|
2. 服務端渲染階段(Server-Side Rendering Phase)
服務端生命週期順序
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
|
export default defineNuxtRouteMiddleware((to, from) => { console.log('Server middleware') })
export default defineComponent({ async setup() { const { data } = await useFetch('/api/posts') return { posts: data.value } } })
|
服務端數據獲取
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| export default defineComponent({ async setup() { const { data: posts } = await useFetch('/api/posts') const { data: user } = await useAsyncData('user', () => $fetch('/api/user') ) const config = await $fetch('/api/config') return { posts, user, config } } })
|
3. 客戶端水合階段(Client Hydration Phase)
客戶端生命週期
1 2 3 4 5 6 7 8 9 10 11 12
|
export default defineNuxtPlugin(() => { console.log('Client hydration started') const router = useRouter() router.beforeEach((to, from) => { console.log('Route change:', from.path, '->', to.path) }) })
|
客戶端中間件
1 2 3 4 5 6 7 8
| export default defineNuxtRouteMiddleware((to, from) => { const user = useUser() if (!user.value && to.path !== '/login') { return navigateTo('/login') } })
|
4. 頁面組件生命週期(Page Component Lifecycle)
Vue 3 Composition API 生命週期
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| <template> <div> <h1>{{ title }}</h1> <div v-if="posts">{{ posts.length }} posts</div> </div> </template>
<script setup> // 1. setup() - 組件初始化 const title = ref('My Blog')
// 2. onMounted() - 客戶端掛載後 onMounted(() => { console.log('Component mounted on client') })
// 3. onBeforeMount() - 掛載前 onBeforeMount(() => { console.log('Before mount') })
// 4. onBeforeUnmount() - 卸載前 onBeforeUnmount(() => { console.log('Before unmount') })
// 5. onUnmounted() - 卸載後 onUnmounted(() => { console.log('Component unmounted') })
// 6. 響應式數據變化 watch(title, (newTitle, oldTitle) => { console.log('Title changed:', oldTitle, '->', newTitle) }) </script>
|
Nuxt 專用生命週期
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| <script setup> // 1. useHead() - 動態設置 head useHead({ title: 'Dynamic Page Title', meta: [ { name: 'description', content: 'Dynamic description' } ] })
// 2. useSeoMeta() - SEO 優化 useSeoMeta({ title: 'SEO Optimized Title', ogTitle: 'Open Graph Title', description: 'SEO description', ogDescription: 'Open Graph description', ogImage: 'https://example.com/image.jpg', twitterCard: 'summary_large_image', })
// 3. 路由變化監聽 const route = useRoute() watch(() => route.path, (newPath) => { console.log('Route changed to:', newPath) }) </script>
|
SEO 最佳實踐(SEO Best Practices)
全局 SEO 配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| export default defineNuxtConfig({ app: { head: { title: 'My Website', meta: [ { charset: 'utf-8' }, { name: 'viewport', content: 'width=device-width, initial-scale=1' }, { name: 'description', content: 'My amazing website' }, { name: 'keywords', content: 'vue, nuxt, javascript' }, { property: 'og:title', content: 'My Website' }, { property: 'og:description', content: 'My amazing website' }, { property: 'og:type', content: 'website' }, { property: 'og:url', content: 'https://mywebsite.com' }, { property: 'og:image', content: 'https://mywebsite.com/og-image.jpg' }, { name: 'twitter:card', content: 'summary_large_image' }, { name: 'twitter:title', content: 'My Website' }, { name: 'twitter:description', content: 'My amazing website' }, { name: 'twitter:image', content: 'https://mywebsite.com/twitter-image.jpg' } ], link: [ { rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }, { rel: 'canonical', href: 'https://mywebsite.com' } ] } } })
|
2. 動態 SEO 優化(Dynamic SEO Optimization)
頁面級別 SEO
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| <template> <div> <h1>{{ post.title }}</h1> <div v-html="post.content"></div> </div> </template>
<script setup> // 獲取路由參數 const route = useRoute() const { data: post } = await useFetch(`/api/posts/${route.params.id}`)
// 動態設置 SEO 元數據 useSeoMeta({ title: () => post.value?.title || 'Post Not Found', description: () => post.value?.excerpt || 'No description available', ogTitle: () => post.value?.title || 'Post Not Found', ogDescription: () => post.value?.excerpt || 'No description available', ogImage: () => post.value?.featuredImage || '/default-image.jpg', twitterCard: 'summary_large_image', twitterTitle: () => post.value?.title || 'Post Not Found', twitterDescription: () => post.value?.excerpt || 'No description available', twitterImage: () => post.value?.featuredImage || '/default-image.jpg', })
// 結構化數據 useJsonld(() => ({ '@context': 'https://schema.org', '@type': 'BlogPosting', headline: post.value?.title, description: post.value?.excerpt, author: { '@type': 'Person', name: post.value?.author?.name }, datePublished: post.value?.publishedAt, image: post.value?.featuredImage })) </script>
|
3. 確保 SSR 可讀性(Ensuring SSR Readability)
關鍵 SEO 檢查點
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| export default defineComponent({ async setup() { const { data: posts } = await useFetch('/api/posts') return { posts } } })
|
條件渲染最佳實踐
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| <template> <div> <!-- ✅ 正確:使用 v-if 確保 SSR 可讀 --> <div v-if="posts"> <h2>Posts ({{ posts.length }})</h2> <ul> <li v-for="post in posts" :key="post.id"> {{ post.title }} </li> </ul> </div> <!-- ✅ 正確:使用 ClientOnly 包裝客戶端專用組件 --> <ClientOnly> <div>This content is client-only</div> </ClientOnly> </div> </template>
<script setup> // 確保數據在服務端獲取 const { data: posts } = await useFetch('/api/posts') </script>
|
圖片優化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| <template> <div> <!-- ✅ 正確:使用 NuxtImg 自動優化 --> <NuxtImg src="/hero-image.jpg" alt="Hero image" width="1200" height="600" format="webp" loading="lazy" /> <!-- ✅ 正確:使用 NuxtPicture 響應式圖片 --> <NuxtPicture src="/responsive-image.jpg" alt="Responsive image" :sizes="sm:100vw md:50vw lg:400px" /> </div> </template>
|
預加載關鍵資源
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| export default defineNuxtConfig({ app: { head: { link: [ { rel: 'preload', href: '/css/critical.css', as: 'style' }, { rel: 'preload', href: '/fonts/main.woff2', as: 'font', type: 'font/woff2', crossorigin: '' }, { rel: 'dns-prefetch', href: 'https://api.example.com' } ] } } })
|
常見問題與解決方案(Common Issues & Solutions)
1. 水合不匹配(Hydration Mismatch)
問題原因
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| <!-- ❌ 錯誤:服務端和客戶端渲染結果不同 --> <template> <div> <span v-if="isClient">Client Only</span> <span v-else>Server Only</span> </div> </template>
<script setup> const isClient = ref(false) onMounted(() => { isClient.value = true }) </script>
|
解決方案
1 2 3 4 5 6 7 8 9
| <!-- ✅ 正確:使用 ClientOnly 組件 --> <template> <div> <span>Always visible</span> <ClientOnly> <span>Client Only Content</span> </ClientOnly> </div> </template>
|
2. 動態路由 SEO 優化
靜態生成最佳實踐
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| export default defineNuxtConfig({ nitro: { prerender: { routes: ['/sitemap.xml', '/robots.txt'] } } })
export default defineEventHandler(async () => { const posts = await getPosts() return posts })
export default defineComponent({ async setup() { const { data: posts } = await useFetch('/api/posts') if (process.server) { const router = useRouter() for (const post of posts.value || []) { await router.push(`/posts/${post.id}`) } } } })
|
3. 第三方腳本優化
安全的第三方腳本加載
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| <script setup> // 使用 useScript 安全加載第三方腳本 const { $script } = useScript({ src: 'https://example.com/analytics.js', defer: true, strategy: 'lazyOnload' })
// 或者使用 useHead 動態加載 useHead({ script: [ { src: 'https://example.com/analytics.js', defer: true, 'data-cfasync': 'false' } ] }) </script>
|
測試與驗證(Testing & Validation)
1. SEO 測試工具
本地 SEO 檢查
1 2 3 4 5
| npm install -g lighthouse
lighthouse http://localhost:3000 --output html --output-path ./lighthouse-report.html
|
結構化數據測試
1 2 3 4 5 6 7 8 9 10 11 12
| const validateStructuredData = () => { const scripts = document.querySelectorAll('script[type="application/ld+json"]') scripts.forEach(script => { try { JSON.parse(script.textContent || '') console.log('✅ Structured data is valid') } catch (error) { console.error('❌ Invalid structured data:', error) } }) }
|
2. 服務端渲染驗證
檢查 SSR 輸出
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| const validateSSR = () => { const content = document.body.innerHTML const checks = [ { selector: 'h1', message: 'H1 tag found' }, { selector: 'meta[name="description"]', message: 'Meta description found' }, { selector: 'title', message: 'Title tag found' } ] checks.forEach(check => { const element = document.querySelector(check.selector) if (element) { console.log(`✅ ${check.message}`) } else { console.error(`❌ Missing: ${check.message}`) } }) }
|
總結(Summary)
Nuxt.js 提供了強大的 SSR 能力和豐富的生命週期鉤子,讓開發者能夠構建既高性能又 SEO 友好的現代化 Web 應用。關鍵是要理解不同生命週期的執行時機,並確保關鍵內容在服務端渲染階段就已經準備好,這樣搜索引擎就能正確抓取和索引你的網站內容。
關鍵要點(Key Takeaways)
- 服務端優先:確保關鍵數據在服務端獲取
- 動態 SEO:使用
useSeoMeta
和 useHead
動態設置元數據
- 性能優化:使用
NuxtImg
和預加載關鍵資源
- 測試驗證:定期使用 Lighthouse 等工具檢查 SEO 表現
- 結構化數據:添加 JSON-LD 結構化數據提升搜索結果展示
通過遵循這些最佳實踐,你的 Nuxt 應用將能夠在搜索引擎中獲得更好的排名和展示效果。