Skip to content

Next.js 部署与优化

课程概述

本课程深入探讨 Next.js 15 应用的部署和优化。掌握部署策略、性能优化、监控和调试技巧,确保应用在生产环境中高效运行。

学习目标:

  • 理解 Next.js 部署选项
  • 掌握 Vercel 部署
  • 学习自托管部署
  • 理解 Docker 容器化
  • 掌握性能优化技巧
  • 学习监控和分析
  • 理解安全最佳实践
  • 构建生产就绪的应用

一、部署基础

1.1 Next.js 输出模式

javascript
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  // 输出模式选择
  output: 'standalone', // 生成独立的服务器应用
  // output: 'export',  // 生成静态HTML导出
  // output: undefined, // 默认Node.js服务器
}

module.exports = nextConfig

输出模式对比:

模式说明适用场景
undefined (默认)完整Node.js应用需要所有Next.js功能
standalone最小化独立应用Docker部署,减小体积
export静态HTML文件静态托管,CDN

1.2 构建命令

bash
# 开发环境
npm run dev
# 或
pnpm dev

# 生产构建
npm run build
# 或
pnpm build

# 启动生产服务器
npm start
# 或
pnpm start

# 构建 + 启动
npm run build && npm start

1.3 环境变量

bash
# .env.local (本地开发,不提交到 Git)
DATABASE_URL=postgresql://localhost/mydb
API_SECRET=super-secret-key

# .env.production (生产环境)
DATABASE_URL=postgresql://prod-server/proddb
API_SECRET=production-secret

# .env (所有环境)
NEXT_PUBLIC_API_URL=https://api.example.com
typescript
// app/config.ts
export const config = {
  // 服务器端可访问
  database: {
    url: process.env.DATABASE_URL,
  },
  
  // 客户端可访问 (NEXT_PUBLIC_ 前缀)
  api: {
    url: process.env.NEXT_PUBLIC_API_URL,
  },
}

// 使用
import { config } from '@/app/config'

export default function Page() {
  return <div>API: {config.api.url}</div>
}

二、Vercel 部署

2.1 快速部署

bash
# 1. 安装 Vercel CLI
npm i -g vercel

# 2. 登录
vercel login

# 3. 部署
vercel

# 4. 生产部署
vercel --prod

2.2 Git 集成

yaml
# 自动部署配置
# 当推送到 GitHub/GitLab/Bitbucket 时自动部署

# 分支策略:
# main/master → 生产环境
# develop → 预览环境
# feature/* → 临时预览

# vercel.json
{
  "github": {
    "enabled": true,
    "autoAlias": true
  },
  "buildCommand": "pnpm build",
  "installCommand": "pnpm install",
  "framework": "nextjs"
}

2.3 环境变量配置

bash
# Vercel 环境变量设置

# 方式1: 通过 CLI
vercel env add DATABASE_URL production
vercel env add API_SECRET production

# 方式2: 通过 Dashboard
# https://vercel.com/your-project/settings/environment-variables

# 方式3: 通过 vercel.json
{
  "env": {
    "NEXT_PUBLIC_API_URL": "https://api.example.com"
  },
  "build": {
    "env": {
      "DATABASE_URL": "@database-url"
    }
  }
}

2.4 域名配置

bash
# 添加自定义域名
vercel domains add example.com

# 配置 DNS
# A Record: 76.76.21.21
# CNAME: cname.vercel-dns.com

# 强制 HTTPS (自动启用)
# SSL 证书自动配置

2.5 Vercel 配置文件

json
// vercel.json
{
  "version": 2,
  "name": "my-nextjs-app",
  "builds": [
    {
      "src": "package.json",
      "use": "@vercel/next"
    }
  ],
  "routes": [
    {
      "src": "/api/(.*)",
      "dest": "/api/$1"
    }
  ],
  "env": {
    "NEXT_PUBLIC_API_URL": "https://api.example.com"
  },
  "build": {
    "env": {
      "DATABASE_URL": "@database-url"
    }
  },
  "headers": [
    {
      "source": "/(.*)",
      "headers": [
        {
          "key": "X-Content-Type-Options",
          "value": "nosniff"
        },
        {
          "key": "X-Frame-Options",
          "value": "DENY"
        },
        {
          "key": "X-XSS-Protection",
          "value": "1; mode=block"
        }
      ]
    }
  ],
  "redirects": [
    {
      "source": "/old-page",
      "destination": "/new-page",
      "permanent": true
    }
  ],
  "rewrites": [
    {
      "source": "/api/:path*",
      "destination": "https://external-api.com/:path*"
    }
  ]
}

