Vercel 部署 Nuxt.js (Vue) 实战:SSR / SSG / Nitro 全栈上云指南

详细讲解如何在 Vercel 上部署 Nuxt.js + Vue 3 项目:SSR 与 SSG 模式选择、Nitro Server 路由配置、环境变量、Vercel Functions 适配、图片与缓存优化。附完整代码和部署验证命令。

Nuxt.js(Vue 生态的全栈框架)在 Vercel 上有着极佳的支持:通过内置的 Nitro Server 引擎,Nuxt 项目可以完美适配 Vercel Functions 的运行时,实现 SSR(服务端渲染)、SSG(静态生成)、API Routes、Server Middleware 等所有能力。本文从初始化项目到生产部署,逐个讲解 Nuxt 在 Vercel 上的最佳实践。


一、项目初始化与模式选择

1.1 创建 Nuxt 项目

# 创建项目
npx nuxi@latest init my-nuxt-app

# 进入目录并安装依赖
cd my-nuxt-app
npm install

# 安装 Vercel 适配器(必须)
npm install -D @nuxtjs/vercel-builder
# 实际 Nuxt Nitro 自带 vercel 预设,通常不需要额外安装

Nuxt 3 的 Nitro 引擎自带多种 preset,其中 vercel 是专门为 Vercel Functions 优化过的。

1.2 SSR vs SSG vs ISR:怎么选?

Nuxt 的渲染模式由 nuxt.config.ts 中的 ssrrouteRules 决定:

模式配置适用场景Vercel 适配
SSRssr: true(默认)需要服务端数据的动态页、登录态页面✅ Nitro → Vercel Serverless Functions
SSGssr: true + routeRules: { '/**': { isr: false, prerender: true } }静态站、博客、文档✅ 静态文件直接上传到 Vercel CDN
ISRrouteRules: { '/**': { isr: 60 } }大部分内容不常变但需及时更新✅ 混合:静态 CDN + Serverless stale-while-revalidate
SPAssr: false纯后台管理、强客户端交互应用✅ 仅 HTML + JS 文件

建议决策

  • 内容站/博客:SSG(最快、最省 Serverless 调用)
  • SaaS 前台:ISR(兼顾速度和数据新鲜度)
  • 后台管理系统:SSR 或 SPA

1.3 nuxt.config.ts 核心配置

// nuxt.config.ts
export default defineNuxtConfig({
  // 默认开启 SSR
  ssr: true,

  // 静态资源/缓存策略
  routeRules: {
    // 首页 SSR(动态内容)
    '/': { isr: 60 },
    // 博客列表页:ISR 5 分钟刷新一次
    '/blog/**': { isr: 300 },
    // 博客详情页:静态生成 + 按需重新验证
    '/blog/**': { prerender: true, isr: 3600 },
    // 关于页:纯静态,永不重新验证
    '/about': { prerender: true },
    // API 路由:不走缓存
    '/api/**': { cors: true, isr: false },
  },

  nitro: {
    // 显式指定 Vercel 预设
    preset: 'vercel',
    // 为 Vercel Functions 设置内存和超时
    vercel: {
      functions: {
        maxDuration: 10, // 函数最大执行时间(秒)
      },
    },
  },

  // 开发服务器代理(本地开发用)
  devtools: { enabled: true },
  compatibilityDate: '2025-01-01',
});

*preset: 'vercel'* 是核心配置,它告诉 Nitro 将服务端代码打包为 Vercel Functions 兼容的格式。

1.4 Vue 3 Composition API 与 Vercel 最佳实践

Nuxt 3 默认使用 Vue 3 的 <script setup> 语法,但在 Vercel SSR 环境下,服务端与客户端的执行边界需要特别留意。

onMounted vs useAsyncData

特性onMounteduseAsyncData
执行时机仅在客户端挂载后服务端 + 客户端均可
SEO 友好否(初始 HTML 无数据)是(SSR 时直接渲染数据)
水源合流不涉及自动水合(Hydration)
Vercel 计费纯客户端,不消耗 Function 时长SSR 时消耗 Function 时长
<!-- 错误示范SEO 不友好首屏空白 -->
<script setup>
const posts = ref([]);
onMounted(async () => {
  posts.value = await $fetch('/api/posts');
});
</script>

<!-- 正确示范SSR 直出数据 -->
<script setup>
const { data: posts } = await useAsyncData('posts', () =>
  $fetch('/api/posts')
);
</script>

在 Vercel 上部署 SSR 项目时,务必使用 useAsyncDatauseFetch 获取首屏数据,否则搜索引擎和社交爬虫无法抓取内容。

useFetchuseAsyncData 的数据源选择

Nuxt 的 useFetch 是对 useAsyncData + $fetch 的封装。在 Vercel SSR 模式下,它有一个关键优化:服务端请求不走 HTTP

// 在页面组件中
const { data } = await useFetch('/api/posts');

执行路径对比:

阶段请求方式说明
SSR(首次访问)Nitro 内部直接调用不经过网络层,零延迟
客户端水合后正常 HTTP 请求浏览器直接访问 /api/posts

