{T}

生产环境部署

本节全面介绍 Vue 3 应用的生产环境构建优化、部署方案和监控实践。

概述

部署流程

code
┌─────────────────────────────────────────────────────────────┐
│                    部署流程                                  │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   ┌─────────────┐    ┌─────────────┐    ┌─────────────┐    │
│   │  代码提交   │───▶│  CI/CD     │───▶│  构建      │    │
│   └─────────────┘    └─────────────┘    └─────────────┘    │
│                                               │             │
│   ┌─────────────┐    ┌─────────────┐         │             │
│   │   监控     │◀───│  CDN/服务器 │◀────────┘             │
│   └─────────────┘    └─────────────┘                       │
│                                                             │
└─────────────────────────────────────────────────────────────┘

部署检查清单

检查项说明
✅ 环境变量配置正确的生产环境变量
✅ 构建优化代码分割、压缩、Tree-shaking
✅ 资源优化图片压缩、字体优化
✅ 安全配置HTTPS、安全头
✅ 缓存策略静态资源长期缓存
✅ 错误监控Sentry 等监控工具
✅ 性能监控Core Web Vitals

构建优化

构建命令

bash
# 生产构建
npm run build

# 分析构建结果
npm run build -- --mode analyze

# 预览构建结果
npm run preview

Vite 构建配置

js
// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'

export default defineConfig({
  plugins: [vue()],
  
  // 基础路径
  base: '/',  // 根路径部署
  // base: '/app/',  // 子路径部署
  
  build: {
    // 输出目录
    outDir: 'dist',
    
    // 静态资源目录
    assetsDir: 'assets',
    
    // 小于此阈值的资源将内联为 base64
    assetsInlineLimit: 4096,
    
    // 启用 sourcemap(调试用)
    sourcemap: false,
    
    // 压缩方式
    minify: 'esbuild',  // 'esbuild' | 'terser'
    
    // Terser 压缩选项(使用 terser 时)
    terserOptions: {
      compress: {
        drop_console: true,
        drop_debugger: true,
        pure_funcs: ['console.log']
      },
      format: {
        comments: false
      }
    },
    
    // 代码分割
    rollupOptions: {
      output: {
        // 入口文件名
        entryFileNames: 'js/[name]-[hash].js',
        
        // chunk 文件名
        chunkFileNames: 'js/[name]-[hash].js',
        
        // 资源文件名
        assetFileNames: (assetInfo) => {
          const info = assetInfo.name.split('.')
          const ext = info[info.length - 1]
          
          if (/\.(png|jpe?g|gif|svg|webp|ico)$/i.test(assetInfo.name)) {
            return 'images/[name]-[hash].[ext]'
          }
          if (/\.(woff2?|eot|ttf|otf)$/i.test(assetInfo.name)) {
            return 'fonts/[name]-[hash].[ext]'
          }
          if (/\.css$/i.test(assetInfo.name)) {
            return 'css/[name]-[hash].[ext]'
          }
          return 'assets/[name]-[hash].[ext]'
        },
        
        // 手动分包
        manualChunks(id) {
          // Vue 核心库
          if (id.includes('node_modules/vue/') || 
              id.includes('node_modules/@vue/') ||
              id.includes('node_modules/vue-router/') ||
              id.includes('node_modules/pinia/')) {
            return 'vue-vendor'
          }
          
          // UI 库
          if (id.includes('node_modules/element-plus/')) {
            return 'element-plus'
          }
          
          // 工具库
          if (id.includes('node_modules/lodash/') ||
              id.includes('node_modules/axios/') ||
              id.includes('node_modules/dayjs/')) {
            return 'utils'
          }
          
          // 其他 node_modules
          if (id.includes('node_modules/')) {
            return 'vendor'
          }
        }
      }
    },
    
    // chunk 大小警告阈值
    chunkSizeWarningLimit: 500,
    
    // 清空输出目录
    emptyOutDir: true,
    
    // CSS 代码分割
    cssCodeSplit: true,
    
    // 报告压缩后大小
    reportCompressedSize: true
  }
})

分析打包结果

bash
npm install -D rollup-plugin-visualizer
js
// vite.config.ts
import { visualizer } from 'rollup-plugin-visualizer'

export default defineConfig({
  plugins: [
    visualizer({
      open: true,
      filename: 'stats.html',
      gzipSize: true,
      brotliSize: true
    })
  ]
})

压缩插件

bash
npm install -D vite-plugin-compression
js
// vite.config.ts
import viteCompression from 'vite-plugin-compression'

export default defineConfig({
  plugins: [
    // Gzip 压缩
    viteCompression({
      algorithm: 'gzip',
      ext: '.gz',
      threshold: 10240,  // 大于 10KB 才压缩
      deleteOriginFile: false
    }),
    
    // Brotli 压缩(更好的压缩率)
    viteCompression({
      algorithm: 'brotliCompress',
      ext: '.br',
      threshold: 10240
    })
  ]
})

