生产环境部署
本节全面介绍 Vue 3 应用的生产环境构建优化、部署方案和监控实践。
概述
部署流程
code
┌─────────────────────────────────────────────────────────────┐
│ 部署流程 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ 代码提交 │───▶│ CI/CD │───▶│ 构建 │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │ │
│ ┌─────────────┐ ┌─────────────┐ │ │
│ │ 监控 │◀───│ CDN/服务器 │◀────────┘ │
│ └─────────────┘ └─────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘部署检查清单
| 检查项 | 说明 |
|---|---|
| ✅ 环境变量 | 配置正确的生产环境变量 |
| ✅ 构建优化 | 代码分割、压缩、Tree-shaking |
| ✅ 资源优化 | 图片压缩、字体优化 |
| ✅ 安全配置 | HTTPS、安全头 |
| ✅ 缓存策略 | 静态资源长期缓存 |
| ✅ 错误监控 | Sentry 等监控工具 |
| ✅ 性能监控 | Core Web Vitals |
构建优化
构建命令
bash
# 生产构建
npm run build
# 分析构建结果
npm run build -- --mode analyze
# 预览构建结果
npm run previewVite 构建配置
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-visualizerjs
// 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-compressionjs
// 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-imageminjs
// 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:roVercel 部署
配置文件
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 --prodNetlify 部署
配置文件
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@v4Vite 配置
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.htmlCI/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/tracingts
// 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 = 2002. 静态资源 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. 缓存问题
问题:更新后用户看到旧版本
解决:
- 使用内容哈希文件名
- HTML 不缓存
- 添加版本检查
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. 白屏问题
问题:生产环境白屏
排查步骤:
- 检查浏览器控制台错误
- 检查网络请求是否成功
- 检查环境变量是否正确
- 检查 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 配置正确
### 监控配置
- [ ] 错误监控已配置
- [ ] 性能监控已配置
- [ ] 日志收集已配置
### 备份与回滚
- [ ] 已备份上一版本
- [ ] 回滚流程已测试