Vue.js

Nuxt 4 SSR 与混合渲染实战:数据获取、缓存、Hydration 与生产部署

从 Nitro API、useFetch、routeRules 到 Hydration、SWR、容器和反向代理,建立可验证、可观测、可回滚的 Nuxt 4 生产方案。

TY
Tycho
技术博主
• 2026-09-28 • 42 分钟阅读 • 3 次浏览
Nuxt 4 SSR 与混合渲染实战:数据获取、缓存、Hydration 与生产部署

一、目标架构:按页面选择渲染策略

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 和上游服务。

TY

Tycho

热爱分享技术知识,帮助开发者成长。

评论 (0)

评论功能当前已关闭
暂无评论,快来抢沙发吧!