Vue.js 项目部署

Vue 项目部署概述

项目部署是将开发完成的 Vue 应用发布到生产环境,让用户可以访问的过程。正确的部署策略可以确保应用的稳定性、安全性和高性能。

部署关键要点
  • 环境配置 - 生产环境 vs 开发环境
  • 构建优化 - 代码压缩、打包优化
  • 服务器配置 - Nginx、Docker 等
  • 自动化部署 - CI/CD 流程
  • 监控告警 - 应用性能监控
  • 回滚策略 - 快速恢复机制
生产环境部署

从开发到上线的完整流程

部署前准备

环境配置检查
# 1. 环境变量配置
# .env.production
VITE_API_BASE_URL=https://api.example.com
VITE_APP_TITLE=生产环境
VITE_ENABLE_SENTRY=true

# 2. 构建脚本确认
# package.json
{
  "scripts": {
    "build": "vue-tsc && vite build",
    "build:prod": "vite build --mode production",
    "build:analyze": "vite build --mode production && vite-bundle-analyzer"
  }
}

# 3. 依赖检查
npm audit --production
npm outdated
预部署检查清单
  • ✅ 代码已通过 ESLint 检查
  • ✅ 所有单元测试通过
  • ✅ 生产环境 API 地址已配置
  • ✅ 敏感信息已从代码中移除
  • ✅ 第三方依赖安全扫描通过
  • ✅ 浏览器兼容性已测试
  • ✅ 性能优化已实施
  • ✅ 错误监控系统已集成

生产环境构建配置

// vite.config.js - 生产环境构建优化
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import compression from 'vite-plugin-compression'
import { visualizer } from 'rollup-plugin-visualizer'

export default defineConfig({
  plugins: [
    vue(),
    // Gzip/Brotli 压缩
    compression({
      algorithm: 'gzip',
      ext: '.gz',
      threshold: 10240
    }),
    // 打包分析
    visualizer({
      open: false,
      filename: 'dist/stats.html'
    })
  ],

  build: {
    target: 'es2020',
    outDir: 'dist',
    assetsDir: 'assets',

    // 关闭 sourcemap 或只在需要时开启
    sourcemap: process.env.SOURCE_MAP === 'true',

    // 代码压缩
    minify: 'terser',
    terserOptions: {
      compress: {
        drop_console: true,    // 移除 console
        drop_debugger: true,   // 移除 debugger
        pure_funcs: ['console.log', 'console.info']
      }
    },

    // 代码分割优化
    rollupOptions: {
      output: {
        // 文件命名规则
        chunkFileNames: 'assets/js/[name]-[hash].js',
        entryFileNames: 'assets/js/[name]-[hash].js',
        assetFileNames: 'assets/[ext]/[name]-[hash].[ext]',

        // 手动拆分包
        manualChunks(id) {
          if (id.includes('node_modules')) {
            if (id.includes('vue')) {
              return 'vue-vendor'
            }
            if (id.includes('element-plus') || id.includes('ant-design')) {
              return 'ui-vendor'
            }
            if (id.includes('lodash') || id.includes('axios')) {
              return 'utils-vendor'
            }
            return 'vendor'
          }
        }
      }
    },

    // 限制 chunk 大小警告
    chunkSizeWarningLimit: 2000
  }
})

Nginx 服务器部署

1. 基础 Nginx 配置

# /etc/nginx/conf.d/vue-app.conf
server {
    listen 80;
    server_name your-domain.com www.your-domain.com;
    root /var/www/vue-app/dist;
    index index.html;

    # Gzip 压缩
    gzip on;
    gzip_vary on;
    gzip_min_length 1024;
    gzip_types text/plain text/css text/xml text/javascript
               application/javascript application/xml+rss
               application/json image/svg+xml;

    # 静态文件缓存
    location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
        expires 1y;
        add_header Cache-Control "public, immutable";
        add_header X-Content-Type-Options nosniff;
    }

    # HTML 文件不缓存
    location ~* \.html$ {
        expires -1;
        add_header Cache-Control "no-store, no-cache, must-revalidate";
    }

    # 主应用入口
    location / {
        try_files $uri $uri/ /index.html;
        add_header X-Frame-Options "SAMEORIGIN";
        add_header X-XSS-Protection "1; mode=block";
        add_header X-Content-Type-Options nosniff;
    }

    # API 代理
    location /api/ {
        proxy_pass http://backend-server:3000;
        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_connect_timeout 60s;
        proxy_send_timeout 60s;
        proxy_read_timeout 60s;
    }

    # 安全头
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
    add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.example.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: https:; connect-src 'self' https://api.example.com;" always;
}

