Next.js: 综合项目:部署与 CI/CD

最后更新:2026-08-26

从代码提交到用户浏览器,中间隔着部署、CI/CD、监控、性能优化——这是让你的应用真正"上线"的最后一步。

1. 你将学到


2. 一个 DevOps 工程师的真实故事

(1) 痛点:每次上线都是"踩地雷"

Diana 是 Acme Corp 的 DevOps 工程师,负责 TaskFlow 的部署运维。团队每月上线 20 次,但每次部署都心惊胆战:手动 SSH 上传代码漏了环境变量(导致生产数据库连不上)、npm run build 本地 OK 但服务器 OOM(内存不足)、上线后 2 小时才发现用户报错(没有监控告警)、Lighthouse 评分从 85 掉到 62 没人察觉。最近一次上线导致 30 分钟宕机,客服收到 200+ 投诉。

(2) 自动化 CI/CD + 双方案部署 + 监控的解法

用 GitHub Actions 自动化质量关卡 + Vercel 零配置部署 + Docker 自托管备选 + PostHog/Sentry 全方位监控。

YAML
# 一行 GitHub Actions 触发全自动部署
name: Deploy
on:
  push:
    branches: [main]
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci && npm run build
      - uses: amondnet/vercel-action@v20

(3) 收益

维度 手动部署 CI/CD 自动化
部署时间 45 分钟(含人工检查) 8 分钟全自动
宕机风险 高(漏文件/错配置) 低(一致性保证)
问题发现 2 小时后用户投诉 实时 Sentry 告警
Lighthouse 评分 每月人工跑一次 PR 自动检测
回滚速度 15 分钟手动还原 1 分钟 Vercel Rollback
团队效率 DevOps 瓶颈 开发自助部署

3. Vercel 部署

(1) Vercel 架构

100%
graph TB
    subgraph "Vercel Edge Network"
        A[Global CDN] --> B[Edge Functions]
        B --> C[Serverless Functions]
        C --> D[Vercel Postgres]
    end

    subgraph "GitHub"
        E[Source Code] -->|Push| F[GitHub Actions]
        F -->|Auto Deploy| A
    end

    subgraph "Monitoring"
        G[Vercel Analytics]
        H[Sentry]
        I[PostHog]
    end

    C --> G
    C --> H
    B --> I

    style A fill:#cce5ff
    style E fill:#d4edda
    style F fill:#ffeeba

▶ 示例:vercel.json 配置

JSON
{
  "framework": "nextjs",
  "buildCommand": "npm run build",
  "outputDirectory": ".next",
  "installCommand": "npm install",
  "regions": ["iad1", "hkg1", "gru1"],
  "env": {
    "NEXT_PUBLIC_APP_URL": "https://taskflow.vercel.app"
  },
  "crons": [
    {
      "path": "/api/cron/daily-digest",
      "schedule": "0 8 * * *"
    }
  ],
  "headers": [
    {
      "source": "/(.*)",
      "headers": [
        {
          "key": "X-Content-Type-Options",
          "value": "nosniff"
        },
        {
          "key": "X-Frame-Options",
          "value": "DENY"
        },
        {
          "key": "X-XSS-Protection",
          "value": "1; mode=block"
        },
        {
          "key": "Referrer-Policy",
          "value": "strict-origin-when-cross-origin"
        }
      ]
    },
    {
      "source": "/uploads/(.*)",
      "headers": [
        {
          "key": "Cache-Control",
          "value": "public, max-age=31536000, immutable"
        }
      ]
    }
  ],
  "redirects": [
    {
      "source": "/app",
      "destination": "/dashboard",
      "permanent": true
    }
  ]
}

▶ 示例:Vercel CLI 部署

BASH
# 1. 安装 Vercel CLI
npm install -g vercel

# 2. 登录 Vercel
vercel login

# 3. 关联项目
vercel link