三、自托管部署

3.1 Node.js 服务器

bash
# 1. 构建应用
npm run build

# 2. 启动服务器
npm start

# 或使用 PM2
npm install -g pm2
pm2 start npm --name "nextjs-app" -- start
pm2 save
pm2 startup
javascript
// ecosystem.config.js (PM2 配置)
module.exports = {
  apps: [{
    name: 'nextjs-app',
    script: 'npm',
    args: 'start',
    instances: 'max',
    exec_mode: 'cluster',
    env: {
      NODE_ENV: 'production',
      PORT: 3000
    },
    error_file: './logs/err.log',
    out_file: './logs/out.log',
    log_date_format: 'YYYY-MM-DD HH:mm:ss'
  }]
}

// 使用
// pm2 start ecosystem.config.js

3.2 Nginx 反向代理

nginx
# /etc/nginx/sites-available/nextjs-app

upstream nextjs_app {
    server 127.0.0.1:3000;
}

server {
    listen 80;
    server_name example.com www.example.com;
    
    # 重定向到 HTTPS
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl http2;
    server_name example.com www.example.com;
    
    # SSL 证书
    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
    
    # SSL 配置
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!aNULL:!MD5;
    ssl_prefer_server_ciphers on;
    
    # 日志
    access_log /var/log/nginx/nextjs-app-access.log;
    error_log /var/log/nginx/nextjs-app-error.log;
    
    # Gzip 压缩
    gzip on;
    gzip_vary on;
    gzip_min_length 1024;
    gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
    
    # 静态文件
    location /_next/static/ {
        proxy_pass http://nextjs_app;
        proxy_cache_valid 200 365d;
        add_header Cache-Control "public, max-age=31536000, immutable";
    }
    
    # 图片优化
    location /_next/image {
        proxy_pass http://nextjs_app;
        proxy_cache_valid 200 7d;
        add_header Cache-Control "public, max-age=604800";
    }
    
    # API 路由
    location /api/ {
        proxy_pass http://nextjs_app;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_cache_bypass $http_upgrade;
    }
    
    # 其他请求
    location / {
        proxy_pass http://nextjs_app;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_cache_bypass $http_upgrade;
    }
}
bash
# 启用配置
sudo ln -s /etc/nginx/sites-available/nextjs-app /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

# SSL 证书 (Let's Encrypt)
sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d example.com -d www.example.com

3.3 Standalone 模式部署

javascript
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  output: 'standalone',
}

module.exports = nextConfig
bash
# 构建
npm run build

# 文件结构
.next/
  standalone/
    .next/
    node_modules/
    package.json
    server.js
  static/

# 复制文件
cp -r .next/standalone ./deploy
cp -r .next/static ./deploy/.next/static
cp -r public ./deploy/public

# 部署到服务器
cd deploy
node server.js

四、Docker 容器化

4.1 Dockerfile

dockerfile
# Dockerfile
FROM node:20-alpine AS base

# 安装依赖阶段
FROM base AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app

COPY package.json pnpm-lock.yaml* ./
RUN corepack enable pnpm && pnpm install --frozen-lockfile

# 构建阶段
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .

ENV NEXT_TELEMETRY_DISABLED 1

RUN corepack enable pnpm && pnpm build

# 运行阶段
FROM base AS runner
WORKDIR /app

ENV NODE_ENV production
ENV NEXT_TELEMETRY_DISABLED 1

RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs

COPY --from=builder /app/public ./public

# Standalone 模式
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static

USER nextjs

EXPOSE 3000

ENV PORT 3000
ENV HOSTNAME "0.0.0.0"

CMD ["node", "server.js"]

4.2 Docker Compose

yaml
# docker-compose.yml
version: '3.8'