图片优化

bash
npm install -D vite-plugin-imagemin
js
// vite.config.ts
import viteImagemin from 'vite-plugin-imagemin'

export default defineConfig({
  plugins: [
    viteImagemin({
      gifsicle: { optimizationLevel: 3 },
      optipng: { optimizationLevel: 7 },
      mozjpeg: { quality: 80 },
      svgo: {
        plugins: [
          { name: 'removeViewBox', active: false }
        ]
      },
      webp: { quality: 80 }
    })
  ]
})

部署方案

静态服务器(Nginx)

基础配置

nginx
# /etc/nginx/sites-available/vue-app
server {
    listen 80;
    server_name example.com;
    
    root /var/www/vue-app/dist;
    index index.html;
    
    # 处理 Vue Router history 模式
    location / {
        try_files $uri $uri/ /index.html;
    }
    
    # 静态资源长期缓存
    location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
        expires 1y;
        add_header Cache-Control "public, immutable";
        access_log off;
    }
    
    # HTML 不缓存
    location ~* \.html$ {
        expires -1;
        add_header Cache-Control "no-store, no-cache, must-revalidate";
    }
    
    # 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 application/json application/javascript application/rss+xml application/atom+xml image/svg+xml;
}

HTTPS 配置

nginx
server {
    listen 443 ssl http2;
    server_name example.com;
    
    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
    
    ssl_session_timeout 1d;
    ssl_session_cache shared:SSL:50m;
    ssl_session_tickets off;
    
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
    ssl_prefer_server_ciphers off;
    
    # HSTS
    add_header Strict-Transport-Security "max-age=63072000" always;
    
    # 其他配置...
}

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

Docker 部署

多阶段构建

dockerfile
# Dockerfile
# 构建阶段
FROM node:18-alpine as builder

WORKDIR /app

# 复制依赖文件
COPY package*.json ./

# 安装依赖
RUN npm ci

# 复制源代码
COPY . .

# 构建
RUN npm run build

# 生产阶段
FROM nginx:alpine

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

# 复制 nginx 配置
COPY nginx.conf /etc/nginx/conf.d/default.conf

EXPOSE 80

CMD ["nginx", "-g", "daemon off;"]

docker-compose.yml

yaml
version: '3.8'

services:
  vue-app:
    build: .
    ports:
      - "80:80"
    restart: unless-stopped
    environment:
      - NODE_ENV=production
    volumes:
      - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro

Vercel 部署

配置文件

json
// vercel.json
{
  "buildCommand": "npm run build",
  "outputDirectory": "dist",
  "framework": "vue",
  "rewrites": [
    { "source": "/(.*)", "destination": "/index.html" }
  ],
  "headers": [
    {
      "source": "/assets/(.*)",
      "headers": [
        { "key": "Cache-Control", "value": "public, max-age=31536000, immutable" }
      ]
    }
  ]
}

部署命令

bash
# 安装 Vercel CLI
npm i -g vercel

# 部署
vercel

# 生产部署
vercel --prod

Netlify 部署

配置文件

toml
# netlify.toml
[build]
  command = "npm run build"
  publish = "dist"

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

[[headers]]
  for = "/assets/*"
  [headers.values]
    Cache-Control = "public, max-age=31536000, immutable"

[[headers]]
  for = "/*.js"
  [headers.values]
    Cache-Control = "public, max-age=31536000, immutable"

[[headers]]
  for = "/*.css"
  [headers.values]
    Cache-Control = "public, max-age=31536000, immutable"

GitHub Pages 部署

GitHub Actions 配置

yaml
# .github/workflows/deploy.yml
name: Deploy to GitHub Pages

on:
  push:
    branches: [main]
  workflow_dispatch:

permissions:
  contents: read
  pages: write
  id-token: write

concurrency:
  group: pages
  cancel-in-progress: false

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4
        
      - name: Setup Node
        uses: actions/setup-node@v4
        with:
          node-version: 18
          cache: 'npm'
          
      - name: Install dependencies
        run: npm ci
        
      - name: Build
        run: npm run build
        env:
          NODE_ENV: production
          
      - name: Setup Pages
        uses: actions/configure-pages@v4
        
      - name: Upload artifact
        uses: actions/upload-pages-artifact@v3
        with:
          path: dist
          
  deploy:
    environment:
      name: github-pages
      url: ${{ steps.deployment.outputs.page_url }}
    runs-on: ubuntu-latest
    needs: build
    steps:
      - name: Deploy to GitHub Pages
        id: deployment
        uses: actions/deploy-pages@v4

Vite 配置

js
// vite.config.ts
export default defineConfig({
  base: '/repository-name/',  // GitHub Pages 子路径
})

云平台部署

