一、目标架构:按页面选择渲染策略
Nuxt 4 并不要求全站只用一种渲染模式。公开内容页适合 SSR 或预渲染,频繁变化但可短暂陈旧的页面适合 SWR,用户后台可采用客户端渲染。本文构建一个商品站点,覆盖数据获取、route rules、缓存键、Hydration、错误边界、Nitro API、容器部署和验收。
Browser -> CDN / Reverse Proxy -> Nuxt Nitro
| |-- SSR product pages
| |-- SWR category pages
| |-- prerendered marketing pages
`-- static assets `-- server/api backend facade
先冻结版本并记录环境。示例命令按当前 Nuxt 4 文档编写,实际项目必须提交 lockfile,不能依赖浮动最新版。
node --version
corepack enable
pnpm --version
pnpm create nuxt@latest storefront
cd storefront
pnpm install --frozen-lockfile
pnpm nuxt info
二、目录与运行时配置
把页面、服务端 API、共享类型和可组合函数分开。公开配置可进入客户端,密钥只能放在 runtimeConfig 私有部分。
app/pages/
index.vue
products/[slug].vue
account/index.vue
server/api/
products/[slug].get.ts
shared/types/
product.ts
nuxt.config.ts
// nuxt.config.ts
export default defineNuxtConfig({
compatibilityDate: '2026-09-01',
devtools: { enabled: false },
runtimeConfig: {
catalogToken: '',
catalogBaseUrl: 'http://catalog-api:8080',
public: {
siteUrl: 'https://shop.example.com'
}
},
nitro: {
compressPublicAssets: true
}
})
生产通过 NUXT_CATALOG_TOKEN、NUXT_CATALOG_BASE_URL 和 NUXT_PUBLIC_SITE_URL 注入,不要把 .env 烘焙进镜像。
三、用 Nitro API 隔离后端凭据
浏览器不应直接拿到上游服务 Token。由 Nitro 端点调用目录服务,同时规范错误和超时。
// shared/types/product.ts
export interface Product {
id: string
slug: string
name: string
price: number
updatedAt: string
}
// server/api/products/[slug].get.ts
export default defineEventHandler(async (event) => {
const slug = getRouterParam(event, 'slug')
if (!slug || !/^[a-z0-9-]{1,80}$/.test(slug)) {
throw createError({ statusCode: 400, statusMessage: 'Invalid slug' })
}
const config = useRuntimeConfig(event)
try {
return await $fetch(`/v1/products/${slug}`, {
baseURL: config.catalogBaseUrl,
headers: { authorization: `Bearer ${config.catalogToken}` },
timeout: 2500,
retry: 1
})
} catch (error) {
throw createError({
statusCode: 502,
statusMessage: 'Catalog temporarily unavailable',
cause: error
})
}
})
服务端日志记录 request_id、upstream_status 和 duration,禁止记录 authorization 头。
四、正确使用 useFetch 与 useAsyncData
避免服务端与客户端重复请求
Nuxt 的 useFetch 和 useAsyncData 会把服务端获取的数据写入 payload,客户端 Hydration 时复用,避免重复请求。不要在 setup 顶层直接使用裸 $fetch 获取页面数据,否则服务端和客户端都可能执行。
<script setup lang="ts">
import type { Product } from '~/shared/types/product'
const route = useRoute()
const slug = computed(() => String(route.params.slug))
const { data: product, status, error, refresh } = await useFetch<Product>(
() => `/api/products/${slug.value}`,
{
key: () => `product:${slug.value}`,
watch: [slug],
dedupe: 'cancel'
}
)
if (error.value?.statusCode === 404) {
throw createError({ statusCode: 404, statusMessage: 'Product not found' })
}
useSeoMeta({
title: () => product.value ? `${product.value.name} · Store` : 'Product',
description: () => product.value ? `查看 ${product.value.name} 的价格与详情` : ''
})
</script>
缓存 key 必须包含影响结果的租户、语言、分页和过滤条件。把用户身份数据放进共享 key 会造成跨用户污染。
五、设计 routeRules 混合渲染
按新鲜度与个性化选择策略
按业务新鲜度与个性化程度选择策略:营销页构建时预渲染;分类页允许 60 秒 SWR;商品页 SSR;账户页关闭 SSR。
export default defineNuxtConfig({
routeRules: {
'/': { prerender: true },
'/about': { prerender: true },
'/categories/**': { swr: 60 },
'/products/**': { ssr: true },
'/account/**': { ssr: false },
'/api/**': { cors: false }
}
})
SWR 返回缓存内容并在后台刷新,适合可短暂陈旧的公开数据。价格、库存、结算和权限判断不能只依赖页面缓存;最终交易必须由后端再次验证。
pnpm nuxt build
find .output/public -maxdepth 3 -type f | sort | head
NITRO_PRESET=node-server node .output/server/index.mjs
curl -I http://127.0.0.1:3000/categories/hardware
六、避免 Hydration 不一致
Hydration 要求客户端第一次渲染与服务端 HTML 一致。常见错误是模板中直接调用 Date.now()、Math.random()、浏览器专属 API 或时区相关格式化。
<!-- 错误:服务端和客户端结果不同 -->
<p>{{ Math.random() }}</p>
<!-- 正确:由服务端提供稳定值,或仅客户端渲染 -->
<ClientOnly fallback-tag="span" fallback="正在加载本地时间…">
<LocalTime :iso="product.updatedAt" />
</ClientOnly>
稳定随机数可以由服务端生成种子并放入 payload;日期先输出 ISO,再在客户端增强显示。不要用 <ClientOnly> 包住整个页面掩盖问题,它会牺牲 SSR 内容和首屏体验。
pnpm nuxt dev
# 浏览器控制台不得出现 hydration mismatch
curl -sS http://127.0.0.1:3000/products/demo > /tmp/server.html
rg 'Product|__NUXT__' /tmp/server.html
七、错误边界与状态模型
页面要区分 pending、empty、业务 404、上游 502 和用户主动刷新。不要把所有错误显示成“加载失败”。
<template>
<main>
<ProductSkeleton v-if="status === 'pending'" />
<ErrorNotice v-else-if="error" :error="error" @retry="refresh" />
<ProductDetail v-else-if="product" :product="product" />
<EmptyState v-else />
</main>
</template>
全局错误页只处理未捕获错误。可恢复的局部请求应在组件内呈现,并保留重试按钮和 request_id。
<!-- error.vue -->
<script setup lang="ts">
const props = defineProps<{ error: { statusCode?: number; message?: string } }>()
const recover = () => clearError({ redirect: '/' })
</script>
八、缓存失效、键与身份边界
缓存设计要写清三件事:键包含哪些维度、过期多久、什么事件主动失效。公开页面可由 CDN 缓存,带 Cookie 或 Authorization 的响应默认不应共享。
// server/routes/sitemap.xml.ts
export default defineCachedEventHandler(async () => {
const products = await $fetch('/v1/products', { baseURL: 'http://catalog-api:8080' })
return renderSitemap(products)
}, {
maxAge: 3600,
name: 'product-sitemap',
getKey: () => 'v1'
})
反向代理缓存前检查 Cache-Control、Vary 和 Set-Cookie。若内容依赖语言,键必须包含规范化 locale;依赖租户则优先不做公共缓存。
九、容器化 Node Server 部署
Nuxt Node 部署入口是 .output/server/index.mjs,监听地址可用 NITRO_HOST 和 NITRO_PORT 控制。多阶段镜像只复制构建产物。
FROM node:24-bookworm-slim AS build
WORKDIR /app
RUN corepack enable
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
RUN --mount=type=cache,target=/pnpm/store pnpm install --frozen-lockfile
COPY . .
RUN pnpm nuxt build
FROM node:24-bookworm-slim
ENV NODE_ENV=production NITRO_HOST=0.0.0.0 NITRO_PORT=3000
WORKDIR /app
COPY --from=build /app/.output ./.output
USER node
EXPOSE 3000
CMD ["node", ".output/server/index.mjs"]
docker build -t registry.example/storefront:2026.09.28 .
docker run --rm -p 3000:3000 \
-e NUXT_CATALOG_BASE_URL=http://host.docker.internal:8080 \
-e NUXT_CATALOG_TOKEN_FILE=/run/secrets/catalog_token \
registry.example/storefront:2026.09.28
curl --fail http://127.0.0.1:3000/
若应用不支持 *_FILE,在受控入口读取 secret 文件并导出环境变量,禁止把密钥写入 Dockerfile 或镜像层。
十、反向代理、静态资源和安全头
静态构建资源使用长缓存且文件名带 hash;HTML 不应长期 immutable。代理保留真实协议并限制超时。
location /_nuxt/ {
proxy_pass http://nuxt:3000;
expires 1y;
add_header Cache-Control "public, max-age=31536000, immutable";
}
location / {
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_connect_timeout 3s;
proxy_read_timeout 30s;
proxy_pass http://nuxt:3000;
}
在 CDN、代理或 Nitro 的一个层级统一设置 CSP、Referrer-Policy、X-Content-Type-Options 和 frame 限制,避免多层生成互相冲突的头。
十一、性能与可观测性验收
同时观察服务端与浏览器
监控服务端请求率、状态码、Nitro 路由延迟、上游延迟、进程 RSS、事件循环延迟、缓存命中率,以及客户端 LCP、INP、CLS。发布前分别测试冷缓存与热缓存。
curl -sS -o /dev/null -w 'dns=%{time_namelookup} connect=%{time_connect} ttfb=%{time_starttransfer} total=%{time_total}\n' \
https://shop.example.com/products/demo
npx lighthouse https://shop.example.com/products/demo \
--only-categories=performance,accessibility,seo \
--output=json --output-path=./lighthouse.json
浏览器验收同时查看 Network 是否出现重复数据请求,控制台是否有 Hydration 警告,页面源代码是否包含关键 SEO 内容,缓存响应头是否符合 route rule。
十二、测试、发布与回滚
单元测试验证数据转换,组件测试覆盖 loading/error/success,端到端测试验证 SSR HTML、导航和身份边界。
import { describe, expect, it } from 'vitest'
describe('product page', () => {
it('keeps cache keys tenant-safe', () => {
expect(productKey('acme', 'zh-CN', 'demo'))
.toBe('product:acme:zh-CN:demo')
})
})
pnpm lint
pnpm typecheck
pnpm test --run
pnpm nuxt build
node .output/server/index.mjs &
curl --fail http://127.0.0.1:3000/products/demo
滚动发布时等待新实例 ready,再逐步切流;比较新旧版本 5xx、P99、RSS 和缓存命中率。回滚到旧镜像前确认 API 与数据结构仍向后兼容。
十三、官方资料与最终检查
- Nuxt 4 Data Fetching:
useFetch、useAsyncData、payload 与去重。 - Nuxt 4 Rendering:SSR、CSR、Universal Rendering 与 Hybrid Rendering。
- Nuxt 4 Server 与 routeRules:Nitro API、缓存和路由规则。
- Nuxt 4 Deployment:Node Server 入口、环境变量和反向代理。
最终检查:无 Hydration 警告;服务端 HTML 含主要内容;私密配置未进入客户端 payload;缓存键包含全部身份维度;错误可区分、可重试;镜像可重复构建;指标能从浏览器体验定位到 Nitro 和上游服务。