services:
  nextjs:
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - "3000:3000"
    environment:
      - DATABASE_URL=postgresql://postgres:password@db:5432/mydb
      - NEXT_PUBLIC_API_URL=https://api.example.com
    depends_on:
      - db
    restart: unless-stopped

  db:
    image: postgres:16-alpine
    environment:
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=password
      - POSTGRES_DB=mydb
    volumes:
      - postgres_data:/var/lib/postgresql/data
    restart: unless-stopped

  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf
      - ./ssl:/etc/ssl
    depends_on:
      - nextjs
    restart: unless-stopped

volumes:
  postgres_data:
bash
# 构建和启动
docker-compose up -d

# 查看日志
docker-compose logs -f nextjs

# 停止
docker-compose down

# 重新构建
docker-compose up -d --build

4.3 多阶段构建优化

dockerfile
# 优化的 Dockerfile
FROM node:20-alpine AS base

FROM base AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app

# 只复制依赖文件
COPY package.json pnpm-lock.yaml* ./
RUN corepack enable pnpm && pnpm install --frozen-lockfile --prod

FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .

ENV NEXT_TELEMETRY_DISABLED 1

# 构建优化
RUN corepack enable pnpm && \
    pnpm build && \
    rm -rf .next/cache

FROM base AS runner
WORKDIR /app

ENV NODE_ENV production
ENV NEXT_TELEMETRY_DISABLED 1

RUN addgroup --system --gid 1001 nodejs && \
    adduser --system --uid 1001 nextjs

# 只复制必要文件
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static

USER nextjs

EXPOSE 3000

ENV PORT 3000
ENV HOSTNAME "0.0.0.0"

# 健康检查
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
  CMD node -e "require('http').get('http://localhost:3000/api/health', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})"

CMD ["node", "server.js"]

五、性能优化

5.1 Bundle 分析

bash
# 安装分析工具
npm install @next/bundle-analyzer

# next.config.js
const withBundleAnalyzer = require('@next/bundle-analyzer')({
  enabled: process.env.ANALYZE === 'true',
})

module.exports = withBundleAnalyzer({
  // Next.js 配置
})

# 运行分析
ANALYZE=true npm run build

5.2 代码分割

typescript
// 动态导入组件
import dynamic from 'next/dynamic'

// 不 SSR 的组件
const HeavyComponent = dynamic(() => import('@/components/HeavyComponent'), {
  ssr: false,
  loading: () => <div>Loading...</div>
})

// 按需加载
const DynamicComponent = dynamic(() => import('@/components/DynamicComponent'))

export default function Page() {
  return (
    <div>
      <DynamicComponent />
      <HeavyComponent />
    </div>
  )
}

// 命名导出
const SpecificComponent = dynamic(
  () => import('@/components/Module').then(mod => mod.SpecificComponent)
)

5.3 缓存策略

typescript
// app/api/data/route.ts
export async function GET() {
  const data = await fetchData()
  
  return Response.json(data, {
    headers: {
      'Cache-Control': 'public, s-maxage=3600, stale-while-revalidate=86400'
    }
  })
}

// 页面级别缓存
// app/blog/page.tsx
export const revalidate = 3600 // 1小时

// 请求级别缓存
export default async function Page() {
  const data = await fetch('https://api.example.com/data', {
    next: { revalidate: 3600 }
  })
  
  return <div>{/* 内容 */}</div>
}

5.4 图片优化

typescript
import Image from 'next/image'

export default function Page() {
  return (
    <div>
      {/* 优化的图片 */}
      <Image
        src="/hero.jpg"
        alt="Hero"
        width={1200}
        height={600}
        priority // 首屏图片
        quality={90}
        sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
      />
      
      {/* 懒加载图片 */}
      <Image
        src="/photo.jpg"
        alt="Photo"
        width={800}
        height={600}
        loading="lazy"
      />
    </div>
  )
}

5.5 字体优化

typescript
// app/layout.tsx
import { Inter } from 'next/font/google'

const inter = Inter({
  subsets: ['latin'],
  display: 'swap',
  preload: true,
})

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en" className={inter.className}>
      <body>{children}</body>
    </html>
  )
}