阿里云 OSS + CDN

yaml
# .github/workflows/aliyun.yml
name: Deploy to Aliyun OSS

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - name: Setup Node
        uses: actions/setup-node@v4
        with:
          node-version: 18
          
      - name: Build
        run: |
          npm ci
          npm run build
          
      - name: Deploy to OSS
        uses: manyuanrong/setup-ossutil@v2.0
        with:
          endpoint: oss-cn-hangzhou.aliyuncs.com
          access-key-id: ${{ secrets.OSS_ACCESS_KEY_ID }}
          access-key-secret: ${{ secrets.OSS_ACCESS_KEY_SECRET }}
          
      - run: ossutil cp -r -f dist oss://your-bucket-name/
      
      - name: Refresh CDN Cache
        run: |
          aliyun cdn RefreshObjectCaches \
            --ObjectPath https://your-domain.com/index.html

CI/CD 配置

完整 GitHub Actions 示例

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

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

env:
  NODE_VERSION: 18

jobs:
  # 代码检查
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - name: Setup Node
        uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'npm'
          
      - name: Install dependencies
        run: npm ci
        
      - name: Lint
        run: npm run lint
        
      - name: Type check
        run: npm run type-check

  # 单元测试
  test:
    runs-on: ubuntu-latest
    needs: lint
    steps:
      - uses: actions/checkout@v4
      
      - name: Setup Node
        uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'npm'
          
      - name: Install dependencies
        run: npm ci
        
      - name: Run tests
        run: npm run test:coverage
        
      - name: Upload coverage
        uses: codecov/codecov-action@v3
        with:
          files: ./coverage/lcov.info

  # 构建
  build:
    runs-on: ubuntu-latest
    needs: test
    steps:
      - uses: actions/checkout@v4
      
      - name: Setup Node
        uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'npm'
          
      - name: Install dependencies
        run: npm ci
        
      - name: Build
        run: npm run build
        env:
          NODE_ENV: production
          
      - name: Upload artifact
        uses: actions/upload-artifact@v4
        with:
          name: dist
          path: dist

  # 部署到生产环境
  deploy-production:
    runs-on: ubuntu-latest
    needs: build
    if: github.ref == 'refs/heads/main'
    environment: production
    steps:
      - name: Download artifact
        uses: actions/download-artifact@v4
        with:
          name: dist
          
      - name: Deploy
        run: |
          # 部署脚本
          echo "Deploying to production..."

环境变量管理

环境文件

bash
# .env.development
VITE_API_URL=http://localhost:3000
VITE_APP_ENV=development

# .env.staging
VITE_API_URL=https://staging-api.example.com
VITE_APP_ENV=staging

# .env.production
VITE_API_URL=https://api.example.com
VITE_APP_ENV=production

# .env.production.local (不提交 git)
VITE_API_KEY=secret-key

运行时环境变量

对于需要运行时注入的环境变量:

html
<!-- index.html -->
<script>
  window.__ENV__ = {
    API_URL: '${API_URL}',
    APP_VERSION: '${APP_VERSION}'
  }
</script>
js
// 构建时替换
// vite.config.ts
export default defineConfig({
  define: {
    __APP_VERSION__: JSON.stringify(process.env.npm_package_version)
  }
})

性能优化

加载性能

路由懒加载

ts
// router/index.ts
import { createRouter, createWebHistory } from 'vue-router'

const routes = [
  {
    path: '/',
    component: () => import('@/views/Home.vue')
  },
  {
    path: '/about',
    component: () => import('@/views/About.vue')
  },
  {
    path: '/admin',
    component: () => import('@/views/Admin.vue'),
    // 预加载
    meta: { preload: true }
  }
]

组件懒加载

Vue SFC
<script setup>
import { defineAsyncComponent } from 'vue'

// 异步组件
const AsyncComponent = defineAsyncComponent(() =>
  import('./components/HeavyComponent.vue')
)

// 带加载状态
const AsyncComponentWithOptions = defineAsyncComponent({
  loader: () => import('./components/HeavyComponent.vue'),
  loadingComponent: LoadingSpinner,
  errorComponent: ErrorComponent,
  delay: 200,
  timeout: 10000
})
</script>

预加载策略

ts
// 预加载关键资源
import { preload } from 'vue'

// 在用户可能访问前预加载
router.beforeEach((to, from, next) => {
  if (to.meta.preload) {
    import('@/views/' + to.name + '.vue')
  }
  next()
})

运行时性能

虚拟列表

Vue SFC
<script setup>
import { useVirtualList } from '@vueuse/core'

const { list, containerProps, wrapperProps } = useVirtualList(
  largeArray,
  { itemHeight: 50 }
)
</script>

<template>
  <div v-bind="containerProps" style="height: 500px; overflow: auto;">
    <div v-bind="wrapperProps">
      <div v-for="{ data, index } in list" :key="index">
        {{ data }}
      </div>
    </div>
  </div>
