生产环境构建
Vite生产构建配置
// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'
import { visualizer } from 'rollup-plugin-visualizer'
export default defineConfig(({ command, mode }) => {
const isProduction = mode === 'production'
return {
plugins: [
vue(),
// 生产环境分析包大小
isProduction && visualizer({
filename: 'dist/stats.html',
open: true,
gzipSize: true
})
].filter(Boolean),
resolve: {
alias: {
'@': resolve(__dirname, 'src')
}
},
build: {
// 生产环境优化
minify: 'terser',
terserOptions: {
compress: {
drop_console: isProduction,
drop_debugger: isProduction
}
},
// 代码分割
rollupOptions: {
output: {
manualChunks: {
vendor: ['vue', 'vue-router'],
ui: ['element-plus', 'ant-design-vue'],
utils: ['lodash', 'axios', 'dayjs']
}
}
},
// 资源内联阈值
assetsInlineLimit: 4096,
// 启用源码映射(可选)
sourcemap: !isProduction,
// 输出目录
outDir: 'dist/client',
// 清空输出目录
emptyOutDir: true
},
// 服务器配置
server: {
port: 3000,
host: true,
proxy: {
'/api': {
target: 'http://localhost:3001',
changeOrigin: true
}
}
},
// 预览配置
preview: {
port: 4173,
host: true
},
// 环境变量
define: {
__APP_VERSION__: JSON.stringify(process.env.npm_package_version),
__BUILD_TIME__: JSON.stringify(new Date().toISOString())
}
}
})
构建脚本配置
{
"scripts": {
"dev": "node server/dev-server.js",
"build": "npm run build:client && npm run build:server",
"build:client": "vite build",
"build:server": "vite build --ssr src/entry-server.js --outDir dist/server",
"preview": "npm run build && npm run serve",
"serve": "NODE_ENV=production node server/prod-server.js",
"analyze": "npm run build:client -- --mode analyze",
"lint": "eslint src --ext .vue,.js,.ts",
"lint:fix": "eslint src --ext .vue,.js,.ts --fix",
"test": "vitest",
"test:coverage": "vitest --coverage",
"docker:build": "docker build -t vue-ssr-app .",
"docker:run": "docker run -p 3000:3000 vue-ssr-app"
}
}
Docker容器化
Dockerfile
# 多阶段构建
# 阶段1: 构建阶段
FROM node:18-alpine AS builder
# 设置工作目录
WORKDIR /app
# 复制package文件
COPY package*.json ./
# 安装依赖
RUN npm ci --only=production && npm cache clean --force
# 复制源代码
COPY . .
# 构建应用
RUN npm run build
# 阶段2: 运行阶段
FROM node:18-alpine AS runner
# 创建非root用户
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
# 设置工作目录
WORKDIR /app
# 复制package文件
COPY package*.json ./
# 只安装生产依赖
RUN npm ci --only=production && npm cache clean --force
# 从构建阶段复制构建产物
COPY --from=builder --chown=nextjs:nodejs /app/dist ./dist
COPY --from=builder --chown=nextjs:nodejs /app/server ./server
COPY --from=builder --chown=nextjs:nodejs /app/public ./public
# 切换到非root用户
USER nextjs
# 暴露端口
EXPOSE 3000
# 设置环境变量
ENV NODE_ENV=production
ENV PORT=3000
# 健康检查
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD node healthcheck.js
# 启动应用
CMD ["npm", "run", "serve"]
Docker Compose配置
# docker-compose.yml
version: '3.8'
services:
app:
build: .
ports:
- "3000:3000"
environment:
- NODE_ENV=production
- REDIS_HOST=redis
- DATABASE_URL=postgresql://user:password@postgres:5432/mydb
depends_on:
- redis
- postgres
restart: unless-stopped
healthcheck:
test: ["CMD", "node", "healthcheck.js"]
interval: 30s
timeout: 10s
retries: 3
volumes:
- ./logs:/app/logs
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
- redis_data:/data
restart: unless-stopped
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 30s
timeout: 10s
retries: 3
postgres:
image: postgres:15-alpine
ports:
- "5432:5432"
environment:
- POSTGRES_DB=mydb
- POSTGRES_USER=user
- POSTGRES_PASSWORD=password
volumes:
- postgres_data:/var/lib/postgresql/data
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", "pg_isready -U user -d mydb"]
interval: 30s
timeout: 10s
retries: 3
nginx:
image: nginx:alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf
- ./ssl:/etc/nginx/ssl
depends_on:
- app
restart: unless-stopped
volumes:
redis_data:
postgres_data:
健康检查脚本
// healthcheck.js
const http = require('http')
const options = {
hostname: 'localhost',
port: process.env.PORT || 3000,
path: '/health',
method: 'GET',
timeout: 3000
}
const req = http.request(options, (res) => {
if (res.statusCode === 200) {
process.exit(0)
} else {
console.error(`健康检查失败: HTTP ${res.statusCode}`)
process.exit(1)
}
})
req.on('error', (err) => {
console.error('健康检查错误:', err.message)
process.exit(1)
})
req.on('timeout', () => {
console.error('健康检查超时')
req.destroy()
process.exit(1)
})
req.end()
Nginx反向代理
Nginx配置
# nginx.conf
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
# 日志格式
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
error_log /var/log/nginx/error.log warn;
# 基础配置
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
# Gzip压缩
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_proxied any;
gzip_comp_level 6;
gzip_types
text/plain
text/css
text/xml
text/javascript
application/json
application/javascript
application/xml+rss
application/atom+xml
image/svg+xml;
# 上游服务器
upstream app_servers {
least_conn;
server app:3000 max_fails=3 fail_timeout=30s;
# 可以添加更多服务器实现负载均衡
# server app2:3000 max_fails=3 fail_timeout=30s;
}
# 限流配置
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=general:10m rate=1r/s;
server {
listen 80;
server_name localhost;
# 重定向到HTTPS
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name localhost;
# SSL配置
ssl_certificate /etc/nginx/ssl/cert.pem;
ssl_certificate_key /etc/nginx/ssl/key.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
# 安全头
add_header X-Frame-Options DENY;
add_header X-Content-Type-Options nosniff;
add_header X-XSS-Protection "1; mode=block";
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
# 静态资源
location /assets/ {
alias /app/dist/client/assets/;
expires 1y;
add_header Cache-Control "public, immutable";
add_header X-Content-Type-Options nosniff;
# 启用Brotli压缩(如果可用)
brotli on;
brotli_comp_level 6;
brotli_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
}
# API路由限流
location /api/ {
limit_req zone=api burst=20 nodelay;
proxy_pass http://app_servers;
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;
# 超时设置
proxy_connect_timeout 30s;
proxy_send_timeout 30s;
proxy_read_timeout 30s;
}
# 健康检查
location /health {
proxy_pass http://app_servers;
access_log off;
}
# 主应用
location / {
limit_req zone=general burst=10 nodelay;
proxy_pass http://app_servers;
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;
# 超时设置
proxy_connect_timeout 30s;
proxy_send_timeout 30s;
proxy_read_timeout 30s;
# 错误页面
error_page 502 503 504 /50x.html;
}
# 错误页面
location = /50x.html {
root /usr/share/nginx/html;
}
}
}
CI/CD流程
GitHub Actions配置
# .github/workflows/deploy.yml
name: Deploy to Production
on:
push:
branches: [main]
pull_request:
branches: [main]
env:
NODE_VERSION: '18'
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run linting
run: npm run lint
- name: Run tests
run: npm run test:coverage
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
file: ./coverage/lcov.info
flags: unittests
name: codecov-umbrella
build:
needs: test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
permissions:
contents: read
packages: write
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Setup Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to Container Registry
uses: docker/login-action@v2
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v4
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=ref,event=pr
type=sha,prefix={{branch}}-
type=raw,value=latest,enable={{is_default_branch}}
- name: Build and push Docker image
uses: docker/build-push-action@v4
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
deploy:
needs: build
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
environment:
name: production
url: https://your-domain.com
steps:
- name: Deploy to production
uses: appleboy/ssh-action@v0.1.5
with:
host: ${{ secrets.HOST }}
username: ${{ secrets.USERNAME }}
key: ${{ secrets.SSH_KEY }}
script: |
cd /opt/vue-ssr-app
docker-compose pull
docker-compose up -d
docker system prune -f
GitLab CI配置
# .gitlab-ci.yml
stages:
- test
- build
- deploy
variables:
DOCKER_DRIVER: overlay2
DOCKER_TLS_CERTDIR: "/certs"
IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
before_script:
- docker info
test:
stage: test
image: node:18-alpine
cache:
paths:
- node_modules/
script:
- npm ci
- npm run lint
- npm run test:coverage
coverage: '/Lines\s*:\s*(\d+\.?\d*)%/'
artifacts:
reports:
coverage_report:
coverage_format: cobertura
path: coverage/cobertura-coverage.xml
build:
stage: build
image: docker:20.10.16
services:
- docker:20.10.16-dind
before_script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
script:
- docker build -t $IMAGE_TAG .
- docker push $IMAGE_TAG
only:
- main
deploy_production:
stage: deploy
image: alpine:latest
before_script:
- apk add --no-cache openssh-client
- eval $(ssh-agent -s)
- echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add -
- mkdir -p ~/.ssh
- chmod 700 ~/.ssh
- ssh-keyscan $DEPLOY_HOST >> ~/.ssh/known_hosts
- chmod 644 ~/.ssh/known_hosts
script:
- |
ssh $DEPLOY_USER@$DEPLOY_HOST << EOF
cd /opt/vue-ssr-app
docker pull $IMAGE_TAG
docker-compose down
docker-compose up -d
docker system prune -f
EOF
environment:
name: production
url: https://your-domain.com
only:
- main
when: manual
环境变量管理
环境配置文件
# .env.production
NODE_ENV=production
PORT=3000
# 数据库配置
DATABASE_URL=postgresql://user:password@postgres:5432/mydb
REDIS_URL=redis://redis:6379
# API配置
API_BASE_URL=https://api.your-domain.com
API_TIMEOUT=10000
# 认证配置
JWT_SECRET=your-super-secret-jwt-key
JWT_EXPIRES_IN=24h
# 缓存配置
CACHE_TTL=300
CACHE_MAX_SIZE=1000
# 监控配置
SENTRY_DSN=https://your-sentry-dsn
SENTRY_ENVIRONMENT=production
# 日志配置
LOG_LEVEL=info
LOG_FORMAT=json
# 安全配置
CORS_ORIGIN=https://your-domain.com
CSP_REPORT_URI=https://your-domain.com/csp-report
配置管理工具
// config/index.js
import dotenv from 'dotenv'
import path from 'path'
// 加载环境变量
const envFile = `.env.${process.env.NODE_ENV || 'development'}`
dotenv.config({ path: path.resolve(process.cwd(), envFile) })
class Config {
constructor() {
this.env = process.env.NODE_ENV || 'development'
this.port = parseInt(process.env.PORT) || 3000
this.host = process.env.HOST || 'localhost'
// 数据库配置
this.database = {
url: process.env.DATABASE_URL,
ssl: this.env === 'production'
}
// Redis配置
this.redis = {
url: process.env.REDIS_URL || 'redis://localhost:6379',
ttl: parseInt(process.env.CACHE_TTL) || 300
}
// API配置
this.api = {
baseUrl: process.env.API_BASE_URL || 'http://localhost:3001',
timeout: parseInt(process.env.API_TIMEOUT) || 10000
}
// 认证配置
this.auth = {
jwtSecret: process.env.JWT_SECRET || 'default-secret',
jwtExpiresIn: process.env.JWT_EXPIRES_IN || '24h'
}
// 监控配置
this.monitoring = {
sentryDsn: process.env.SENTRY_DSN,
sentryEnvironment: process.env.SENTRY_ENVIRONMENT || this.env
}
// 日志配置
this.logging = {
level: process.env.LOG_LEVEL || 'info',
format: process.env.LOG_FORMAT || 'combined'
}
// 安全配置
this.security = {
corsOrigin: process.env.CORS_ORIGIN || 'http://localhost:3000',
cspReportUri: process.env.CSP_REPORT_URI
}
this.validate()
}
validate() {
const required = [
'JWT_SECRET',
'DATABASE_URL'
]
const missing = required.filter(key => !process.env[key])
if (missing.length > 0) {
throw new Error(`缺少必需的环境变量: ${missing.join(', ')}`)
}
}
get isDevelopment() {
return this.env === 'development'
}
get isProduction() {
return this.env === 'production'
}
get isTest() {
return this.env === 'test'
}
}
export const config = new Config()
export default config
监控和日志
应用监控
// server/monitoring/index.js
import * as Sentry from '@sentry/node'
import { ProfilingIntegration } from '@sentry/profiling-node'
import prometheus from 'prom-client'
import config from '../config/index.js'
// 初始化Sentry
if (config.monitoring.sentryDsn) {
Sentry.init({
dsn: config.monitoring.sentryDsn,
environment: config.monitoring.sentryEnvironment,
integrations: [
new ProfilingIntegration()
],
tracesSampleRate: config.isProduction ? 0.1 : 1.0,
profilesSampleRate: config.isProduction ? 0.1 : 1.0
})
}
// Prometheus指标
const register = new prometheus.Registry()
// 默认指标
prometheus.collectDefaultMetrics({ register })
// 自定义指标
const httpRequestDuration = new prometheus.Histogram({
name: 'http_request_duration_seconds',
help: 'HTTP请求持续时间',
labelNames: ['method', 'route', 'status_code'],
buckets: [0.1, 0.3, 0.5, 0.7, 1, 3, 5, 7, 10]
})
const httpRequestTotal = new prometheus.Counter({
name: 'http_requests_total',
help: 'HTTP请求总数',
labelNames: ['method', 'route', 'status_code']
})
const activeConnections = new prometheus.Gauge({
name: 'active_connections',
help: '活跃连接数'
})
register.registerMetric(httpRequestDuration)
register.registerMetric(httpRequestTotal)
register.registerMetric(activeConnections)
// 监控中间件
export function monitoringMiddleware(req, res, next) {
const start = Date.now()
res.on('finish', () => {
const duration = (Date.now() - start) / 1000
const route = req.route?.path || req.path
httpRequestDuration
.labels(req.method, route, res.statusCode)
.observe(duration)
httpRequestTotal
.labels(req.method, route, res.statusCode)
.inc()
})
next()
}
// 指标端点
export async function metricsHandler(req, res) {
res.set('Content-Type', register.contentType)
res.end(await register.metrics())
}
export { Sentry, register }
下一步
在下一章节中,我们将学习Vue SSR的故障排除和调试技巧,以及常见问题的解决方案。