六、监控与分析

6.1 Vercel Analytics

typescript
// app/layout.tsx
import { Analytics } from '@vercel/analytics/react'

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        {children}
        <Analytics />
      </body>
    </html>
  )
}

6.2 Web Vitals

typescript
// app/layout.tsx
'use client'

import { useReportWebVitals } from 'next/web-vitals'

export function WebVitals() {
  useReportWebVitals((metric) => {
    console.log(metric)
    // 发送到分析服务
    // analytics.track(metric.name, metric.value)
  })
  
  return null
}

// 使用
export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        <WebVitals />
        {children}
      </body>
    </html>
  )
}

6.3 错误追踪 (Sentry)

bash
npm install @sentry/nextjs
javascript
// sentry.client.config.js
import * as Sentry from '@sentry/nextjs'

Sentry.init({
  dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
  tracesSampleRate: 1.0,
  environment: process.env.NODE_ENV,
})

// sentry.server.config.js
import * as Sentry from '@sentry/nextjs'

Sentry.init({
  dsn: process.env.SENTRY_DSN,
  tracesSampleRate: 1.0,
  environment: process.env.NODE_ENV,
})

// next.config.js
const { withSentryConfig } = require('@sentry/nextjs')

module.exports = withSentryConfig(
  {
    // Next.js 配置
  },
  {
    silent: true,
    org: 'your-org',
    project: 'your-project',
  }
)

6.4 自定义监控

typescript
// lib/monitoring.ts
export class PerformanceMonitor {
  static measurePageLoad() {
    if (typeof window === 'undefined') return
    
    window.addEventListener('load', () => {
      const navigation = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming
      
      const metrics = {
        ttfb: navigation.responseStart - navigation.requestStart,
        domLoaded: navigation.domContentLoadedEventEnd - navigation.domContentLoadedEventStart,
        pageLoad: navigation.loadEventEnd - navigation.loadEventStart,
      }
      
      console.log('Performance Metrics:', metrics)
      // 发送到监控服务
    })
  }
  
  static trackError(error: Error) {
    console.error('Error:', error)
    // 发送到错误追踪服务
  }
}

// app/layout.tsx
'use client'

import { useEffect } from 'react'
import { PerformanceMonitor } from '@/lib/monitoring'

export function Monitoring() {
  useEffect(() => {
    PerformanceMonitor.measurePageLoad()
  }, [])
  
  return null
}

七、安全最佳实践

7.1 环境变量安全

typescript
// ✓ 正确 - 使用环境变量
const apiKey = process.env.API_KEY

// ✗ 错误 - 硬编码密钥
const apiKey = 'hardcoded-api-key'

// ✓ 正确 - 服务器端密钥
// .env.local
DATABASE_URL=postgresql://...
API_SECRET=...

// ✗ 错误 - 暴露给客户端
NEXT_PUBLIC_API_SECRET=... // 不要这样做!

7.2 HTTP 安全头

typescript
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  async headers() {
    return [
      {
        source: '/(.*)',
        headers: [
          {
            key: 'X-DNS-Prefetch-Control',
            value: 'on'
          },
          {
            key: 'Strict-Transport-Security',
            value: 'max-age=63072000; includeSubDomains; preload'
          },
          {
            key: 'X-Frame-Options',
            value: 'SAMEORIGIN'
          },
          {
            key: 'X-Content-Type-Options',
            value: 'nosniff'
          },
          {
            key: 'X-XSS-Protection',
            value: '1; mode=block'
          },
          {
            key: 'Referrer-Policy',
            value: 'origin-when-cross-origin'
          },
          {
            key: 'Permissions-Policy',
            value: 'camera=(), microphone=(), geolocation=()'
          }
        ]
      }
    ]
  }
}

module.exports = nextConfig

7.3 CSRF 保护

typescript
// lib/csrf.ts
import { randomBytes } from 'crypto'

export function generateCSRFToken(): string {
  return randomBytes(32).toString('hex')
}

export function verifyCSRFToken(token: string, sessionToken: string): boolean {
  return token === sessionToken
}

// app/api/form/route.ts
import { cookies } from 'next/headers'
import { verifyCSRFToken } from '@/lib/csrf'