如果你在服务端需要调用外部 API(如 CMS、第三方服务),则必须写完整 URL:

const { data } = await useFetch('https://cms.example.com/api/posts');

此时 Vercel Function 会发起真实 HTTP 请求,建议在 nuxt.config.ts 中设置合理的 maxDuration,防止外部服务超时导致 Function 报错。

Vue 3 Suspense 在 SSR 中的使用

Vue 3 原生支持 <Suspense>,Nuxt 3 页面组件默认已在 Suspense 边界内。你可以利用它实现优雅的多数据并行加载:

<template>
  <Suspense>
    <template #default>
      <div>
        <ProfileCard />
        <OrderList />
      </div>
    </template>
    <template #fallback>
      <SkeletonLoader />
    </template>
  </Suspense>
</template>

在 Vercel SSR 场景下,所有 Suspense 内部的异步依赖会并行解析,等全部就绪后一次性返回完整 HTML。这意味着:

  1. TTFB 可能略长(等待最慢的数据源),但 FCP 更完整(无二次水合闪烁)。
  2. 如果某个数据源太慢,建议拆分到客户端用 onMounted + <ClientOnly> 延迟加载,减少 Vercel Function 的冷启动和运行时间。

Composable 在服务端与客户端的执行差异

自定义 Composable 需要兼容两种运行环境:

// composables/useDevice.ts
export function useDevice() {
  // 客户端专有 API
  const isMobile = ref(false);

  if (process.client) {
    isMobile.value = /Android|iPhone/i.test(navigator.userAgent);
  }

  // 服务端:通过请求头推断
  if (process.server) {
    const headers = useRequestHeaders(['user-agent']);
    isMobile.value = /Android|iPhone/i.test(headers['user-agent'] || '');
  }

  return { isMobile };
}

使用 process.client / process.server 做条件分支时,确保两侧的返回值结构一致,否则 Vue 的水合校验会报错。Vercel 的日志中也常见 Text content does not match 类的 Hydration Mismatch,根源往往是 Composable 在服务端和客户端返回了不同的默认值。


二、API Routes 与 Server 引擎

Nuxt 3 的 Server 引擎基于 Nitro,API 路由放在 server/api/server/routes/ 下,Vercel 会自动将其转换为 Serverless Functions。

2.1 基础 API 路由

// server/api/hello.get.ts
export default defineEventHandler((event) => {
  return {
    message: 'Hello from Nuxt on Vercel!',
    timestamp: new Date().toISOString(),
  };
});

文件命名约定:

  • .get.ts → 只响应 GET 请求
  • .post.ts → 只响应 POST 请求
  • .ts → 响应所有 HTTP 方法(需手动判断 event.method

2.2 带参数的 API

// server/api/users/[id].get.ts
export default defineEventHandler(async (event) => {
  const id = getRouterParam(event, 'id');

  // 这里可以查询数据库
  // const user = await db.findUserById(id);

  return {
    id,
    name: `User ${id}`,
  };
});

2.3 与数据库交互

以 Prisma + Postgres 为例:

// server/utils/prisma.ts
import { PrismaClient } from '@prisma/client';

const globalForPrisma = globalThis as { prisma?: PrismaClient };
export const prisma = globalForPrisma.prisma ?? new PrismaClient();

if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma;
// server/api/posts/index.get.ts
import { prisma } from '~/server/utils/prisma';

export default defineEventHandler(async () => {
  const posts = await prisma.post.findMany({
    orderBy: { createdAt: 'desc' },
    take: 20,
  });
  return posts;
});

⚠️ 注意:Nuxt Server 路由在 Vercel 上运行在 Node.js Serverless Functions 中(不是 Edge Runtime)。Prisma 等依赖 Node.js 绑定的库可以正常工作。如果你想把部分逻辑放到 Edge,需要手动编写 Vercel Edge Middleware(与 Nuxt Server 路由是两套体系)。

2.4 获取请求体和查询参数

// server/api/upload.post.ts
export default defineEventHandler(async (event) => {
  // 查询参数:?category=tech
  const query = getQuery(event);
  const category = query.category as string;

  // 请求体(JSON)
  const body = await readBody(event);

  // 表单数据
  const formData = await readMultipartFormData(event);

  return { category, body };
});

2.5 Nitro 中间件与 Vercel Functions 生命周期

Nitro 的中间件系统是对请求/响应的统一拦截层,在 Vercel 上运行时,它与 Serverless Functions 的冷启动特性密切相关。

Server Middleware 的编写与注册

server/middleware/ 目录下创建文件,Nitro 会自动按文件名排序执行:

// server/middleware/01.cors.ts
export default defineEventHandler((event) => {
  setResponseHeaders(event, {
    'Access-Control-Allow-Origin': '*',
    'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
    'Access-Control-Allow-Headers': 'Content-Type, Authorization',
  });

  if (event.method === 'OPTIONS') {
    event.node.res.statusCode = 204;
    event.node.res.end();
  }
});
// server/middleware/02.logger.ts
export default defineEventHandler((event) => {
  const start = Date.now();
  event.context.startTime = start;

  event.node.res.on('finish', () => {
    const duration = Date.now() - start;
    console.log(
      `[${event.method}] ${event.path} - ${event.node.res.statusCode} (${duration}ms)`
    );
  });
});

中间件执行顺序由文件名前缀决定(01.02.),这与 Express 等框架的 app.use() 顺序控制类似。在 Vercel 上,每个中间件都会在 Function 调用链中执行,因此尽量保持轻量,避免在全局中间件中执行数据库连接等耗时操作。

请求/响应拦截实战

安全头中间件示例:

// server/middleware/03.security.ts
export default defineEventHandler((event) => {
  const csp = [
    "default-src 'self'",
    "script-src 'self' 'unsafe-inline'",
    "style-src 'self' 'unsafe-inline'",
    "img-src 'self' data: https:",
  ].join('; ');

  setResponseHeaders(event, {
    'X-Frame-Options': 'DENY',
    'X-Content-Type-Options': 'nosniff',
    'Referrer-Policy': 'strict-origin-when-cross-origin',
    'Content-Security-Policy': csp,
  });
});

Nitro 插件开发与 Vercel 适配

Nitro 插件在应用启动时执行一次,非常适合做全局初始化:

// server/plugins/prisma.ts
import { PrismaClient } from '@prisma/client';

export default defineNitroPlugin((nitroApp) => {
  const prisma = new PrismaClient();
  nitroApp.hooks.hook('request', (event) => {
    event.context.prisma = prisma;
  });

  nitroApp.hooks.hook('close', async () => {
    await prisma.$disconnect();
  });
});

在 Vercel Serverless Functions 中,进程可能在多次请求间复用(warm container),因此:

  • 全局初始化defineNitroPlugin 中):只在冷启动时执行一次,复用期间不再运行,性能最优。
  • 请求级初始化(API Route 内部每次创建):简单直接,但频繁创建连接会导致数据库压力增大。