2. HTTPS 配置(SSL/TLS)

Let's Encrypt 证书
# 安装 Certbot
sudo apt-get update
sudo apt-get install certbot python3-certbot-nginx

# 获取证书
sudo certbot --nginx -d your-domain.com -d www.your-domain.com

# 自动续期测试
sudo certbot renew --dry-run

# 自动续期脚本
# crontab -e
# 0 0,12 * * * /usr/bin/certbot renew --quiet
Nginx HTTPS 配置
# HTTPS 配置
server {
    listen 443 ssl http2;
    server_name your-domain.com;

    # SSL 证书路径
    ssl_certificate /etc/letsencrypt/live/your-domain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/your-domain.com/privkey.pem;

    # SSL 配置
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512;
    ssl_prefer_server_ciphers off;

    # SSL 会话缓存
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 10m;

    # OCSP Stapling
    ssl_stapling on;
    ssl_stapling_verify on;

    # HSTS
    add_header Strict-Transport-Security "max-age=31536000" always;

    # 其他配置与 HTTP 相同
    root /var/www/vue-app/dist;
    # ...
}

3. 性能优化配置

# /etc/nginx/nginx.conf - 性能优化
user www-data;
worker_processes auto;
pid /run/nginx.pid;

events {
    worker_connections 4096;        # 每个worker最大连接数
    multi_accept on;                # 同时接受多个连接
    use epoll;                      # 使用epoll高效I/O模型
}