export async function POST(request: Request) {
  const cookieStore = await cookies()
  const sessionToken = cookieStore.get('csrf_token')?.value
  const formData = await request.formData()
  const token = formData.get('csrf_token') as string
  
  if (!sessionToken || !verifyCSRFToken(token, sessionToken)) {
    return Response.json({ error: 'Invalid CSRF token' }, { status: 403 })
  }
  
  // 处理表单
  return Response.json({ success: true })
}

7.4 输入验证

typescript
// lib/validation.ts
import { z } from 'zod'

export const userSchema = z.object({
  email: z.string().email(),
  name: z.string().min(2).max(50),
  age: z.number().min(18).max(120),
})

// app/api/users/route.ts
import { userSchema } from '@/lib/validation'

export async function POST(request: Request) {
  try {
    const body = await request.json()
    const data = userSchema.parse(body)
    
    // 安全地使用验证后的数据
    // ...
    
    return Response.json({ success: true })
  } catch (error) {
    return Response.json({ error: 'Invalid data' }, { status: 400 })
  }
}

八、CI/CD 流程

8.1 GitHub Actions

yaml
# .github/workflows/ci.yml
name: CI/CD

on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main ]

jobs:
  build:
    runs-on: ubuntu-latest
    
    steps:
    - uses: actions/checkout@v3
    
    - name: Setup Node.js
      uses: actions/setup-node@v3
      with:
        node-version: '20'
        cache: 'pnpm'
    
    - name: Install pnpm
      uses: pnpm/action-setup@v2
      with:
        version: 8
    
    - name: Install dependencies
      run: pnpm install --frozen-lockfile
    
    - name: Lint
      run: pnpm lint
    
    - name: Type check
      run: pnpm tsc --noEmit
    
    - name: Test
      run: pnpm test
    
    - name: Build
      run: pnpm build
      env:
        DATABASE_URL: ${{ secrets.DATABASE_URL }}
    
    - name: Deploy to Vercel
      if: github.ref == 'refs/heads/main'
      uses: amondnet/vercel-action@v20
      with:
        vercel-token: ${{ secrets.VERCEL_TOKEN }}
        vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}
        vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}
        vercel-args: '--prod'

8.2 预览部署

yaml
# .github/workflows/preview.yml
name: Preview Deployment

on:
  pull_request:
    types: [opened, synchronize]

jobs:
  deploy-preview:
    runs-on: ubuntu-latest
    
    steps:
    - uses: actions/checkout@v3
    
    - name: Deploy to Vercel Preview
      uses: amondnet/vercel-action@v20
      with:
        vercel-token: ${{ secrets.VERCEL_TOKEN }}
        vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}
        vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}
      
    - name: Comment PR
      uses: actions/github-script@v6
      with:
        script: |
          github.rest.issues.createComment({
            issue_number: context.issue.number,
            owner: context.repo.owner,
            repo: context.repo.name,
            body: '✅ Preview deployed: https://preview-url.vercel.app'
          })

九、最佳实践总结

9.1 部署检查清单

typescript
// ✓ 构建成功
// ✓ 测试通过
// ✓ 环境变量配置
// ✓ 数据库迁移
// ✓ 缓存配置
// ✓ CDN 配置
// ✓ SSL 证书
// ✓ 域名解析
// ✓ 监控配置
// ✓ 错误追踪
// ✓ 备份策略
// ✓ 回滚计划

9.2 性能优化检查

typescript
// ✓ 图片优化
// ✓ 字体优化
// ✓ 代码分割
// ✓ Bundle 大小
// ✓ 缓存策略
// ✓ SSR/SSG/ISR
// ✓ Web Vitals
// ✓ Lighthouse 分数

9.3 学习资源

  1. 官方文档

  2. 工具

    • Vercel
    • Docker
    • PM2
    • Nginx

课后练习

  1. 部署应用到 Vercel
  2. 配置自托管部署
  3. 创建 Docker 镜像
  4. 实现 CI/CD 流程
  5. 优化应用性能

通过本课程的学习,你应该能够自信地部署和优化 Next.js 应用,确保生产环境的高可用性和优异性能!