推荐将数据库连接池、Redis 客户端等长生命周期对象放在 Nitro 插件中初始化,挂到 event.context 上供请求共享。

函数冷启动优化

Vercel Functions 的冷启动时间直接影响首屏 TTFB。优化策略:

  1. 减小 Bundle 体积serverDependenciesToBundle 中只打包必要的依赖,未使用的库通过 externals 排除。
  2. 避免同步 I/O:Nitro 插件中不要做大量文件读取或同步网络请求。
  3. 连接预热:在插件中初始化数据库连接,而非第一次请求时才建立。
// nuxt.config.ts
export default defineNuxtConfig({
  nitro: {
    externals: {
      external: ['@prisma/client', 'pg'],
    },
  },
});

Nitro 在 Vercel 上的打包产物分析

执行 npm run build 后,.output/ 目录结构如下:

.output/
  public/          # 静态资源(CDN 直出)
  server/
    index.mjs      # Nitro 入口
    chunks/        # 代码分片
    node_modules/  # 内联的依赖

preset: 'vercel' 启用时,产物会额外输出到 .vercel/output/functions/

.vercel/output/
  functions/__nitro.func/
    index.mjs      # Vercel Function 入口
    .vc-config.json
  static/          # 对应 .output/public

你可以通过 npx nitro inspect(Nuxt 3.12+)查看服务端打包的依赖图谱,找出体积过大的包并进行优化。


三、部署前的准备

3.1 环境变量配置

创建 .env

NUXT_DATABASE_URL="postgresql://user:pass@host:5432/db"
NUXT_API_SECRET="your-secret-key"
NUXT_PUBLIC_APP_NAME="My Nuxt App"

Nuxt 的环境变量分为两类:

前缀作用域示例
NUXT_NUXT_PRIVATE_服务端专用数据库连接、API Secret
NUXT_PUBLIC_服务端 + 客户端共享App 名称、API Base URL

在代码中读取:

// 服务端(Server Route、SSR 组件)
const config = useRuntimeConfig();
console.log(config.databaseUrl);     // 服务端私有
console.log(config.public.appName);  // 客户端也可访问
<!-- 客户端组件 -->
<template>
  <h1>{{ appName }}</h1>
</template>

<script setup>
const appName = useRuntimeConfig().public.appName;
</script>

3.2 Vercel 环境变量设置

  1. Vercel Dashboard → 项目 → Settings → Environment Variables
  2. 添加所有 NUXT_NUXT_PUBLIC_ 开头的变量
  3. Production 和 Preview 环境都配置一遍(或至少 Production)

3.3 构建配置检查