http {
    # 基础设置
    sendfile on;
    tcp_nopush on;
    tcp_nodelay on;
    keepalive_timeout 65;
    types_hash_max_size 2048;
    server_tokens off;              # 隐藏Nginx版本信息

    # MIME类型
    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;

    # Gzip压缩
    gzip on;
    gzip_disable "msie6";
    gzip_vary on;
    gzip_proxied any;
    gzip_comp_level 6;
    gzip_buffers 16 8k;
    gzip_http_version 1.1;
    gzip_min_length 256;
    gzip_types
        application/atom+xml
        application/javascript
        application/json
        application/ld+json
        application/manifest+json
        application/rss+xml
        application/vnd.geo+json
        application/vnd.ms-fontobject
        application/x-font-ttf
        application/x-web-app-manifest+json
        application/xhtml+xml
        application/xml
        font/opentype
        image/bmp
        image/svg+xml
        image/x-icon
        text/cache-manifest
        text/css
        text/plain
        text/vcard
        text/vnd.rim.location.xloc
        text/vtt
        text/x-component
        text/x-cross-domain-policy;

    # 包含站点配置
    include /etc/nginx/conf.d/*.conf;
    include /etc/nginx/sites-enabled/*;
}

Docker 容器化部署

1. Dockerfile 配置

多阶段构建
# 第一阶段:构建应用
FROM node:18-alpine as builder

# 设置工作目录
WORKDIR /app

# 复制 package 文件
COPY package*.json ./
COPY pnpm-lock.yaml ./

# 安装依赖(使用国内镜像加速)
RUN npm config set registry https://registry.npmmirror.com \
    && npm install -g pnpm \
    && pnpm install --frozen-lockfile

# 复制源代码
COPY . .

# 构建应用
RUN pnpm run build

# 第二阶段:运行应用
FROM nginx:alpine as production

# 复制构建产物
COPY --from=builder /app/dist /usr/share/nginx/html

# 复制自定义 Nginx 配置
COPY nginx.conf /etc/nginx/conf.d/default.conf

# 创建非root用户
RUN addgroup -g 1001 -S nodejs \
    && adduser -S -u 1001 -G nodejs nodejs \
    && chown -R nodejs:nodejs /usr/share/nginx/html

# 切换到非root用户
USER nodejs

# 暴露端口
EXPOSE 80

# 启动 Nginx
CMD ["nginx", "-g", "daemon off;"]
Docker Compose 配置
# docker-compose.yml
version: '3.8'

services:
  # Vue 前端应用
  frontend:
    build:
      context: .
      dockerfile: Dockerfile
    container_name: vue-app
    ports:
      - "8080:80"
    environment:
      - NODE_ENV=production
      - VITE_API_URL=http://backend:3000
    volumes:
      - ./logs:/var/log/nginx
      - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
    networks:
      - app-network
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:80"]
      interval: 30s
      timeout: 10s
      retries: 3
    restart: unless-stopped

  # 后端 API 服务
  backend:
    image: node:18-alpine
    container_name: api-server
    working_dir: /app
    volumes:
      - ./backend:/app
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=production
      - DATABASE_URL=postgres://user:pass@db:5432/app
    networks:
      - app-network
    depends_on:
      - db
    command: ["node", "server.js"]

  # 数据库
  db:
    image: postgres:15-alpine
    container_name: postgres-db
    environment:
      - POSTGRES_USER=app_user
      - POSTGRES_PASSWORD=secure_password
      - POSTGRES_DB=app_db
    volumes:
      - postgres_data:/var/lib/postgresql/data
    networks:
      - app-network
    restart: unless-stopped

  # 监控服务
  prometheus:
    image: prom/prometheus:latest
    container_name: prometheus
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml
    ports:
      - "9090:9090"
    networks:
      - app-network

  # 可视化监控
  grafana:
    image: grafana/grafana:latest
    container_name: grafana
    ports:
      - "3001:3000"
    volumes:
      - grafana_data:/var/lib/grafana
    networks:
      - app-network

networks:
  app-network:
    driver: bridge

volumes:
  postgres_data:
  grafana_data:

2. 构建和运行脚本

#!/bin/bash
# deploy.sh - Docker 部署脚本

# 设置变量
APP_NAME="vue-app"
REGISTRY="your-registry.com"
TAG="latest"
ENVIRONMENT="production"

echo "开始构建和部署 $APP_NAME..."

# 1. 构建 Docker 镜像
docker build -t $REGISTRY/$APP_NAME:$TAG .

# 2. 推送镜像到仓库
docker push $REGISTRY/$APP_NAME:$TAG

# 3. 拉取最新镜像并部署
ssh user@server << EOF
  # 拉取最新镜像
  docker pull $REGISTRY/$APP_NAME:$TAG

  # 停止并删除旧容器
  docker stop $APP_NAME || true
  docker rm $APP_NAME || true

  # 运行新容器
  docker run -d \\
    --name $APP_NAME \\
    --restart unless-stopped \\
    -p 80:80 \\
    -p 443:443 \\
    -v /etc/letsencrypt:/etc/letsencrypt:ro \\
    -v /var/log/nginx:/var/log/nginx \\
    --env-file .env.$ENVIRONMENT \\
    $REGISTRY/$APP_NAME:$TAG

  # 清理无用镜像
  docker image prune -f

  echo "$APP_NAME 部署完成!"
EOF

echo "部署完成!"

CI/CD 自动化部署

GitHub Actions
# .github/workflows/deploy.yml
name: Deploy Vue App

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

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: pnpm/action-setup@v2
        with:
          version: 8
      - uses: actions/setup-node@v3
        with:
          node-version: 18
          cache: 'pnpm'
      - run: pnpm install --frozen-lockfile
      - run: pnpm run lint
      - run: pnpm run test:unit

  build-and-deploy:
    needs: test
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    steps:
      - uses: actions/checkout@v3

      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: 18
          cache: 'pnpm'

      - name: Install dependencies
        run: pnpm install --frozen-lockfile

      - name: Build application
        run: pnpm run build
        env:
          VITE_API_URL: ${{ secrets.PROD_API_URL }}
          VITE_APP_TITLE: ${{ secrets.APP_TITLE }}

      - name: Deploy to Server
        uses: appleboy/scp-action@v0.1.4
        with:
          host: ${{ secrets.SSH_HOST }}
          username: ${{ secrets.SSH_USER }}
          key: ${{ secrets.SSH_PRIVATE_KEY }}
          source: "dist/*"
          target: "/var/www/vue-app"

      - name: Restart Nginx
        uses: appleboy/ssh-action@v0.1.6
        with:
          host: ${{ secrets.SSH_HOST }}
          username: ${{ secrets.SSH_USER }}
          key: ${{ secrets.SSH_PRIVATE_KEY }}
          script: |
            sudo systemctl restart nginx
            echo "部署成功!"
GitLab CI/CD
# .gitlab-ci.yml
stages:
  - test
  - build
  - deploy

variables:
  NODE_VERSION: "18"
  PNPM_VERSION: "8"

cache:
  key: ${CI_COMMIT_REF_SLUG}
  paths:
    - node_modules/
    - .pnpm-store/

before_script:
  - apt-get update -qq && apt-get install -y -qq curl
  - curl -sL https://deb.nodesource.com/setup_${NODE_VERSION}.x | bash -
  - apt-get install -y nodejs
  - npm install -g pnpm@${PNPM_VERSION}

test:
  stage: test
  script:
    - pnpm install --frozen-lockfile
    - pnpm run lint
    - pnpm run test:unit
  artifacts:
    paths:
      - coverage/

build:
  stage: build
  script:
    - pnpm run build
    - ls -la dist/
  artifacts:
    paths:
      - dist/
  only:
    - main

deploy_production:
  stage: deploy
  script:
    - apt-get update && apt-get install -y openssh-client
    - mkdir -p ~/.ssh
    - echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa
    - chmod 600 ~/.ssh/id_rsa
    - '[[ -f /.dockerenv ]] && echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config'
    - scp -r dist/* $DEPLOY_USER@$DEPLOY_HOST:$DEPLOY_PATH
    - ssh $DEPLOY_USER@$DEPLOY_HOST "cd $DEPLOY_PATH && sudo systemctl restart nginx"
  only:
    - main
  when: manual
云平台部署
Vercel
// vercel.json
{
  "rewrites": [
    { "source": "/(.*)", "destination": "/index.html" }
  ],
  "headers": [
    {
      "source": "/(.*)",
      "headers": [
        { "key": "Cache-Control", "value": "public, max-age=3600" },
        { "key": "X-Frame-Options", "value": "DENY" }
      ]
    }
  ]
}
Netlify
# netlify.toml
[build]
  command = "pnpm run build"
  publish = "dist"

[[redirects]]
  from = "/*"
  to = "/index.html"
  status = 200

[[headers]]
  for = "/*"
  [headers.values]
    X-Frame-Options = "DENY"
    X-XSS-Protection = "1; mode=block"

性能监控与优化

应用性能监控
// 集成性能监控
// main.js
import { createApp } from 'vue'
import App from './App.vue'
import * as Sentry from '@sentry/vue'
import { BrowserTracing } from '@sentry/tracing'

const app = createApp(App)

// Sentry 错误监控
if (import.meta.env.PROD) {
  Sentry.init({
    app,
    dsn: import.meta.env.VITE_SENTRY_DSN,
    integrations: [
      new BrowserTracing({
        routingInstrumentation: Sentry.vueRouterInstrumentation(router),
        tracingOrigins: ['localhost', 'your-domain.com']
      })
    ],
    tracesSampleRate: 1.0,
    environment: import.meta.env.MODE
  })
}

// Web Vitals 监控
import { getCLS, getFID, getFCP, getLCP, getTTFB } from 'web-vitals'

function sendToAnalytics(metric) {
  const body = {
    name: metric.name,
    value: metric.value,
    rating: metric.rating
  }

  // 发送到监控服务器
  fetch('/api/analytics', {
    method: 'POST',
    body: JSON.stringify(body),
    headers: { 'Content-Type': 'application/json' }
  })
}

getCLS(sendToAnalytics)
getFID(sendToAnalytics)
getFCP(sendToAnalytics)
getLCP(sendToAnalytics)
getTTFB(sendToAnalytics)
性能优化配置
// 性能优化配置
export default defineConfig({
  build: {
    rollupOptions: {
      output: {
        // 预加载关键资源
        manualChunks: {
          vendor: ['vue', 'vue-router', 'pinia'],
          ui: ['element-plus'],
          utils: ['axios', 'lodash-es']
        }
      }
    }
  },

  // 预加载配置
  experimental: {
    renderBuiltUrl(filename, { hostId, hostType, type }) {
      if (type === 'asset' && filename.endsWith('.woff2')) {
        return {
          runtime: `(() => {
            const link = document.createElement('link')
            link.rel = 'preload'
            link.as = 'font'
            link.type = 'font/woff2'
            link.crossorigin = 'anonymous'
            link.href = '${filename}'
            document.head.appendChild(link)
          })()`
        }
      }
    }
  }
})

// 关键 CSS 提取
import { createApp } from 'vue'
import App from './App.vue'

const app = createApp(App)

// 服务端渲染关键 CSS
if (typeof window !== 'undefined') {
  import('./styles/critical.css')
}

CDN 和缓存策略

# Nginx 缓存配置
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
    # 长期缓存静态资源
    expires 1y;
    add_header Cache-Control "public, immutable";

    # 添加版本号防止缓存
    add_header ETag "";

    # 开启 Brotli 压缩
    brotli_static on;
    gzip_static on;
}

# 禁用 HTML 缓存
location ~* \.html$ {
    expires -1;
    add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate";
    add_header Pragma "no-cache";
}

# CDN 配置
location /assets/ {
    # 使用 CDN
    add_header X-CDN "cloudflare";

    # 允许跨域
    add_header Access-Control-Allow-Origin "*";

    # 安全头
    add_header X-Content-Type-Options "nosniff";
    add_header X-Frame-Options "DENY";
}

安全部署实践

安全配置清单
  • HTTPS 强制 - 所有流量加密
  • CSP 策略 - 防止 XSS 攻击
  • 安全头设置 - 增加安全防护
  • API 防护 - 速率限制、验证
  • 敏感信息保护 - 环境变量管理
  • 依赖安全扫描 - 定期检查漏洞
  • 访问控制 - 最小权限原则
  • 日志审计 - 操作记录追踪
常见安全问题
1. 敏感信息泄露

确保 API keys、数据库密码等不提交到版本控制

2. 依赖安全漏洞

定期运行 npm audit 检查依赖安全

3. 跨站脚本攻击

设置 Content-Security-Policy 头限制资源加载

4. 点击劫持保护

设置 X-Frame-Options 头防止页面被嵌入

部署检查清单

完整的部署检查清单

总结

部署核心要点
  • 环境隔离 - 开发、测试、生产环境严格分离
  • 自动化部署 - 使用 CI/CD 减少人为错误
  • 配置管理 - 敏感信息使用环境变量
  • 容器化部署 - Docker 提供一致的环境
  • 性能优化 - 构建优化、缓存策略、CDN 使用
  • 安全防护 - HTTPS、安全头、访问控制
  • 监控告警 - 实时监控、错误追踪、性能分析
  • 容灾备份 - 备份策略、回滚方案、多地域部署
最佳实践建议:
  1. 使用 Infrastructure as Code(IaC)管理服务器配置
  2. 实施蓝绿部署或金丝雀发布减少部署风险
  3. 定期进行安全扫描和性能测试
  4. 建立完整的监控和告警体系
  5. 制定详细的回滚计划和灾难恢复方案