# 4. 设置环境变量
vercel env add DATABASE_URL
vercel env add AUTH_SECRET
vercel env add GOOGLE_CLIENT_ID
vercel env add GOOGLE_CLIENT_SECRET
vercel env add AUTH_URL
vercel env add NEXT_PUBLIC_POSTHOG_KEY
vercel env add SENTRY_DSN

# 5. 部署 Preview
vercel

# 6. 部署 Production
vercel --prod

# 7. 查看部署日志
vercel logs
TEXT 📖 仅展示
Vercel CLI 28.0.0
? Set up and deploy "C:\Users\admin\taskflow"? [Y/n] Y
? Which scope? Acme Corp
? Link to existing project? No
? What's your project's name? taskflow
? In which directory is your code? ./
Auto-detected Project Settings (Next.js):
- Build Command: npx next build
- Output Directory: .next
- Node.js Version: 20.x
✅ Production: https://taskflow.vercel.app [7s]

▶ 示例:连接 Vercel Postgres

BASH
# 1. 创建 Postgres 数据库
vercel env add DATABASE_URL

# 2. 安装 Vercel Postgres SDK
npm install @vercel/postgres

# 3. 更新 Prisma 数据源
PRISMA
// prisma/schema.prisma
datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
  // Vercel Postgres 自动处理连接池
  // 在 Vercel 上启用 shadowDatabaseUrl 避免迁移冲突
}
TYPESCRIPT
// src/lib/db.ts
import { PrismaClient } from "@prisma/client"

const globalForPrisma = globalThis as unknown as {
  prisma: PrismaClient | undefined
}

export const prisma = globalForPrisma.prisma ?? new PrismaClient()

if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma

4. Docker 自托管

(1) 自托管架构

100%
graph TB
    subgraph "Docker Host"
        A[Nginx<br/>Reverse Proxy] --> B[Node.js App<br/>(PM2)]
        B --> C[PostgreSQL<br/>(Container)]
    end

    subgraph "External"
        D[DNS: taskflow.example.com]
    end

    D --> A
    A -->|SSL Termination| B

    style A fill:#d4edda
    style B fill:#cce5ff
    style C fill:#f8d7da

▶ 示例:Dockerfile 多阶段构建

DOCKERFILE
# ============================================
# Stage 1: Dependencies
# ============================================
FROM node:20-alpine AS deps
RUN apk add --no-cache libc6-compat

WORKDIR /app

COPY package.json package-lock.json ./
RUN npm ci --only=production --ignore-scripts
RUN npm ci --only=development --ignore-scripts

# ============================================
# Stage 2: Build
# ============================================
FROM node:20-alpine AS builder
WORKDIR /app

COPY --from=deps /app/node_modules ./node_modules
COPY . .

ENV NEXT_TELEMETRY_DISABLED=1
ENV NODE_ENV=production
ENV NEXT_OUTPUT=standalone

RUN npx prisma generate
RUN npm run build

# ============================================
# Stage 3: Production Runner
# ============================================
FROM node:20-alpine 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
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
COPY --from=builder /app/prisma ./prisma
COPY --from=builder /app/node_modules/.prisma ./node_modules/.prisma

USER nextjs

EXPOSE 3000

ENV PORT=3000
ENV HOSTNAME="0.0.0.0"

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

▶ 示例:docker-compose.yml

YAML
version: "3.8"