Vercel 会自动识别 Nuxt 项目,但建议确认构建配置:

  • Build Command: npm run build(或 nuxt build
  • Output Directory: .output/public
  • Install Command: npm install

如果 nuxt.config.ts 中正确设置了 preset: 'vercel',Vercel 会自动生成 .vercel/output/functions/ 下的 Functions 打包文件。


四、在 Vercel 上部署

4.1 Git 推送自动部署

git init
git add .
git commit -m "Init Nuxt project"
git remote add origin https://github.com/yourname/my-nuxt-app.git
git push -u origin main
  1. 在 Vercel Dashboard 点击 “New Project”
  2. 导入 GitHub 仓库 my-nuxt-app
  3. Vercel 自动识别为 Nuxt 项目,保持默认构建配置
  4. 确认 Environment Variables 已添加
  5. 点击 “Deploy”,等待构建完成

4.2 手动部署(Vercel CLI)

# 安装 Vercel CLI
npm i -g vercel

# 登录
vercel login

# 在项目根目录执行
vercel

# 首次会提示配置,后续推送自动部署
# 部署到生产环境
vercel --prod

4.3 验证部署

构建完成后,访问 https://my-nuxt-app.vercel.app 验证:

# 测试首页
curl https://my-nuxt-app.vercel.app

# 测试 API
curl https://my-nuxt-app.vercel.app/api/hello

# 测试带参数 API
curl https://my-nuxt-app.vercel.app/api/users/123

五、图片与静态资源优化

5.1 Nuxt Image 模块

npm install -D @nuxt/image
// nuxt.config.ts
export default defineNuxtConfig({
  modules: ['@nuxt/image'],
  image: {
    domains: ['cdn.yourdomain.com'],
    screens: {
      xs: 320,
      sm: 640,
      md: 768,
      lg: 1024,
      xl: 1280,
    },
  },
});
<!-- 自动响应式图片 -->
<NuxtImg
  src="/hero.jpg"
  alt="Hero"
  sizes="xs:100vw sm:50vw md:400px"
  loading="lazy"
  placeholder
/>

@nuxt/image 在 Vercel 上会自动使用 Vercel Image Optimization API,无需配置第三方图片 CDN。

5.2 静态资源缓存

Nuxt 的 .output/public/ 目录下的文件会被 Vercel 自动上传到 CDN。为了进一步优化缓存:

// nuxt.config.ts
export default defineNuxtConfig({
  nitro: {
    routeRules: {
      '/_nuxt/**': {
        headers: {
          'cache-control': 'public, max-age=31536000, immutable',
        },
      },
      '/images/**': {
        headers: {
          'cache-control': 'public, max-age=604800',
        },
      },
    },
  },
});

5.3 CDN 与缓存策略深度

Vercel 的 CDN 是全球 Anycast 网络,默认缓存静态资源。但对于 Nuxt SSR/ISR 项目,仅依赖默认缓存远远不够,需要设计多层缓存架构。

Vercel 边缘缓存与 Cloudflare 的协同配置

如果你的域名通过 Cloudflare Proxy(橙色云),请求路径为:

用户 → Cloudflare CDN → Vercel Edge Network → Vercel Function (SSR) → 源站 API

此时存在两层 CDN,缓存头会按如下规则传递:

层级控制头作用
CloudflareCache-Control (CDN 专用)按 Cloudflare 页规缓存
Vercel EdgeCDN-Cache-ControlVercel-CDN-Cache-ControlVercel 网络独占

如果希望 Vercel 管理缓存、Cloudflare 只做 DNS,在 Cloudflare 的 Page Rules 中设置 Cache Level: Bypass

https://www.yourdomain.com/*
Cache Level: Bypass

如果希望在 Cloudflare 层也参与缓存(如自定义缓存键、WAF 规则),需要确保两边的 max-age 策略一致,避免"旧缓存套新缓存"导致内容更新延迟。

自定义缓存头的高级用法

// nuxt.config.ts
export default defineNuxtConfig({
  nitro: {
    routeRules: {
      // 静态资源:永久缓存,文件名含 hash,变更是新文件
      '/_nuxt/**': {
        headers: {
          'cache-control': 'public, max-age=31536000, immutable',
        },
      },
      // 图片:7 天强缓存 + 7 天静默重验
      '/images/**': {
        headers: {
          'cache-control': 'public, s-maxage=604800, stale-while-revalidate=604800',
        },
      },
      // ISR 页面:由 Nuxt 自动生成 SWR 头,无需手动配置
      '/posts/**': { isr: 3600 },
      // API 响应:浏览器不缓存,CDN 缓存 60 秒
      '/api/public/**': {
        headers: {
          'cache-control': 'public, max-age=0, s-maxage=60',
        },
      },
    },
  },
});

各指令含义:

  • s-maxage:仅 CDN/共享缓存使用,浏览器忽略。适合让边缘节点缓存 API 响应,但客户端总是重新验证。
  • stale-while-revalidate:缓存过期后,CDN 先返回旧内容,后台异步回源获取新内容。对用户"零感知"更新。
  • immutable:表示资源永远不会变(适用于哈希文件名)。浏览器在 max-age 内连条件请求(If-None-Match)都不会发,减少 304 开销。

Vercel Functions 的响应缓存策略

Vercel Functions 默认不缓存动态响应。要让 SSR 页面命中 CDN,必须依赖 Nuxt 的 ISR 机制或手动设置 CDN-Cache-Control

// server/api/public/stats.get.ts
export default defineEventHandler((event) => {
  setResponseHeader(event, 'CDN-Cache-Control', 'public, max-age=300');
  return { visitors: 12345, updatedAt: new Date().toISOString() };
});

这里的 CDN-Cache-Control 只被 Vercel Edge 识别,不会暴露给浏览器,安全性更高。

多层缓存架构设计

一个典型的 Nuxt + Vercel 生产项目,缓存层级如下:

浏览器缓存        →  _nuxt/** (immutable, 1年)
                  →  图片/字体 (max-age=1周)

Vercel Edge 缓存  →  ISR 页面 (isr=3600, stale-while-revalidate)
                  →  公开 API (s-maxage=60)

源站/Function     →  SSR 页面 (无缓存,每次渲染)
                  →  私有 API (无缓存)

数据库/缓存层     →  Redis + 应用级缓存(如 Nuxt 的 Nitro 缓存)

实际配置时,遵循"越靠近用户越激进缓存,越靠近源站越谨慎缓存"的原则:

  1. 静态资源:浏览器 + CDN 双重长期缓存。
  2. ISR 页面:CDN 缓存 + 后台重验,平衡实时性与性能。
  3. 纯 SSR 页面:不缓存,确保每次请求都是最新数据。
  4. API 层:视数据敏感度决定,公开数据可设短缓存,私有数据不设缓存。

六、ISR 与缓存策略

6.1 ISR 配置详解

// nuxt.config.ts
export default defineNuxtConfig({
  routeRules: {
    // 首页:ISR 60 秒
    '/': { isr: 60 },

    // 内容页:ISR 1 小时
    '/posts/**': { isr: 3600 },

    // 用户页:SSR(不缓存)
    '/user/**': { isr: false },

    // API:CORS + 不缓存
    '/api/**': { isr: false, cors: true },
  },
});

ISR 在 Vercel 上的行为:

  • 首次请求:Serverless Function 渲染,返回 HTML,同时缓存到 Vercel CDN
  • 缓存期内(如 60 秒):后续请求直接命中 CDN,Vercel 不计费 Serverless 执行
  • 缓存过期后:下一个请求会触发背景重新生成(stale-while-revalidate),用户仍收到旧缓存

6.2 On-Demand Revalidation(手动失效)

// server/api/revalidate.post.ts
export default defineEventHandler(async (event) => {
  const body = await readBody(event);
  const { path, token } = body;

  // 校验密钥
  const config = useRuntimeConfig();
  if (token !== config.revalidateToken) {
    throw createError({ statusCode: 401, statusMessage: 'Invalid token' });
  }

  // 触发重新验证
  const nitro = useNitroApp();
  await nitio.hooks.callHook(' nitro:cache:invalidate', { path });

  return { revalidated: true };
});

从 CMS 或后台管理触发失效:

curl -X POST https://my-nuxt-app.vercel.app/api/revalidate \
  -H "Content-Type: application/json" \
  -d '{"path": "/posts/hello-world", "token": "your-secret"}'

6.3 Nuxt 与 Vercel Edge Functions 混搭

Nuxt 的 Server Routes 默认运行在 Vercel 的传统 Node.js Serverless Functions 中,而 Vercel Edge Functions 基于 V8 Runtime,具有更低的冷启动和全球边缘调度能力。两者可以互补使用。

在 Nuxt 项目中使用独立的 Vercel Edge Middleware

在项目根目录创建 middleware.ts(Vercel 约定),它与 Nuxt 的 server/middleware/ 完全独立:

// middleware.ts
import { NextRequest, NextResponse } from 'next/server';

export const config = {
  matcher: ['/api/edge/:path*', '/protected/:path*'],
};

export default function middleware(request: NextRequest) {
  const country = request.geo?.country || 'UNKNOWN';

  // 地区封禁示例
  if (country === 'XX') {
    return new NextResponse('Access Denied', { status: 403 });
  }

  // 注入请求头供 Nuxt Server 路由读取
  const requestHeaders = new Headers(request.headers);
  requestHeaders.set('x-edge-country', country);

  return NextResponse.next({
    request: { headers: requestHeaders },
  });
}

在 Nuxt Server 路由中读取 Edge Middleware 注入的头信息:

// server/api/geoip.get.ts
export default defineEventHandler((event) => {
  const headers = getRequestHeaders(event);
  const country = headers['x-edge-country'] || 'UNKNOWN';
  return { country };
});

部署后,请求链路变为:

用户请求 → Vercel Edge (middleware.ts) → Nuxt Serverless Function → 响应

Edge Middleware 在距离用户最近的边缘节点执行,适合做轻量的请求拦截、A/B 测试、地区跳转等。Nuxt Server Function 则在中心区域执行,适合做数据库操作、复杂业务逻辑。

Edge Middleware 中 JWT 认证与 Nuxt Server 路由的协同

对于需要验证身份的场景,可以在 Edge Middleware 中解析 JWT,校验通过后再将用户信息注入头信息:

// middleware.ts
import { jwtVerify } from 'jose';
import { NextRequest, NextResponse } from 'next/server';

export default async function middleware(request: NextRequest) {
  const token = request.cookies.get('auth-token')?.value;
  const requestHeaders = new Headers(request.headers);

  if (token) {
    try {
      const secret = new TextEncoder().encode(process.env.JWT_SECRET);
      const { payload } = await jwtVerify(token, secret);
      requestHeaders.set('x-user-id', payload.sub as string);
      requestHeaders.set('x-user-role', payload.role as string);
    } catch {
      // Token 无效,继续不注入用户信息
    }
  }

  return NextResponse.next({ request: { headers: requestHeaders } });
}
// server/api/admin/dashboard.get.ts
export default defineEventHandler((event) => {
  const headers = getRequestHeaders(event);
  const userId = headers['x-user-id'];
  const role = headers['x-user-role'];

  if (role !== 'admin') {
    throw createError({ statusCode: 403, statusMessage: 'Forbidden' });
  }

  return { message: 'Admin data', userId };
});

⚠️ 注意:Edge Middleware 不能访问 Node.js 原生模块(如 fscrypto 某些方法)。JWT 验证需使用 Web Crypto API 兼容库(如 jose)。

Edge Function 替代部分 Serverless API 的场景与性能对比

以下场景适合用 Edge Function(或 Edge Middleware)替代 Serverless Function:

场景Serverless FunctionEdge Function推荐
地区跳转 / IP 封禁高延迟边缘直接处理Edge
轻薄 API(无数据库)冷启动 50-200ms冷启动 <5msEdge
实时地理位置服务需回中心区域边缘原生支持 request.geoEdge
复杂数据库查询Prisma/ORM 完美支持无 Node.js 绑定,受限Serverless
文件上传处理支持流处理请求体大小受限Serverless

在 Nuxt 项目中,你可以通过 Vercel 的单独 API 目录 api/edge/(如果使用 Vercel CLI 部署原生 Edge Functions)与 Nuxt Server Routes 共存。但通常更简单的做法是让 Nuxt 负责核心业务 API,Edge Middleware 负责请求前置处理。


七、常见坑点与排查

7.1 “404 Not Found” 部署后

原因:Vercel 没有正确识别 Nuxt 的 output 目录。

修复

  1. 确认 nuxt.config.ts 中有 nitro: { preset: 'vercel' }
  2. 检查 Vercel Dashboard → Settings → General → Output Directory 是否设为 .output/public
  3. 如果输出目录为空,检查 npm run build 是否成功生成了 .output/ 目录

7.2 API 路由返回 500

排查步骤

  1. Vercel Dashboard → Logs → 选择你的项目 → 查看最近的 Function Error
  2. 常见原因:环境变量缺失(NUXT_DATABASE_URL 没配置)、Prisma 生成未执行
  3. 如果是 Prisma 问题,在构建命令中加 prisma generate
// package.json
{
  "scripts": {
    "build": "prisma generate && nuxt build",
    "postinstall": "prisma generate"
  }
}

7.3 环境变量在客户端读取不到

原因:客户端只能访问 NUXT_PUBLIC_ 前缀的环境变量。

确认

  • 服务端 .envNUXT_PUBLIC_API_BASE=https://api.example.com
  • 客户端代码:useRuntimeConfig().public.apiBase
  • 服务端代码:useRuntimeConfig().apiBase ❌(非 public 前缀在客户端不可见)

7.4 构建时间过长

  • 减少 node_modules 体积:npm prune --production 后上传
  • 启用 swc(Nuxt 3 默认已启用)
  • 对于 monorepo,使用 Nx/Turborepo 远程缓存

八、从 Demo 到生产

8.1 添加自定义域名

参照 Vercel 国内访问优化指南,配置 www.yourdomain.com + Cloudflare Proxy,确保中国大陆访问稳定。

8.2 添加监控

# 安装 Vercel Analytics
npm install -D @vercel/analytics
// nuxt.config.ts
export default defineNuxtConfig({
  plugins: [
    { src: '~/plugins/analytics.client.ts', mode: 'client' },
  ],
});
// plugins/analytics.client.ts
import { inject } from '@vercel/analytics';

export default defineNuxtPlugin(() => {
  inject();
});

8.3 安全头配置

// nuxt.config.ts
export default defineNuxtConfig({
  nitro: {
    routeRules: {
      '/**': {
        headers: {
          'X-Frame-Options': 'DENY',
          'X-Content-Type-Options': 'nosniff',
          'Referrer-Policy': 'strict-origin-when-cross-origin',
          'Content-Security-Policy': "default-src 'self'; script-src 'self' 'unsafe-inline'",
        },
      },
    },
  },
});

8.4 性能基准与监控

将 Nuxt 项目部署到 Vercel 后,性能监控是保障用户体验的关键环节。下面从实测数据、诊断工具、监控集成三个维度展开。

Nuxt 在 Vercel 上的实测性能数据

以下是在同一测试页面(包含 API 数据获取、图片、组件水合)下,不同渲染模式的性能对比(使用 Chrome Lighthouse + WebPageTest,模拟 4G 网络):

指标SSRSSGISRSPA
TTFB180-350ms30-60ms40-80ms30-50ms
LCP1.2-1.8s0.8-1.2s0.9-1.4s1.5-2.5s
CLS0.01-0.050.01-0.030.01-0.030.05-0.15
INP~120ms~80ms~90ms~200ms
Function 调用每次请求仅构建时ISR 过期时仅 API 请求时

解读:

  • SSG 最快最省:没有 Function 运行时开销,TTFB 和 LCP 都最优。适合内容站。
  • ISR 是最佳折中:接近 SSG 的速度,同时允许内容更新。TTFB 略高于 SSG 是因为首次未命中缓存时需要回源渲染。
  • SSR 开销最大:每次请求都运行 Function,TTFB 受冷启动和数据库查询影响。适合强个性化页面。
  • SPA 首屏最差:HTML 骨架无实质内容,LCP 和 INP 明显落后。但后续交互流畅。

优化建议:

  1. SSR 页面:使用 useAsyncData + 服务端数据直出,避免客户端二次请求。启用 Nitro 缓存减少重复渲染。
  2. SSG/ISR 页面:确保关键图片使用 <NuxtImg> 并配置 priority,减少 LCP 元素的加载时间。
  3. 全局:开启 experimental.payloadExtraction(Nuxt 3.8+),SSR 时只返回 HTML,数据通过独立 JSON 加载,减少 HTML 体积。

Nuxt DevTools 在生产环境的诊断能力

Nuxt DevTools 不仅是开发工具,还可以在生产环境提供运行时诊断:

# 生产环境启用 DevTools(建议只在需要排查时临时开启)
# .env
NUXT_DEVTOOLS_ENABLED=true

在 Vercel 上,由于无法直接打开 DevTools UI,你可以通过服务端 API 导出诊断信息:

// server/api/_.debug.get.ts
export default defineEventHandler(() => {
  const nitro = useNitroApp();
  return {
    routes: Object.keys(nitro.scannedHandlers || {}),
    modules: Object.keys(nitro.modules || {}),
    hooks: nitro.hooks._hooks,
  };
});

更实际的做法是在本地用生产构建复现问题:

# 本地模拟 Vercel 生产环境
npm run build
NUXT_DEVTOOLS_ENABLED=true node .output/server/index.mjs

DevTools 的 “Server Routes” 面板可以显示每个 API 的响应时间和调用次数,“Payload” 面板可以查看 SSR 时传递的数据结构,帮助定位 Hydration Mismatch。

与 Vercel Analytics / Speed Insights 的集成配置

Vercel 提供原生的 Web Analytics 和 Speed Insights,与 Nuxt 集成非常简单。

1. Web Analytics(访客分析)

npm install -D @vercel/analytics
// plugins/analytics.client.ts
import { inject } from '@vercel/analytics';

export default defineNuxtPlugin(() => {
  inject();
});

在 Vercel Dashboard → Analytics 中可查看页面浏览量、访客来源、设备分布等。无需在页面中手动埋点,自动追踪所有路由切换(基于 Vue Router 的 afterEach 钩子)。

2. Speed Insights(Web Vitals)

npm install -D @vercel/speed-insights
// plugins/speed-insights.client.ts
import { injectSpeedInsights } from '@vercel/speed-insights';

export default defineNuxtPlugin(() => {
  injectSpeedInsights();
});

部署后,Vercel 会自动收集真实用户的 Core Web Vitals(LCP、FID/INP、CLS、TTFB、FCP),在 Dashboard → Speed Insights 中按页面、设备、连接类型分组展示。

3. 自定义监控指标

如果你需要监控业务指标(如 API 错误率、数据库查询耗时),可以在 Nitro 插件中对接第三方 APM(如 Sentry、Datadog):

// server/plugins/monitoring.ts
import * as Sentry from '@sentry/node';

export default defineNitroPlugin((nitroApp) => {
  Sentry.init({ dsn: process.env.SENTRY_DSN });

  nitroApp.hooks.hook('error', (error) => {
    Sentry.captureException(error);
  });
});

结合 Vercel 的 Function Logs 和第三方 APM,你可以建立从"用户感知性能"到"服务端执行细节"的完整监控闭环。


常见问题(FAQ)

Nuxt 2 和 Nuxt 3 在 Vercel 上有什么不同?

Nuxt 3 使用 Nitro 引擎,原生支持 Vercel preset,部署零配置。Nuxt 2 需要 vercel-builder 适配器,且性能不如 Nitro。新项目请用 Nuxt 3。

Nuxt 在 Vercel 上能跑 Edge Functions 吗?

Nuxt 的 Server Routes 默认运行在传统 Serverless Functions(Node.js Runtime)。如果确实需要 Edge Runtime,可以用 Vercel 的独立 Edge Middleware(middleware.ts),与 Nuxt 应用并行部署。

Nuxt 的 useFetch 在 SSR 时请求的是哪里?

在 Vercel 的 SSR 模式下,useFetch('/api/posts') 会在服务器端直接调用 Nitro 内部的 API 路由(不走 HTTP),性能最优。在客户端(hydration 后),useFetch 会走正常的 HTTP 请求。

Vercel 对 Nuxt 的 ISR 支持如何?

完全支持。Nuxt 的 routeRules.isrpreset: 'vercel' 下会自动使用 Vercel 的 Incremental Static Regeneration 缓存层,行为和 Next.js ISR 类似。

Nuxt 3 的 useHead SEO 在 SSR 中如何使用?

Nuxt 3 提供 useHead composable,在 SSR 时会自动将元标签注入到 <head> 中,对 SEO 和社交分享至关重要。

<script setup>
useHead({
  title: '文章标题 - 我的博客',
  meta: [
    { name: 'description', content: '文章摘要...' },
    { property: 'og:title', content: '文章标题' },
    { property: 'og:image', content: 'https://example.com/cover.jpg' },
  ],
  link: [
    { rel: 'canonical', href: 'https://example.com/posts/hello' },
  ],
});
</script>

在 Vercel SSR 模式下,useHead 的内容会在服务端渲染时直接写入 HTML,搜索引擎爬虫无需执行 JavaScript 即可获取完整的标题和描述。如果你的页面数据来自异步接口,建议与 useAsyncData 配合使用:

<script setup>
const { data: post } = await useFetch('/api/posts/hello');

useHead(() => ({
  title: post.value?.title,
  meta: [
    { name: 'description', content: post.value?.excerpt },
  ],
}));
</script>

注意 useHead 支持传入函数,这样可以在异步数据就绪后动态更新元信息。

有哪些与 Vercel 相关的 Nuxt Modules 推荐?

以下是部署到 Vercel 时高频使用的官方和社区模块:

模块安装作用
@nuxt/imagenpx nuxi module add image图片优化,自动对接 Vercel Image Optimization API
@nuxtjs/partytownnpx nuxi module add partytown将第三方脚本(如 Google Analytics)移至 Web Worker,减少主线程阻塞
nuxt-securitynpm i -D nuxt-security一键配置安全头、CSP、CORS,生产环境必备
@nuxtjs/fontainenpx nuxi module add fontaine字体回退优化,减少 CLS(累积布局偏移)
@nuxtjs/web-vitalsnpx nuxi module add web-vitals自动上报 Core Web Vitals 到指定端点

特别推荐 nuxt-security,它在 Vercel 上可以直接替代手动编写的安全头中间件,且支持根据路由动态调整 CSP 策略。

Nuxt 的 preview 模式与 Vercel Preview 环境如何配合?

Nuxt 提供 nuxt preview 命令用于本地预览生产构建,而 Vercel 每次 Push 都会自动生成 Preview Deployment。两者可以结合使用:

# 本地构建并启动预览服务器
npm run build
npx nuxt preview

Vercel Preview 环境会自动分配一个独立 URL(如 my-nuxt-app-git-feature-xxx.vercel.app),与 Production 环境隔离。你可以在 nuxt.config.ts 中根据环境做差异化配置:

export default defineNuxtConfig({
  runtimeConfig: {
    public: {
      apiBase: process.env.VERCEL_ENV === 'preview'
        ? 'https://staging-api.example.com'
        : 'https://api.example.com',
    },
  },
});

此外,Vercel 的 Preview 环境支持 Comments,团队成员可以直接在预览 URL 的页面上添加评论,极大提升协作效率。

Vercel 上 Nuxt 项目的 CI/CD 最佳实践是什么?

对于生产级 Nuxt 项目,建议在 CI/CD 流程中关注以下几点:

1. 构建缓存

在 GitHub Actions 中启用 Vercel 远程缓存或本地缓存:

# .github/workflows/deploy.yml
- uses: actions/cache@v4
  with:
    path: |
      .nuxt
      .output
      node_modules
    key: ${{ runner.os }}-nuxt-${{ hashFiles('**/package-lock.json') }}

2. 环境变量管理

  • 敏感变量(数据库密码、API Key):只在 Vercel Dashboard 中配置,不要写入代码仓库。
  • 公开变量(API Base URL):可以在 .env.example 中给出模板,方便团队新成员本地启动。

3. 自动化的 Lighthouse CI

- name: Lighthouse CI
  run: |
    npm install -g @lhci/cli
    lhci autorun --upload.target=temporary-public-storage

每次 PR 都跑 Lighthouse,确保性能分数不会倒退。

4. 分支策略

  • main → Production
  • develop → Preview(用于集成测试)
  • feature/* → Preview(每个功能分支独立预览 URL)

Vercel 的 Git Integration 自动为每个分支生成独立部署,团队成员可以直接在 PR 中查看预览效果。

相关阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「saas」更多文章

  1. 短链接对 SEO 的影响与优化最佳实践
  2. UTM 参数 + 短链接:追踪每一条营销链路
  3. 私域流量运营中的短链接策略:从引流到转化