Appearance
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 start1.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.comtypescript
// 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 --prod2.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 startupjavascript
// 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.js3.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.com3.3 Standalone 模式部署
javascript
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
output: 'standalone',
}
module.exports = nextConfigbash
# 构建
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 --build4.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 build5.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/nextjsjavascript
// 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 = nextConfig7.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 学习资源
官方文档
工具
- Vercel
- Docker
- PM2
- Nginx
课后练习
- 部署应用到 Vercel
- 配置自托管部署
- 创建 Docker 镜像
- 实现 CI/CD 流程
- 优化应用性能
通过本课程的学习,你应该能够自信地部署和优化 Next.js 应用,确保生产环境的高可用性和优异性能!