services:
  app:
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - "3000:3000"
    environment:
      - DATABASE_URL=postgresql://taskflow:password@db:5432/taskflow
      - AUTH_SECRET=${AUTH_SECRET}
      - AUTH_URL=https://taskflow.example.com
      - GOOGLE_CLIENT_ID=${GOOGLE_CLIENT_ID}
      - GOOGLE_CLIENT_SECRET=${GOOGLE_CLIENT_SECRET}
      - NEXTAUTH_URL=https://taskflow.example.com
      - NODE_ENV=production
    depends_on:
      db:
        condition: service_healthy
    restart: unless-stopped
    networks:
      - taskflow-network
    healthcheck:
      test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3000/api/health"]
      interval: 30s
      timeout: 10s
      retries: 3

  db:
    image: postgres:16-alpine
    volumes:
      - postgres-data:/var/lib/postgresql/data
    environment:
      - POSTGRES_DB=taskflow
      - POSTGRES_USER=taskflow
      - POSTGRES_PASSWORD=${DB_PASSWORD}
    ports:
      - "5432:5432"
    restart: unless-stopped
    networks:
      - taskflow-network
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U taskflow"]
      interval: 10s
      timeout: 5s
      retries: 5

  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
      - ./ssl:/etc/nginx/ssl:ro
    depends_on:
      - app
    restart: unless-stopped
    networks:
      - taskflow-network

volumes:
  postgres-data:

networks:
  taskflow-network:
    driver: bridge

▶ 示例:Nginx 配置

NGINX
# nginx.conf
events {
  worker_connections 1024;
}

http {
  upstream taskflow_app {
    server app:3000;
  }

  server {
    listen 80;
    server_name taskflow.example.com;
    return 301 https://$server_name$request_uri;
  }

  server {
    listen 443 ssl http2;
    server_name taskflow.example.com;

    ssl_certificate /etc/nginx/ssl/cert.pem;
    ssl_certificate_key /etc/nginx/ssl/key.pem;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!aNULL:!MD5;

    # Security headers
    add_header X-Frame-Options "DENY" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-XSS-Protection "1; mode=block" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;

    # Gzip compression
    gzip on;
    gzip_types text/plain text/css application/json application/javascript text/xml application/xml text/javascript image/svg+xml;
    gzip_min_length 1000;
    gzip_comp_level 6;

    # Static assets (cache forever)
    location /_next/static {
      proxy_pass http://taskflow_app;
      proxy_set_header Host $host;
      proxy_set_header X-Real-IP $remote_addr;
      expires 365d;
      add_header Cache-Control "public, immutable";
    }

    # Uploads
    location /uploads {
      proxy_pass http://taskflow_app;
      proxy_set_header Host $host;
      expires 30d;
      add_header Cache-Control "public, immutable";
    }

    # API routes (no cache)
    location /api {
      proxy_pass http://taskflow_app;
      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_no_cache 1;
      proxy_cache_bypass 1;
    }

    # Everything else
    location / {
      proxy_pass http://taskflow_app;
      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;

      # WebSocket support
      proxy_http_version 1.1;
      proxy_set_header Upgrade $http_upgrade;
      proxy_set_header Connection "upgrade";
    }
  }
}

5. GitHub Actions CI/CD

(1) 流水线架构

100%
graph LR
    A[Push to main] --> B[Lint]
    B --> C[Type Check]
    C --> D[Unit Test]
    D --> E[Build]
    E --> F[Deploy to Vercel]
    F --> G[Lighthouse CI]
    G --> H{Score >= 90?}
    H -->|Yes| I[Success]
    H -->|No| J[Rollback]

    style A fill:#cce5ff
    style F fill:#d4edda
    style G fill:#ffeeba
    style J fill:#f8d7da

▶ 示例:CI/CD 工作流

YAML
# .github/workflows/ci-cd.yml
name: CI/CD Pipeline

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

env:
  NODE_VERSION: "20"

