前端部署与发布
大约 6 分钟约 1913 字
前端部署与发布
前端部署全景
前端部署是将开发完成的应用交付到用户可访问的服务器的完整过程,涵盖构建优化、CDN 分发、CI/CD 自动化、灰度发布和监控回滚等环节。一个成熟的部署流程需要兼顾交付速度、发布安全和用户体验。
构建产物优化
构建前检查清单
在部署之前,确保构建产物已经过优化:
# 1. 分析构建产物体积
npx vite-bundle-visualizer # Vite 项目
npx webpack-bundle-analyzer dist/stats.json # Webpack 项目
# 2. 检查 Tree Shaking 是否生效
# 对比期望使用的代码和实际产物,确认未使用的代码已被移除
# 3. 确认 Source Map 已正确生成(但不部署到 CDN)构建配置
// vite.config.ts — 生产构建优化
export default defineConfig(({ mode }) => ({
build: {
outDir: 'dist',
sourcemap: mode === 'development', // 生产环境不生成 source map(或只上传到错误监控)
rollupOptions: {
output: {
manualChunks: {
vendor: ['vue', 'vue-router', 'pinia'],
ui: ['element-plus'],
},
// 文件名包含 contenthash
chunkFileNames: 'assets/[name]-[hash].js',
entryFileNames: 'assets/[name]-[hash].js',
assetFileNames: 'assets/[name]-[hash].[ext]',
},
},
// CSS 代码分割
cssCodeSplit: true,
// 压缩
minify: 'terser',
terserOptions: {
compress: { drop_console: true, drop_debugger: true },
},
// chunk 大小警告阈值
chunkSizeWarningLimit: 500,
},
}));Source Map 管理策略
# 方案 1:生产环境不生成 Source Map
# 优点:部署简单
# 缺点:生产错误无法定位源码
# 方案 2:生成但不上传到 CDN
npm run build
# 将 .map 文件单独上传到错误监控服务(Sentry)
npx @sentry/cli sourcemaps upload ./dist --url-prefix '~/assets'
# 方案 3:使用 hidden source map
# 构建产物中不引用 source map,但生成 .map 文件供错误监控使用CDN 分发策略
静态资源上传到 CDN
# 使用 AWS S3 + CloudFront
# 1. 上传静态资源(带哈希文件名)— 设置长期缓存
aws s3 sync dist/assets/ s3://my-app-assets/assets/ \
--cache-control "public, max-age=31536000, immutable" \
--exclude "index.html"
# 2. 上传 HTML 入口 — 不缓存或短缓存
aws s3 cp dist/index.html s3://my-app/index.html \
--cache-control "no-cache, no-store, must-revalidate" \
--content-type "text/html; charset=utf-8"
# 3. 刷新 CDN 缓存(仅刷新 HTML)
aws cloudfront create-invalidation \
--distribution-id XXXXXXXXXX \
--paths "/index.html"Nginx 配置
server {
listen 80;
server_name www.example.com;
root /usr/share/nginx/html;
index index.html;
# 静态资源 — 长期缓存(文件名含哈希)
location /assets/ {
expires 1y;
add_header Cache-Control "public, immutable";
# 开启 gzip
gzip on;
gzip_types text/css application/javascript image/svg+xml;
}
# HTML 入口 — 不缓存
location / {
try_files $uri $uri/ /index.html;
add_header Cache-Control "no-cache, no-store, must-revalidate";
add_header Pragma "no-cache";
}
# API 反向代理
location /api/ {
proxy_pass http://backend:5000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
# 安全头
add_header X-Frame-Options "SAMEORIGIN";
add_header X-Content-Type-Options "nosniff";
add_header X-XSS-Protection "1; mode=block";
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' cdn.example.com;";
}CI/CD 自动化流水线
GitHub Actions 完整配置
# .github/workflows/deploy.yml
name: Deploy
on:
push:
branches: [main, release/*]
pull_request:
branches: [main]
env:
NODE_VERSION: '20'
REGISTRY: registry.example.com
jobs:
# 阶段 1:代码检查和测试
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- run: npm ci
- run: npm run lint
- run: npm run type-check # TypeScript 类型检查
- run: npm run test -- --coverage --ci
- name: 上传覆盖率
uses: codecov/codecov-action@v3
# 阶段 2:构建和部署
deploy:
needs: test
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: 设置版本号
run: echo "VERSION=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- run: npm ci
- run: npm run build
# E2E 测试
- name: 安装 Playwright
run: npx playwright install --with-deps
- name: 启动预览服务器
run: npx vite preview --port 4173 &
- name: 运行 E2E 测试
run: npx playwright test
# 上传到 CDN
- name: 配置 AWS 凭证
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ap-northeast-1
- name: 上传到 S3
run: |
aws s3 sync dist/ s3://my-app/${{ env.VERSION }}/ --delete
aws s3 cp s3://my-app/${{ env.VERSION }}/index.html s3://my-app/current/index.html
- name: 刷新 CDN
run: |
aws cloudfront create-invalidation \
--distribution-id ${{ secrets.CDN_DISTRIBUTION_ID }} \
--paths "/index.html"
# 上传 Source Map 到 Sentry
- name: 上传 Source Map
run: |
npx @sentry/cli sourcemaps upload ./dist \
--url-prefix 'https://www.example.com' \
--org my-org --project my-app
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
# 部署通知
- name: 发送部署通知
run: |
curl -X POST "${{ secrets.WEBHOOK_URL }}" \
-H "Content-Type: application/json" \
-d '{"text":"前端部署成功: ${{ env.VERSION }}"}'GitLab CI 配置
# .gitlab-ci.yml
stages:
- test
- build
- deploy
test:
stage: test
image: node:20
script:
- npm ci
- npm run lint
- npm run test -- --coverage
artifacts:
reports:
coverage_report:
coverage_format: cobertura
path: coverage/cobertura-coverage.xml
build:
stage: build
image: node:20
script:
- npm ci
- npm run build
artifacts:
paths:
- dist/
deploy_staging:
stage: deploy
image: alpine
needs: [build]
script:
- apk add --no-cache aws-cli
- aws s3 sync dist/ s3://my-app-staging/ --delete
environment:
name: staging
url: https://staging.example.com
only:
- develop
deploy_production:
stage: deploy
image: alpine
needs: [build]
script:
- apk add --no-cache aws-cli
- aws s3 sync dist/ s3://my-app-production/ --delete
environment:
name: production
url: https://www.example.com
when: manual # 手动触发
only:
- main版本管理与回滚策略
版本化部署
# 每次部署使用唯一的版本标识
VERSION=$(git rev-parse --short HEAD)
TIMESTAMP=$(date +%Y%m%d%H%M%S)
DEPLOY_ID="${VERSION}-${TIMESTAMP}"
# 上传到版本化目录
aws s3 sync dist/ s3://my-app/releases/${DEPLOY_ID}/
# 更新 current 指针(原子操作)
aws s3 cp s3://my-app/releases/${DEPLOY_ID}/index.html s3://my-app/current/index.html
# 保存版本记录
echo "${DEPLOY_ID} $(date -Iseconds) ${GIT_COMMIT_MESSAGE}" >> s3://my-app/deployments.log快速回滚
# 方案 1:CDN 回滚 — 更新指针指向上一版本
aws s3 cp s3://my-app/releases/PREVIOUS_VERSION/index.html s3://my-app/current/index.html
aws cloudfront create-invalidation --distribution-id XXXXX --paths "/index.html"
# 方案 2:保留最近 N 个版本,滚动回滚
#!/bin/bash
DEPLOYMENTS=$(aws s3 ls s3://my-app/releases/ | awk '{print $2}' | sort)
DEPLOY_ARRAY=($DEPLOYMENTS)
# 回滚到上一个版本
PREVIOUS=${DEPLOY_ARRAY[-2]}
aws s3 cp "s3://my-app/releases/${PREVIOUS}index.html" s3://my-app/current/index.html
# 方案 3:Docker 部署回滚
kubectl rollout undo deployment/my-app
kubectl rollout status deployment/my-app
# 方案 4:蓝绿部署
# 蓝:当前版本,绿:新版本
# 出问题时立即切回蓝色环境Docker 部署
# Dockerfile — 多阶段构建
# 阶段 1:构建
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# 阶段 2:Nginx 托管
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]# docker-compose.yml
version: '3.8'
services:
frontend:
build: .
ports:
- "80:80"
environment:
- API_BASE_URL=https://api.example.com
restart: unless-stopped灰度发布
Feature Flags 实现灰度
// featureFlags.ts
interface FeatureFlags {
newDashboard: boolean;
darkMode: boolean;
newEditor: boolean;
}
// 从服务端获取功能开关
async function getFeatureFlags(userId: string): Promise<FeatureFlags> {
const res = await fetch(`/api/features?userId=${userId}`);
return res.json();
}
// 在组件中使用
function Dashboard() {
const { user } = useAuth();
const { flags } = useFeatureFlags(user.id);
if (flags.newDashboard) {
return <NewDashboard />;
}
return <LegacyDashboard />;
}CDN 层面的灰度
# Nginx 按比例灰度
split_clients "${remote_addr}" $variant {
10% "new";
* "old";
}
location / {
if ($variant = "new") {
rewrite ^(.*)$ /v2/index.html break;
}
try_files $uri /index.html;
}监控与告警
性能监控
// 上报 Web Vitals
import { onCLS, onFID, onLCP, onFCP, onTTFB, onINP } from 'web-vitals';
function reportMetric(metric: any) {
// 上报到监控服务
const url = '/api/metrics';
const body = JSON.stringify({
name: metric.name,
value: metric.value,
rating: metric.rating, // 'good' | 'needs-improvement' | 'poor'
delta: metric.delta,
id: metric.id,
url: window.location.href,
timestamp: Date.now(),
});
if (navigator.sendBeacon) {
navigator.sendBeacon(url, body);
} else {
fetch(url, { body, method: 'POST', keepalive: true });
}
}
onCLS(reportMetric);
onFID(reportMetric);
onLCP(reportMetric);
onFCP(reportMetric);
onTTFB(reportMetric);
onINP(reportMetric);错误监控
// 全局错误捕获
window.addEventListener('error', (event) => {
reportError({
type: 'js_error',
message: event.message,
filename: event.filename,
lineno: event.lineno,
colno: event.colno,
stack: event.error?.stack,
});
});
window.addEventListener('unhandledrejection', (event) => {
reportError({
type: 'promise_rejection',
reason: event.reason,
});
});常见误区
- 构建产物没有压缩和 Tree Shaking — 首屏加载时间长
- HTML 文件也设置了长期缓存 — 用户无法获取最新版本
- 忘记刷新 CDN 缓存 — 发布后用户看到旧版本
- Source Map 上传到 CDN — 暴露源码
- 没有回滚机制 — 发布出问题无法快速恢复
排错清单
- 检查 CDN 缓存是否已刷新
- 确认构建产物包含最新代码(检查文件哈希是否变化)
- 检查 Source Map 是否正确上传到错误监控
- 确认 HTML 入口的 Cache-Control 是否为 no-cache
- 检查 Nginx 的 try_files 配置是否正确处理 SPA 路由
最佳实践总结
- 静态资源长期缓存 + HTML 不缓存 — 这是前端缓存的核心策略
- CI/CD 自动化 — lint → test → build → deploy 全流程自动化
- 版本化部署 — 每次部署保留版本记录,支持快速回滚
- 灰度发布 — 按比例逐步放量,降低发布风险
- 发布后监控 — 检查错误率、性能指标和用户反馈