</template>

防抖节流

Vue SFC
<script setup>
import { useDebounceFn, useThrottleFn } from '@vueuse/core'

// 防抖
const handleSearch = useDebounceFn((query) => {
  // 搜索逻辑
}, 300)

// 节流
const handleScroll = useThrottleFn(() => {
  // 滚动处理
}, 100)
</script>

监控与错误追踪

Sentry 集成

bash
npm install @sentry/vue @sentry/tracing
ts
// main.ts
import * as Sentry from '@sentry/vue'
import { BrowserTracing } from '@sentry/tracing'

Sentry.init({
  dsn: import.meta.env.VITE_SENTRY_DSN,
  integrations: [
    new BrowserTracing({
      routingInstrumentation: Sentry.vueRouterInstrumentation(router)
    })
  ],
  tracesSampleRate: 0.1,
  environment: import.meta.env.MODE,
  release: import.meta.env.VITE_APP_VERSION
})

性能监控

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

function sendToAnalytics(metric) {
  const body = JSON.stringify({
    name: metric.name,
    value: metric.value,
    rating: metric.rating,
    delta: metric.delta,
    id: metric.id
  })
  
  navigator.sendBeacon('/analytics', body)
}

getCLS(sendToAnalytics)
getFID(sendToAnalytics)
getFCP(sendToAnalytics)
getLCP(sendToAnalytics)
getTTFB(sendToAnalytics)

Vue 错误处理

ts
// main.ts
import { createApp } from 'vue'
import App from './App.vue'

const app = createApp(App)

// 全局错误处理
app.config.errorHandler = (err, instance, info) => {
  console.error('Global error:', err)
  console.error('Component:', instance)
  console.error('Info:', info)
  
  // 发送到错误监控
  Sentry.captureException(err)
}

// 警告处理
app.config.warnHandler = (msg, instance, trace) => {
  console.warn('Vue warning:', msg)
}

app.mount('#app')

常见问题

1. 路由 404 问题

问题:刷新页面显示 404

解决:配置服务器重定向

nginx
# Nginx
location / {
    try_files $uri $uri/ /index.html;
}
yaml
# Netlify
[[redirects]]
  from = "/*"
  to = "/index.html"
  status = 200

2. 静态资源 404

问题:部署后资源加载失败

解决:检查 base 配置

js
// vite.config.ts
export default defineConfig({
  base: '/',           // 根路径
  base: '/app/',       // 子路径
  base: './',          // 相对路径
})

3. CORS 问题

问题:API 请求跨域错误

解决

nginx
# Nginx 配置 CORS
location /api {
    add_header Access-Control-Allow-Origin *;
    add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS';
    add_header Access-Control-Allow-Headers 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type';
    
    if ($request_method = OPTIONS) {
        return 204;
    }
    
    proxy_pass http://backend;
}

4. 缓存问题

问题:更新后用户看到旧版本

解决

  1. 使用内容哈希文件名
  2. HTML 不缓存
  3. 添加版本检查
js
// 版本检查
const currentVersion = __APP_VERSION__

setInterval(async () => {
  const res = await fetch('/version.json')
  const { version } = await res.json()
  
  if (version !== currentVersion) {
    console.log('新版本可用,请刷新页面')
  }
}, 60000)  // 每分钟检查

5. 白屏问题

问题:生产环境白屏

排查步骤

  1. 检查浏览器控制台错误
  2. 检查网络请求是否成功
  3. 检查环境变量是否正确
  4. 检查 base 路径配置
js
// 添加全局错误捕获
window.addEventListener('error', (event) => {
  console.error('Uncaught error:', event.error)
})

window.addEventListener('unhandledrejection', (event) => {
  console.error('Unhandled promise rejection:', event.reason)
})

6. 打包体积过大

解决

js
// 分析打包结果
npm run build -- --mode analyze

// 优化策略
1. 按需引入组件库
2. 使用 CDN 加载大型库
3. 代码分割
4. Tree-shaking

部署检查清单

markdown
## 部署前检查

### 构建检查
- [ ] `npm run build` 无错误
- [ ] 构建产物大小合理
- [ ] 无 console.log 输出(生产环境)

### 环境变量
- [ ] API 地址正确
- [ ] 环境标识正确
- [ ] 敏感信息未暴露

### 性能优化
- [ ] 图片已优化
- [ ] 代码已分割
- [ ] Gzip 压缩已启用

### 安全配置
- [ ] HTTPS 已启用
- [ ] 安全头已配置
- [ ] CORS 配置正确

### 监控配置
- [ ] 错误监控已配置
- [ ] 性能监控已配置
- [ ] 日志收集已配置

### 备份与回滚
- [ ] 已备份上一版本
- [ ] 回滚流程已测试

相关资源