jobs:
  quality:
    name: Quality Checks
    runs-on: ubuntu-latest

    services:
      postgres:
        image: postgres:16-alpine
        env:
          POSTGRES_USER: test
          POSTGRES_PASSWORD: test
          POSTGRES_DB: taskflow_test
        ports:
          - 5432:5432
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5

    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: "npm"

      - name: Install dependencies
        run: npm ci

      - name: Generate Prisma Client
        run: npx prisma generate

      - name: Run database migrations
        run: npx prisma migrate deploy
        env:
          DATABASE_URL: postgresql://test:test@localhost:5432/taskflow_test

      - name: Lint
        run: npm run lint

      - name: Type check
        run: npx tsc --noEmit

      - name: Unit tests
        run: npm run test -- --coverage
        env:
          DATABASE_URL: postgresql://test:test@localhost:5432/taskflow_test
          AUTH_SECRET: test-secret
          AUTH_URL: http://localhost:3000

      - name: Upload coverage
        uses: actions/upload-artifact@v4
        with:
          name: coverage
          path: coverage/

  build:
    name: Build
    needs: quality
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: "npm"

      - name: Install dependencies
        run: npm ci

      - name: Generate Prisma Client
        run: npx prisma generate

      - name: Build
        run: npm run build
        env:
          DATABASE_URL: postgresql://test:test@localhost:5432/taskflow_test
          AUTH_SECRET: build-secret
          AUTH_URL: http://localhost:3000
          NEXT_PUBLIC_POSTHOG_KEY: ${{ secrets.NEXT_PUBLIC_POSTHOG_KEY }}
          SENTRY_DSN: ${{ secrets.SENTRY_DSN }}

      - name: Upload build artifact
        uses: actions/upload-artifact@v4
        with:
          name: build
          path: .next/

  deploy:
    name: Deploy
    needs: build
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Deploy to Vercel
        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"
          working-directory: ./

  lighthouse:
    name: Lighthouse CI
    needs: deploy
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Audit URLs with Lighthouse
        uses: treosh/lighthouse-ci-action@v10
        with:
          urls: |
            https://taskflow.vercel.app
            https://taskflow.vercel.app/dashboard
            https://taskflow.vercel.app/dashboard/projects
          budgetPath: ./lighthouse-budget.json
          uploadArtifacts: true
          temporaryPublicStorage: true
        env:
          LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_GITHUB_APP_TOKEN }}

      - name: Check Lighthouse Scores
        run: |
          echo "Lighthouse audit complete"
          echo "View results: https://taskflow.vercel.app/lighthouse-report"

▶ 示例:Lighthouse Budget 配置

JSON
{
  "ci": {
    "collect": {
      "numberOfRuns": 3,
      "settings": {
        "preset": "desktop",
        "throttlingMethod": "simulate"
      }
    },
    "assert": {
      "assertions": {
        "categories:performance": ["error", { "minScore": 0.9 }],
        "categories:accessibility": ["error", { "minScore": 0.9 }],
        "categories:best-practices": ["error", { "minScore": 0.9 }],
        "categories:seo": ["error", { "minScore": 0.9 }],
        "categories:pwa": ["warn", { "minScore": 0.5 }],
        "first-contentful-paint": ["error", { "maxNumericValue": 2000 }],
        "largest-contentful-paint": ["error", { "maxNumericValue": 2500 }],
        "cumulative-layout-shift": ["error", { "maxNumericValue": 0.1 }],
        "total-blocking-time": ["error", { "maxNumericValue": 300 }],
        "interactive": ["error", { "maxNumericValue": 3500 }]
      }
    },
    "upload": {
      "target": "temporary-public-storage"
    }
  }
}

6. 生产监控

(1) 监控体系架构

100%
graph TB
    subgraph "User Metrics"
        A["PostHog<br/>Events & Funnels"]
        B["Vercel Speed Insights<br/>Real User Monitoring"]
    end

    subgraph "Error Tracking"
        C["Sentry<br/>Errors & Performance"]
    end

    subgraph "Performance"
        D["Lighthouse CI<br/>Score Trends"]
        E["Bundle Analyzer<br/>Bundle Size"]
    end

    subgraph "Alerts"
        F["Sentry Alerts<br/>PagerDuty"]
        G["GitHub Checks<br/>PR Comment"]
    end

    A --> F
    B --> D
    C --> F
    D --> G
    E --> G

    style A fill:#d4edda
    style B fill:#cce5ff
    style C fill:#f8d7da

