Nuxt 4伺服器端路由實戰:SSR優化與Nitro引擎的5個核心模式
Nuxt 4伺服器端路由實戰:SSR優化與Nitro引擎的5個核心模式
你的Nuxt應用首屏渲染超過3秒?Hydration Mismatch報錯滿天飛?API路由和頁面路由耦合得像義大利麵?2026年,Nuxt4 + Nitro引擎的伺服器端路由體系已經完全成熟——從檔案路由到混合渲染,從中介軟體認證到邊緣部署,5個核心模式讓你的SSR應用快如閃電。
背景知識
Nuxt 4伺服器端路由核心概念
| 概念 | 說明 | 關鍵API |
|---|---|---|
| Nuxt 4 | Vue3全端框架,預設SSR | defineNuxtConfig |
| Nitro引擎 | 伺服器端執行時,支援多平台部署 | defineEventHandler |
| SSR | 伺服器端渲染,首屏直出HTML | useFetch / useAsyncData |
| Hybrid Rendering | 混合渲染,按路由配置渲染模式 | routeRules |
| Server Routes | 檔案式API路由 | server/api/ / server/routes/ |
| Server Middleware | 伺服器端中介軟體,攔截請求 | server/middleware/ |
| Route Rules | 路由規則,控制快取與渲染 | nitro.routeRules |
| useFetch | 帶SSR支援的資料獲取 | useFetch / $fetch |
| defineEventHandler | Nitro事件處理器 | defineEventHandler |
問題分析
Nuxt SSR的5大核心挑戰
- SSR效能瓶頸:伺服器端渲染阻塞、資料串行獲取、大頁面TTFB過高
- Hydration錯誤:伺服器端與客戶端渲染不一致、第三方指令碼干擾、時間戳/隨機數導致mismatch
- 路由守衛實作:認證中介軟體順序、SSR模式下的重定向、權限校驗時機
- API路由設計:檔案路由命名規範、動態參數處理、跨域與錯誤處理
- 多環境部署:Node.js/Edge/Serverless差異、環境變數管理、冷啟動最佳化
分步實操
模式1:Nitro伺服器端路由與API設計
// server/api/articles/index.get.ts
export default defineEventHandler(async (event) => {
const query = getQuery(event)
const page = Number(query.page) || 1
const limit = Number(query.limit) || 20
const [articles, total] = await Promise.all([
db.article.findMany({
skip: (page - 1) * limit,
take: limit,
orderBy: { createdAt: 'desc' },
include: { author: { select: { id: true, name: true, avatar: true } } },
}),
db.article.count(),
])
return { articles, pagination: { page, limit, total } }
})
// server/api/articles/[id].get.ts
export default defineEventHandler(async (event) => {
const id = getRouterParam(event, 'id')!
const article = await db.article.findUnique({
where: { id },
include: { author: true, tags: true },
})
if (!article) {
throw createError({ statusCode: 404, message: 'Article not found' })
}
return article
})
// server/api/articles/index.post.ts
export default defineEventHandler(async (event) => {
const session = await getUserSession(event)
if (!session?.user?.id) {
throw createError({ statusCode: 401, message: 'Unauthorized' })
}
const body = await readBody(event)
const validated = articleSchema.parse(body)
const article = await db.article.create({
data: {
title: validated.title,
content: validated.content,
authorId: session.user.id,
tags: { connect: validated.tagIds.map((id: string) => ({ id })) },
},
})
setResponseStatus(event, 201)
return article
})
// server/routes/sitemap.xml.ts
export default defineEventHandler(async (event) => {
const articles = await db.article.findMany({
select: { slug: true, updatedAt: true },
orderBy: { updatedAt: 'desc' },
})
const sitemap = `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url><loc>https://toolsku.com</loc><changefreq>daily</changefreq></url>
${articles.map(a => `<url><loc>https://toolsku.com/article/${a.slug}</loc><lastmod>${a.updatedAt.toISOString()}</lastmod></url>`).join('\n ')}
</urlset>`
setResponseHeader(event, 'Content-Type', 'application/xml')
return sitemap
})
模式2:混合渲染策略(SSR/SSG/SPA/ISR)
// nuxt.config.ts
export default defineNuxtConfig({
routeRules: {
'/': { isr: 3600, headers: { 'Cache-Control': 's-maxage=3600' } },
'/article/**': { swr: 600 },
'/blog/**': { isr: 1800 },
'/tools/**': { prerender: true },
'/dashboard/**': { ssr: false },
'/admin/**': { appMiddleware: ['auth'] },
'/api/**': { cors: true, headers: { 'Cache-Control': 'no-store' } },
},
nitro: {
preset: 'cloudflare-pages',
compressPublicAssets: true,
cache: {
pages: ['/article/**'],
data: ['/api/articles/**'],
},
},
})
<!-- pages/article/[slug].vue -->
<script lang="ts" setup>
const route = useRoute()
const slug = route.params.slug as string
const { data: article, error } = await useFetch(`/api/articles/${slug}`, {
key: `article-${slug}`,
transform: (data) => ({
...data,
publishedAt: new Date(data.publishedAt).toLocaleDateString('zh-TW'),
}),
})
if (error.value) {
throw createError({ statusCode: 404, message: 'Article not found' })
}
useHead({
title: () => `${article.value?.title} - 工具庫`,
meta: [
{ name: 'description', content: () => article.value?.summary || '' },
{ property: 'og:title', content: () => article.value?.title || '' },
],
})
</script>
<template>
<article class="max-w-3xl mx-auto px-4 py-8">
<h1 class="text-3xl font-bold mb-4">{{ article?.title }}</h1>
<div class="flex items-center gap-3 text-gray-500 mb-8">
<img :src="article?.author?.avatar" class="w-8 h-8 rounded-full" alt="" />
<span>{{ article?.author?.name }}</span>
<span>{{ article?.publishedAt }}</span>
</div>
<div class="prose prose-lg" v-html="article?.content" />
</article>
</template>
模式3:伺服器端中介軟體與認證
// server/middleware/1.auth.ts
export default defineEventHandler(async (event) => {
if (!event.path.startsWith('/api/')) return
if (event.path.startsWith('/api/auth/')) return
if (event.path.startsWith('/api/public/')) return
const accessToken = getCookie(event, 'access_token')
if (!accessToken) {
throw createError({ statusCode: 401, message: 'Unauthorized' })
}
try {
const payload = await verifyAccessToken(accessToken)
event.context.auth = payload
} catch {
throw createError({ statusCode: 401, message: 'Invalid token' })
}
})
// server/middleware/2.rbac.ts
export default defineEventHandler(async (event) => {
const auth = event.context.auth
if (!auth) return
const role = auth.role as string
const path = event.path
const method = event.method
const permissionMap: Record<string, Record<string, string[]>> = {
'/api/users': { GET: ['users:read'], POST: ['users:write'], DELETE: ['users:delete'] },
'/api/articles': { GET: ['content:read'], POST: ['content:write'], DELETE: ['content:delete'] },
'/api/settings': { GET: ['settings:read'], POST: ['settings:write'] },
}
for (const [routePath, methods] of Object.entries(permissionMap)) {
if (!path.startsWith(routePath)) continue
const requiredPerms = methods[method]
if (requiredPerms && !hasAnyPermission(role, requiredPerms)) {
throw createError({ statusCode: 403, message: 'Forbidden' })
}
}
})
// server/middleware/3.rate-limit.ts
const requestCounts = new Map<string, { count: number; resetAt: number }>()
export default defineEventHandler((event) => {
if (!event.path.startsWith('/api/')) return
const clientIp = getRequestIP(event, { xForwardedFor: true }) || 'unknown'
const key = `${clientIp}:${event.path}`
const now = Date.now()
const record = requestCounts.get(key)
if (!record || now > record.resetAt) {
requestCounts.set(key, { count: 1, resetAt: now + 60_000 })
return
}
record.count++
if (record.count > 60) {
setResponseHeader(event, 'Retry-After', String(Math.ceil((record.resetAt - now) / 1000)))
throw createError({ statusCode: 429, message: 'Too many requests' })
}
})
模式4:資料獲取與快取策略
// server/api/articles/[slug].get.ts - 伺服器端快取
export default defineEventHandler(defineCachedEventHandler(async (event) => {
const slug = getRouterParam(event, 'slug')!
const article = await db.article.findUnique({
where: { slug },
include: { author: { select: { name: true, avatar: true } } },
})
if (!article) {
throw createError({ statusCode: 404, message: 'Not found' })
}
return article
}, {
maxAge: 60 * 10,
staleMaxAge: 60 * 60,
swr: true,
getKey: (event) => `article:${getRouterParam(event, 'slug')}`,
}))
<!-- composables/useArticleList.ts -->
<script lang="ts">
export function useArticleList(page: Ref<number>) {
const { data, pending, error, refresh } = await useFetch('/api/articles', {
key: `articles-page-${page.value}`,
query: { page, limit: 20 },
lazy: true,
server: false,
transform: (data: any) => data.articles,
default: () => [],
watch: [page],
})
return { data, pending, error, refresh }
}
</script>
// server/api/search.get.ts - 串流回應
export default defineEventHandler(async (event) => {
const query = getQuery(event)
const keyword = query.q as string
if (!keyword) {
throw createError({ statusCode: 400, message: 'Keyword required' })
}
const stream = new ReadableStream({
async start(controller) {
const results = await searchArticles(keyword)
for (const result of results) {
controller.enqueue(new TextEncoder().encode(JSON.stringify(result) + '\n'))
}
controller.close()
},
})
setResponseHeader(event, 'Content-Type', 'text/event-stream')
return sendStream(event, stream)
})
模式5:生產部署與邊緣渲染
// nuxt.config.ts - 生產配置
export default defineNuxtConfig({
nitro: {
preset: process.env.NITRO_PRESET || 'node-cluster',
compressPublicAssets: { brotli: true },
minify: true,
experimental: {
wasm: true,
nodeCompat: true,
},
rollupConfig: {
external: ['sharp'],
},
},
runtimeConfig: {
dbUrl: '',
redisUrl: '',
jwtSecret: '',
public: {
siteUrl: process.env.SITE_URL || 'http://localhost:3000',
},
},
})
// server/plugins/db.ts
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient({
log: process.env.NODE_ENV === 'development' ? ['query'] : ['error'],
})
export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook('close', async () => {
await prisma.$disconnect()
})
})
export { prisma }
// server/plugins/redis.ts
import { createClient } from 'redis'
export default defineNitroPlugin(async (nitroApp) => {
const redis = createClient({ url: process.env.REDIS_URL })
await redis.connect()
nitroApp.hooks.hook('close', async () => {
await redis.quit()
})
})
避坑指南
坑1:useFetch在SSR中重複請求
❌ 同一個API在伺服器端和客戶端各請求一次
✅ useFetch自動去重,確保key參數唯一;使用useAsyncData的dedupe選項:dedupe: 'defer'
坑2:Hydration Mismatch
❌ 模板中使用Date.now()或Math.random(),伺服器端與客戶端渲染結果不一致
✅ 將動態值放入onMounted中賦值,或使用<ClientOnly>包裹;時間戳統一使用ISO格式
坑3:中介軟體執行順序混亂
❌ RBAC中介軟體在Auth中介軟體之前執行,event.context.auth為undefined
✅ 中介軟體檔名加數字前綴:1.auth.ts、2.rbac.ts、3.rate-limit.ts,Nitro按字典序執行
坑4:routeRules快取策略不當
❌ 所有頁面統一SSR,動態頁面TTFB過高;API路由缺少no-store
✅ 靜態頁面用prerender或isr,動態頁面用swr,API用no-store,Dashboard用ssr: false
坑5:Edge Runtime相容性
❌ 使用Node.js原生模組(fs、path),部署Cloudflare Workers報錯
✅ 使用Nitro的useStorage()替代fs,用nodeCompat相容層,或選擇node-cluster預設
報錯排查
| 序號 | 報錯資訊 | 原因 | 解決方法 |
|---|---|---|---|
| 1 | Hydration mismatch |
伺服器端/客戶端渲染不一致 | 檢查Date/隨機值,用<ClientOnly>包裹 |
| 2 | 500 API request failed |
SSR期間API不可達 | 檢查runtimeConfig中API位址,確保SSR可存取 |
| 3 | useFetch data is null |
key重複導致快取命中空值 | 確保每個useFetch的key唯一 |
| 4 | 429 Too Many Requests |
SSR渲染觸發API限流 | 伺服器端請求使用內部URL繞過限流 |
| 5 | Cannot read property of undefined |
中介軟體執行順序錯誤 | 用數字前綴控制中介軟體順序 |
| 6 | Module not found: fs |
Edge Runtime不支援Node API | 使用useStorage()或nodeCompat |
| 7 | CORS error |
API跨域配置缺失 | 在routeRules中配置cors: true |
| 8 | Cache key collision |
快取key衝突 | 使用完整路徑+參數生成唯一key |
| 9 | Cold start timeout |
Serverless冷啟動逾時 | 預熱函式、減少依賴、使用ISR快取 |
| 10 | Prisma Client could not be generated |
建置時未生成Prisma Client | 在postinstall中新增prisma generate |
進階最佳化
1. 串流SSR渲染
// server/routes/preview/[id].get.ts
export default defineEventHandler(async (event) => {
const id = getRouterParam(event, 'id')!
const stream = renderToStream(await getPreviewData(id))
return sendStream(event, stream)
})
2. Nitro Storage抽象
// server/utils/storage.ts
export async function getCachedData<T>(key: string, fetcher: () => Promise<T>, ttl = 3600): Promise<T> {
const cached = await useStorage().getItem<T>(`cache:${key}`)
if (cached) return cached
const data = await fetcher()
await useStorage().setItem(`cache:${key}`, data, { ttl })
return data
}
3. 請求去重與批次處理
// composables/useDedupedFetch.ts
const inflightRequests = new Map<string, Promise<any>>()
export function useDedupedFetch<T>(url: string, key: string) {
const existing = inflightRequests.get(key)
if (existing) return existing
const promise = $fetch<T>(url).finally(() => inflightRequests.delete(key))
inflightRequests.set(key, promise)
return promise
}
4. 邊緣快取策略
// server/middleware/edge-cache.ts
export default defineEventHandler((event) => {
if (event.path.startsWith('/api/')) return
setResponseHeader(event, 'CDN-Cache-Control', 'public, s-maxage=3600, stale-while-revalidate=86400')
setResponseHeader(event, 'Vary', 'Accept-Encoding')
})
對比分析
| 維度 | Nuxt 4 | Next.js 15 | SvelteKit | Remix |
|---|---|---|---|---|
| 渲染模式 | SSR/SSG/SPA/ISR混合 | SSR/SSG/ISR | SSR/SSG/SPA | SSR為主 |
| 伺服器端引擎 | Nitro(多平台) | Next Server | Node/Edge | Node/Edge |
| API路由 | 檔案式server/api/ |
App Router API | +server.ts |
Resource路由 |
| 中介軟體 | Nitro Middleware | Next Middleware | Hooks | Loader上下文 |
| 邊緣部署 | 原生支援 | Vercel Edge | 介面卡模式 | 介面卡模式 |
| 快取策略 | routeRules細粒度 | fetch cache | 手動 | Cache-Control |
| 資料獲取 | useFetch自動SSR | Server Actions | load函式 | Loader |
| 學習曲線 | 低 | 中 | 低 | 中 |
| Vue生態 | 原生 | - | - | - |
| 生產就緒 | 2026成熟 | 成熟 | 成熟 | 成熟 |
總結展望
總結:Nuxt4伺服器端路由的5個核心模式——Nitro檔案路由實現清晰的API設計、混合渲染策略按場景選擇SSR/SSG/SPA/ISR、中介軟體鏈實現認證與權限控制、資料獲取與快取策略提升效能、生產部署與邊緣渲染降低延遲。核心原則:routeRules精細控制渲染模式、中介軟體數字前綴保證執行順序、useFetch的key確保快取唯一、Edge部署避免Node原生模組、ISR+SWR兼顧效能與即時性。建議新專案直接採用Nitro + Cloudflare Pages部署,存量專案逐步遷移混合渲染策略。
線上工具推薦
- JSON格式化(API除錯):/zh-TW/json/format
- Hash計算(簽章驗證):/zh-TW/encode/hash
- cURL轉程式碼(API測試):/zh-TW/dev/curl-to-code
本站提供瀏覽器本地工具,免註冊即可試用 →