▶ 示例:PostHog 事件追踪

TYPESCRIPT
// src/lib/posthog.ts
import { PostHog } from "posthog-node"

export function initPostHog() {
  if (!process.env.NEXT_PUBLIC_POSTHOG_KEY) return null

  return new PostHog(process.env.NEXT_PUBLIC_POSTHOG_KEY, {
    host: process.env.POSTHOG_HOST || "https://app.posthog.com",
  })
}

export async function trackEvent(
  userId: string,
  event: string,
  properties?: Record<string, unknown>
) {
  const client = initPostHog()
  if (!client) return

  client.capture({
    distinctId: userId,
    event,
    properties: {
      ...properties,
      app: "taskflow",
      environment: process.env.NODE_ENV,
    },
  })

  await client.shutdown()
}
TSX
// src/components/providers/posthog-provider.tsx
"use client"

import { usePathname, useSearchParams } from "next/navigation"
import { useEffect } from "react"
import posthog from "posthog-js"
import { PostHogProvider as PHProvider } from "posthog-js/react"

if (typeof window !== "undefined" && process.env.NEXT_PUBLIC_POSTHOG_KEY) {
  posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY, {
    api_host: process.env.NEXT_PUBLIC_POSTHOG_HOST || "https://app.posthog.com",
    capture_pageview: false,
    loaded: (ph) => {
      if (process.env.NODE_ENV !== "production") {
        ph.opt_out_capturing()
      }
    },
  })
}

export function PostHogProvider({ children }: { children: React.ReactNode }) {
  return <PHProvider client={posthog}>{children}</PHProvider>
}

export function PostHogPageView() {
  const pathname = usePathname()
  const searchParams = useSearchParams()

  useEffect(() => {
    if (pathname) {
      let url = window.origin + pathname
      if (searchParams?.toString()) {
        url += `?${searchParams.toString()}`
      }
      posthog.capture("$pageview", { $current_url: url })
    }
  }, [pathname, searchParams])

  return null
}

▶ 示例:Sentry 错误追踪

TYPESCRIPT
// src/lib/sentry.ts
import * as Sentry from "@sentry/nextjs"

Sentry.init({
  dsn: process.env.SENTRY_DSN,
  environment: process.env.NODE_ENV,
  tracesSampleRate: process.env.NODE_ENV === "production" ? 0.1 : 1.0,
  replaysSessionSampleRate: 0.1,
  replaysOnErrorSampleRate: 1.0,
  integrations: [
    Sentry.replayIntegration(),
    Sentry.feedbackIntegration({
      colorScheme: "system",
      isEmailRequired: true,
    }),
  ],
})
TYPESCRIPT
// src/instrumentation.ts
export async function register() {
  if (process.env.NEXT_RUNTIME === "nodejs") {
    await import("./lib/sentry")
  }
}

export async function onRequestError(err: unknown, request: unknown, context: unknown) {
  if (process.env.NEXT_RUNTIME === "nodejs") {
    const Sentry = await import("@sentry/nextjs")
    Sentry.captureException(err, { extra: { request, context } })
  }
}

▶ 示例:Vercel Speed Insights

TSX
// src/app/layout.tsx 中添加
import { SpeedInsights } from "@vercel/speed-insights/next"
import { Analytics } from "@vercel/analytics/react"

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

7. 性能优化

(1) 优化策略对比

策略 影响指标 优化前 优化后 工具
next/image lazy LCP 3.2s 1.5s <Image loading="lazy">
Bundle 分割 TTI 4.5s 2.8s @next/bundle-analyzer
字体优化 CLS 0.25 0.02 next/font display:swap
PPR 静态壳 FCP 2.1s 0.4s experimental.ppr
图片 CDN LCP 2.8s 1.2s Vercel Image Optimization
缓存策略 TTFB 800ms 120ms revalidateTag + CDN

▶ 示例:next/image 懒加载

TSX
// src/components/tasks/task-attachment-image.tsx
import Image from "next/image"

interface TaskAttachmentImageProps {
  src: string
  alt: string
  priority?: boolean
}

export function TaskAttachmentImage({
  src,
  alt,
  priority = false,
}: TaskAttachmentImageProps) {
  return (
    <div className="relative aspect-video overflow-hidden rounded-lg">
      <Image
        src={src}
        alt={alt}
        fill
        sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
        className="object-cover"
        loading={priority ? "eager" : "lazy"}
        priority={priority}
        placeholder="blur"
        blurDataURL="data:image/webp;base64,UklGRi4AAABXRUJQVlA4ICIAAABQAQCdASoIAAgAAwA/JZgC7AAAvAADe4AAf/AAf/A"
      />
    </div>
  )
}

▶ 示例:Bundle Analyzer

TYPESCRIPT
// next.config.ts
import type { NextConfig } from "next"

const withBundleAnalyzer = process.env.ANALYZE === "true"
  ? (await import("@next/bundle-analyzer")).default
  : (config: NextConfig) => config

const nextConfig: NextConfig = withBundleAnalyzer({
  experimental: {
    ppr: true,
  },
  images: {
    formats: ["image/avif", "image/webp"],
    deviceSizes: [640, 750, 828, 1080, 1200, 1920],
    remotePatterns: [
      {
        protocol: "https",
        hostname: "api.dicebear.com",
      },
      {
        protocol: "https",
        hostname: "utfs.io",
      },
    ],
  },
  headers: async () => [
    {
      source: "/:path*",
      headers: [
        { key: "X-DNS-Prefetch-Control", value: "on" },
        { key: "Strict-Transport-Security", value: "max-age=63072000; includeSubDomains; preload" },
      ],
    },
  ],
})

export default nextConfig
BASH
# 在本地运行 Bundle Analyzer
ANALYZE=true npm run build
TEXT 📖 仅展示
Bundle Analysis:
- page (dashboard): 128 kB (initial) / 48 kB (shared)
- page (dashboard/projects/[id]): 145 kB (initial) / 52 kB (shared)
- page (dashboard/tasks): 156 kB (initial) / 62 kB (shared)
- page (auth/signin): 64 kB (initial) / 28 kB (shared)
- Shared modules: @nextui-org/react (12 kB), recharts (18 kB), date-fns (6 kB)
- Total initial JS: 246 kB (target: < 300 kB) ✅

8. 完整示例:生产环境部署与验证

BASH
# ============================================
# TaskFlow 生产部署完整流程
# ============================================

# --- 方案 A: Vercel 部署 ---

# 1. 构建前检查
npm run lint && npx tsc --noEmit && npm run test -- --run

# 2. 构建验证
npm run build
# 输出: ✓ Compiled successfully in 12.8s

# 3. 部署 Preview
vercel
# 输出: https://taskflow-git-feature-abc123.vercel.app

# 4. 运行 E2E 测试
npx playwright test --url=https://taskflow-git-feature-abc123.vercel.app

# 5. Lighthouse 检查
npx lhci autorun

# 6. 部署 Production
vercel --prod
# 输出: https://taskflow.vercel.app (Production)

# 7. 配置自定义域名
vercel domains add taskflow.example.com
# 输出: ✓ Domain taskflow.example.com added

# 8. 验证
curl -I https://taskflow.example.com/api/health
# 输出: HTTP/2 200

# --- 方案 B: Docker 自托管 ---

# 1. 构建 Docker 镜像
docker build -t taskflow:latest .

# 2. 启动服务
docker compose up -d

# 3. 运行数据库迁移
docker compose exec app npx prisma migrate deploy

# 4. 运行 Seeding
docker compose exec app npx prisma db seed

# 5. 验证
curl http://localhost/api/health
# 输出: {"status":"healthy","timestamp":"2026-07-06T10:00:00Z"}

# 6. 查看日志
docker compose logs -f app

# 7. 监控指标
# - PostHog: https://app.posthog.com/project/taskflow
# - Sentry: https://sentry.io/organizations/acme/projects/taskflow
# - Vercel Dashboard: https://vercel.com/acme/taskflow

# --- 性能验证 ---

# Lighthouse 报告
# Performance: 94/100 ✅
# Accessibility: 97/100 ✅
# Best Practices: 93/100 ✅
# SEO: 100/100 ✅

# Core Web Vitals (RUM)
# LCP: 1.2s ✅ (目标 < 2.5s)
# FID: 12ms ✅ (目标 < 100ms)
# CLS: 0.02 ✅ (目标 < 0.1)

9. 学习路径回顾与进阶

(1) Next.js 16 全栈学习路线图

100%
graph TB
    subgraph "Phase 1: 基础 (6 lessons)"
        A1["01-02: 简介 + 环境"]
        A2["03-04: 路由 + 布局"]
        A3["05-06: 导航 + Parallel Routes"]
    end

    subgraph "Phase 2: 数据 (7 lessons)"
        B1["07-08: RSC + 数据获取"]
        B2["09-10: SSG/ISR/PPR"]
        B3["11-12: Cache + Server Actions"]
        B4["13-14: Actions 进阶 + API Routes"]
    end

    subgraph "Phase 3: 进阶 (6 lessons)"
        C1["15: 认证 Auth.js"]
        C2["16: 数据库 Prisma"]
        C3["17: Streaming"]
        C4["18: 图片/字体"]
        C5["19: SEO/Metadata"]
        C6["20: i18n/中间件"]
    end

    subgraph "Phase 4: 生产 (6 lessons)"
        D1["21-22: 单元 + E2E 测试"]
        D2["23-24: CI/CD + Docker"]
        D3["25-26: 性能 + 安全/迁移"]
    end

    subgraph "Phase 5: 综合项目 (4 lessons)"
        E1["27: 初始化 + 认证"]
        E2["28: Dashboard + PPR"]
        E3["29: CRUD + Server Actions"]
        E4["30: 部署 + CI/CD + 监控"]
    end

    A1 --> A2 --> A3 --> B1
    B1 --> B2 --> B3 --> B4 --> C1
    C1 --> C2 --> C3 --> C4 --> C5 --> C6 --> D1
    D1 --> D2 --> D3 --> E1
    E1 --> E2 --> E3 --> E4

    style A1 fill:#d4edda
    style E4 fill:#cce5ff

(2) 能力清单

能力 对应课程 掌握程度
App Router 文件系统路由 #03-#06 精通
Server Components 思维 #07 精通
数据获取与缓存 #08-#11 精通
Server Actions CRUD #12-#13, #29 精通
认证与授权 #15, #27 精通
Prisma ORM 数据库 #16, #27 精通
测试(Vitest + Playwright) #21-#22 熟练
CI/CD 流水线 #23, #30 熟练
Vercel + Docker 双方案部署 #24, #30 熟练
性能监控与优化 #25, #30 熟练
安全与迁移 #26 了解
PPR + Cache Components #10-#11 精通(独家)

(3) 进阶方向

方向 学习内容 推荐资源
AI 集成 Vercel AI SDK + RAG + Streaming vercel/ai (10k⭐)
微前端 Module Federation + Turborepo Nx / Turborepo 官方
GraphQL Apollo + Next.js + Codegen Apollo Client 文档
实时应用 WebSocket + Socket.io + Liveblocks Liveblocks Next.js 集成
Edge Computing Edge Functions + Durable Objects Cloudflare Workers
平台工程 Backstage + Internal Developer Portal Spotify Backstage

❓ 常见问题

Q Vercel 和 Docker 自托管应该选哪个?
A Vercel 适合 90% 的团队——零配置、全球 CDN、自动 HTTPS、Preview 部署。Docker 自托管仅在以下场景需要:合规要求(数据必须留在本地)、内部系统(无需公网)、超大规模(需要自定义基础设施)。TaskFlow 推荐 Vercel 主方案 + Docker 备选。
Q GitHub Actions 的 Lighthouse CI 分数阈值设多少合适?
A 建议 Performance ≥ 90、Accessibility ≥ 90、Best Practices ≥ 90、SEO ≥ 90。如果项目有特殊需求(如大量第三方脚本导致 Performance 降低),可降至 80,但应加 TODO 注释说明原因并计划修复。
Q PostHog 和 Sentry 的功能会不会重复?
A 不会。PostHog 侧重用户行为分析(页面浏览、按钮点击、转化漏斗),Sentry 侧重错误追踪(Crash 堆栈、性能 Span、Release 健康)。两者互补:PostHog 告诉你"用户做了什么",Sentry 告诉你"为什么出错了"。
Q Vercel Postgres 和自建 PostgreSQL 有什么区别?
A Vercel Postgres 是托管服务——自动备份、自动扩容、VPC 网络低延迟、按使用付费。自建 PostgreSQL 完全控制但需要运维(备份、监控、升级)。TaskFlow 开发时用本地 Postgres,Vercel 部署用 Vercel Postgres,Docker 自托管用容器化 Postgres。
Q Docker 多阶段构建为什么要分三个阶段?
A 阶段 1 (deps) 安装全部依赖;阶段 2 (builder) 只使用开发依赖构建;阶段 3 (runner) 仅复制产物和运行时依赖。最终镜像从 1.2GB 压缩到 358MB,减少 70%。生产容器不包含 TypeScript 编译器、测试工具、node_modules 的开发依赖包。
Q Standalone 模式(output: 'standalone')有什么作用?
A @latest/next 的独立模式只输出运行所需的最小文件集(server.js + .next 部分目录),不含 node_modules 中不需要的包。配合 Docker 多阶段构建,最终镜像仅包含 Next.js 运行时,大幅减少体积和攻击面。
Q TaskFlow 项目学完后,如何继续提升?
A 建议三个方向:1) 给 TaskFlow 添加新功能(AI 任务推荐、甘特图、时间追踪);2) 将 TaskFlow 部署到生产环境真实使用;3) 学习进阶框架(tRPC + Next.js 全栈类型安全、Turborepo 微前端架构)。每添加一个功能就重复 Phase 5 的 CI/CD + 监控流程。

📖 小节

📝 作业

  1. 基础题(⭐):将 TaskFlow 部署到 Vercel(免费账户),配置自定义域名(可选),验证 /api/health 返回 200。配置 NEXT_PUBLIC_POSTHOG_KEY 环境变量并确认 PostHog 接收到 pageview 事件。

  2. 进阶题(⭐⭐):在本地测试 Docker 自托管方案——构建 Docker 镜像、启动 docker-compose(App + Nginx + PostgreSQL)、配置 SSL 自签名证书、运行数据库迁移、验证 HTTPS 访问。提交 docker-compose.yml + nginx.conf + Dockerfile 到项目仓库。

  3. 挑战题(⭐⭐⭐):为 TaskFlow 实现完整的 Function Monitoring:在 PostHog 中定义 5 个关键事件(task_created / task_completed / user_signup / file_uploaded / project_created)并添加属性(耗时/浏览器/地理位置);在 Sentry 中配置 Alert 规则(错误率 > 5% 时发 Slack 通知 + PagerDuty);在 GitHub Actions 中添加 Playwright E2E 测试步骤(Deploy → E2E → Lighthouse 串联);最终提交一份 README.md 包含:部署架构图 + 监控面板截图 + Lighthouse 报告 + CI/CD 状态徽章。

Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