Skip to content
前端部署
概述
前端部署解决的问题,不只是“将文件传输到服务器”这一环,而是在持续迭代的线上环境里,使每次发布变得可控、可验证、可回滚。
所有前端部署可以抽象为同一模型:
text
构建产物(静态文件)→ 分发管道 → 边缘节点 → 用户浏览器
↑ ↑
构建阶段 缓存策略
- 编译/打包 - CDN 缓存
- hash 命名 - 浏览器缓存
- 代码分割 - Service Worker该模型的三个核心变量:
- 产物的唯一性:构建产物的文件名是否能唯一对应一次提交?(hash 命名)
- 分发的原子性:版本切换时,用户是否会加载新旧混合的产物?(缓存失效窗口)
- 回滚的速度:从发现问题到用户恢复正确版本的路径有多长?(CDN 刷新 vs 版本回退)
这三个变量直接决定部署系统在持续变更下的可靠性。
构建产物
文件命名与 content hash
前端部署中对静态资源命名方式的选择,影响缓存策略能否正确执行。
js
// 三种常见命名
bundle.js // 无版本信息
bundle.1.2.3.js // 语义版本,手动维护不可靠
bundle.a3f8b2c1.js // 基于内容的 hash,内容变则文件名变基于内容生成的文件名(content hash)可以让同名约定发生语义变化:文件内容不变,文件名就不变;内容变化,文件名必然变化。这是对资源设置长期强缓存的前提。
具体构建工具中的注意事项:
- Webpack 的三种 hash:
hash(整个项目共用,改一个文件导致全部 hash 变更)、chunkhash(每个 chunk 独立 hash)、contenthash(基于文件内容)。对于需要长期缓存的资源,应当使用contenthash。 - Vite / Rollup 在生产构建默认使用 content hash,不需要额外配置。
- CSS 文件的 hash 会受引入的资源(字体、图片)影响,这些资源 hash 的改变会连带更新 CSS 的 hash。Webpack 可以通过
MiniCssExtractPlugin+contenthash处理该场景。
缓存策略分配
不同资源类型采用不同的缓存 header:
| 资源类型 | 缓存策略 | Header 配置 | 原因 |
|---|---|---|---|
*.a3f8b2c1.js | 永久缓存 | max-age=31536000, immutable | hash 保证唯一性 |
*.a3f8b2c1.css | 永久缓存 | max-age=31536000, immutable | 同上 |
index.html | 不缓存 / 协商缓存 | Cache-Control: no-cache 或 max-age=0 | HTML 是入口,必须每次验证 |
| 图片/字体(带 hash) | 永久缓存 | max-age=31536000, immutable | hash 保证唯一性 |
| 图片/字体(无 hash) | 短期缓存 | max-age=86400 | 内容可能变化 |
HTML 文件不应被长时间缓存。因为 JS/CSS 的文件名(含 hash)每次构建都可能变化,如果 HTML 被缓存,用户拿到的旧 HTML 将继续引用旧资源,而这些旧资源可能已被新的部署覆盖,引发 404 或白屏。
注意
如果 HTML 被 CDN 缓存较长时间(例如 max-age=600),同时静态资源又未使用 content hash 命名,那么先部署静态资源再更新 HTML 时,窗口期内用户可能加载缓存的旧 HTML 去请求已被覆盖的旧文件,导致资源加载失败。规避这类问题的方式是:资源使用 content hash,HTML 缓存策略设为 no-cache。
一种常见架构是:HTML 由源站(应用服务器或反向代理)直接服务,不经过 CDN 缓存;JS/CSS/图片等走 CDN 加速。
构建产物目录结构
text
dist/
├── index.html # 入口 HTML,由源站服务
├── assets/
│ ├── js/
│ │ ├── main.a3f8b2.js # 主入口(hash)
│ │ ├── vendor.b7c3d1.js # 第三方库(独立 chunk)
│ │ └── runtime.e9f4a5.js
│ ├── css/
│ │ └── main.c2d8e1.css
│ └── images/
│ └── logo.f1a2b3.svg
└── robots.txt设计原则:
- 入口 HTML 在根目录,便于源站直接响应。
- 静态资源集中在
assets/,整个目录可以推送至 CDN 源站(OSS/S3)。 - 全部资源带 content hash,允许永久缓存。
vendor独立分包,利用浏览器缓存差异:第三方库变更频率远低于业务代码,用户升级业务代码时无需重新下载 vendor。
CDN 分发
分层缓存模型
CDN 是一个多层缓存结构,而不仅是加速服务器:
text
用户请求
│
▼
L1: 浏览器缓存(Cache-Control, Service Worker)
│ (miss)
▼
L2: CDN 边缘节点
│ (miss)
▼
L3: CDN 区域节点
│ (miss)
▼
L4: 源站(服务器/OSS)多数 CDN 供应商(如阿里云 CDN、Cloudflare、AWS CloudFront)遵循这一分层。影响线上体验的关键指标是 缓存命中率,边缘节点命中率通常 > 95%。
CDN 缓存配置
CDN 上的缓存需要同时满足加速(缓存尽量久)与可控(更新可生效)。
- 带 content hash 的资源:设置为永久缓存(例如 365 天)。文件名变更相当于新文件,不存在“更新”冲突。
- HTML 文件:如果经 CDN 分发,应配置合理的缓存键(至少包含
Host和URL Path),回源 TTL 设为较短时间(如 60s)或no-cache。
缓存刷新
部署流程中,CDN 缓存刷新(purge / invalidate)主要针对 HTML 入口:
text
构建完成 → 上传静态资源到 CDN 源站(OSS/S3)
→ 发布 HTML(新版本 HTML 引用新 hash 资源)
→ 必要时刷新 CDN 上 HTML 的缓存带 hash 的静态资源不需要刷新,因为其 URL 是全新的。这正是 content hash 命名的架构优势:部署由“覆盖文件”变为“新增文件 + 修改 HTML 引用”,旧版本文件可在 CDN 上自然过期。
多级 CDN 架构
大型应用可能使用两级 CDN:
text
用户 → 第三方 CDN(Cloudflare / Akamai,全球加速)
│
▼
自有 CDN(阿里云 CDN / 腾讯云 CDN,国内加速)
│
▼
源站 OSS(存储构建产物)双层架构会带来额外成本,但对于高流量业务,首屏加载时间的每一点下降都能转化为可量化的业务指标提升。
CDN 使用中的注意事项
- 回源超时:CDN 回源时源站响应过慢,边缘节点返回 504。建议 CDN 回源超时时间 ≥ 源站正常响应 P99 + 2 倍余量。
- 缓存雪崩:同一时间大量缓存同时过期,请求穿透至源站。可对不同资源的 TTL 加随机偏移。
- 错误状态码被缓存:源站短暂故障返回 5xx,CDN 可能缓存该错误响应,导致故障恢复后用户仍看到错误页面。应当配置 CDN 的状态码缓存规则,只缓存 2xx/3xx 响应,不缓存 4xx/5xx。此外可配合回源成功率告警,当成功率骤降时能及时发现。
- 跨域:CDN 域名与主站域名不同会引起 CORS 问题。需在 CDN 源站(OSS)设置 CORS 头,并让 CDN 转发这些头。
Nginx 静态资源服务与反向代理
角色
Nginx 在前端部署中承担以下一项或多项角色:
- 静态资源服务器:直接分发构建产物。
- 反向代理:将请求转至 Node.js 应用(如 SSR)。
- 网关:路由分发、限流、认证、日志。
- 负载均衡:将流量分配至多个应用实例。
SPA 部署配置示例
nginx
server {
listen 443 ssl http2;
server_name example.com;
ssl_certificate /etc/nginx/ssl/fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/privkey.pem;
root /var/www/app/dist;
# Gzip
gzip on;
gzip_comp_level 6;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml;
gzip_min_length 1000;
gzip_vary on;
# 静态资源(带 hash):永久缓存
location /assets/ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# HTML:不缓存
location / {
try_files $uri $uri/ /index.html;
add_header Cache-Control "no-cache, no-store, must-revalidate";
add_header Pragma "no-cache";
add_header Expires "0";
}
# 安全头
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
}要点:
try_files $uri $uri/ /index.html是 SPA 路由核心配置:先尝试匹配文件,失败则回退到 index.html,交由前端路由接管。expires 1y和immutable告诉浏览器与中间代理该资源在一年内可直接使用缓存,无需验证,适用于带 content hash 的资源。X-Frame-Options防止点击劫持,X-Content-Type-Options阻止 MIME 嗅探。
API 网关配置
nginx
server {
listen 443 ssl http2;
server_name example.com;
# 前端
location / {
root /var/www/app/dist;
try_files $uri $uri/ /index.html;
}
# API 代理
location /api/ {
proxy_pass http://backend-service: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 10s;
proxy_send_timeout 30s;
proxy_read_timeout 30s;
proxy_buffering on;
proxy_buffer_size 4k;
proxy_buffers 8 4k;
}
}静态资源与 API 在相同域名下,避免跨域问题。
Docker 部署
适用场景
当部署需求不仅仅是处理静态文件时,Docker 能提供统一的环境和交付物。常见的引入场景包括:需要多环境一致性、SSR 应用的进程管理、微前端或 BFF 层的服务治理,以及需要水平扩展。
纯静态 SPA 多阶段构建
dockerfile
# 阶段 1:构建
FROM node:20-alpine AS builder
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN corepack enable && pnpm install --frozen-lockfile
COPY . .
RUN pnpm build
# 阶段 2:运行
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;"]多阶段构建的最终镜像只包含 nginx + 构建产物,大小约 10-15 MB,不包含 node_modules 或构建工具链。
SSR 应用
dockerfile
FROM node:20-alpine AS builder
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN corepack enable && pnpm install --frozen-lockfile
COPY . .
RUN pnpm build
FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/.output /app/.output
EXPOSE 3000
ENV NODE_ENV=production
CMD ["node", ".output/server/index.mjs"]要点:
--frozen-lockfile保证 CI 环境中不会意外修改 lockfile。.dockerignore需排除node_modules、.git、dist等,减小构建上下文。- 使用 Node.js 镜像时,可通过
USER node切换为非 root 用户运行。
Docker Compose 编排
yaml
version: '3.8'
services:
frontend:
build: .
ports:
- "80:80"
restart: unless-stopped
environment:
- NODE_ENV=production
networks:
- app-network
backend:
image: my-backend:latest
ports:
- "3000:3000"
restart: unless-stopped
networks:
- app-network
networks:
app-network:
driver: bridgerestart: unless-stopped 确保容器在宿主重启后自动拉起。
关于不可变基础设施
即使不使用 Docker,也应贯彻“构建一次,到处运行”的思路:部署到服务器的是一份完整的环境快照(包含运行时、依赖和配置),而非零散的文件与手工修改的配置。
CI/CD 流水线
流水线结构
text
Git Push → CI Pipeline
├── 1. Lint & Type Check
├── 2. Unit Test
├── 3. Build
├── 4. E2E Test ← 可并行
├── 5. Bundle Analysis ← 可并行
└── 6. Deploy
├── 上传静态资源到 OSS
├── 刷新 CDN(仅 HTML)
└── 部署通知设计原则:
- 容易失败的步骤前置,快速退出。
- 无依赖的步骤并行执行。
- 只有全部前置步骤通过才触发部署。
GitHub Actions 示例
yaml
name: Deploy
on:
push:
branches: [main]
jobs:
quality:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v2
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- run: pnpm install --frozen-lockfile
- run: pnpm lint
- run: pnpm typecheck
- run: pnpm test
build-and-deploy:
needs: quality
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v2
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- run: pnpm install --frozen-lockfile
- run: pnpm build
# 上传静态资源到 OSS
- name: Upload to OSS
uses: manyuanrong/setup-ossutil@v3
with:
endpoint: oss-cn-shenzhen.aliyuncs.com
access-key-id: ${{ secrets.OSS_ACCESS_KEY_ID }}
access-key-secret: ${{ secrets.OSS_ACCESS_KEY_SECRET }}
- run: |
ossutil cp -r dist/assets/ oss://my-bucket/assets/ --update
# 部署 HTML
- name: Deploy HTML
uses: appleboy/scp-action@v0.1.7
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USER }}
key: ${{ secrets.SERVER_SSH_KEY }}
source: dist/index.html
target: /var/www/app/
# 刷新 CDN
- name: Purge CDN
run: |
curl -X POST "https://cdn.aliyuncs.com/..." \
-d "ObjectPath=https://example.com/index.html"
# 通知
- name: Notify
run: |
curl -X POST "${{ secrets.WEBHOOK_URL }}" \
-H "Content-Type: application/json" \
-d '{"text":"✅ 部署成功: '${GITHUB_SHA:0:7}'"}'密钥与缓存
- 密钥不得出现在 workflow 文件中,应通过 GitHub Secrets 或类似机制注入。
actions/setup-node@v4配合cache: 'pnpm'可缓存包存储,对于大中型项目能节省 60-80% 的安装时间。- 区分 staging 与 production 流水线,staging 由 feature branch 触发,production 由 main branch 触发。
部署策略
策略对比
| 策略 | 原理 | 回滚速度 | 风险暴露面 | 适用场景 |
|---|---|---|---|---|
| 全量部署 | 所有用户同时看到新版本 | 数分钟(CDN 刷新) | 100% | 低风险变更 |
| 蓝绿部署 | 两套环境切换 | < 1 min(流量切换) | 50%(切换瞬间 100%) | 重大版本升级 |
| 金丝雀部署 | 逐步增加新版本流量比例 | < 1 min | 1% → 5% → 25% → 100% | 需验证的变更 |
| 灰度发布 | 按用户属性分流 | < 1 min | 按灰度比例 | 定向验证 |
部署速度和部署安全存在天然张力。更快的部署往往跳过部分验证环节。解决方向不是跳过验证,而是通过自动化测试、健康检查和指标对比,让验证更快完成。
金丝雀部署(Nginx 分流)
前端静态文件的“金丝雀”需要 Nginx 层分流:
nginx
# 通过 cookie 分流
map $cookie_canary $backend {
"true" "new-version-server";
default "stable-server";
}
server {
location / {
proxy_pass http://$backend;
}
}也可以按 IP + User-Agent 的 hash 分流 10% 流量:
nginx
split_clients "${remote_addr}${http_user_agent}" $variant {
10% "new-version-server";
* "stable-server";
}split_clients 保证同一用户不被分配到不同版本,避免刷新时跳变。
蓝绿部署
蓝绿部署保留两套完整环境,通过 Nginx 切换。
部署脚本核心逻辑:
bash
#!/bin/bash
# 1. 确定当前环境
CURRENT=$(readlink /var/www/current)
if [ "$CURRENT" = "/var/www/blue" ]; then
TARGET="green"
else
TARGET="blue"
fi
# 2. 部署到空闲环境
rsync -avz --delete dist/ /var/www/$TARGET/
# 3. 健康检查
curl -f http://localhost:8000/health || exit 1
# 4. 切换
ln -sfn /var/www/$TARGET /var/www/current
nginx -s reload
# 5. 验证后确认,否则回切核心思想:始终保留上一个可用版本;切换成本是一次软链接更新。
环境配置
配置注入方式
构建时注入
js
// vite.config.js
export default defineConfig(({ mode }) => ({
define: {
__API_BASE_URL__: JSON.stringify(
mode === 'production' ? 'https://api.example.com' : 'http://localhost:3000'
),
},
}))简单,但每个环境需要单独构建,产物不通用。
运行时注入
在 HTML 中放置占位符,由启动脚本替换:
html
<script>
window.__APP_CONFIG__ = {
API_BASE_URL: '__API_BASE_URL__',
ENV: '__ENV__',
};
</script>bash
# docker-entrypoint.sh
sed -i "s|__API_BASE_URL__|${API_BASE_URL}|g" /usr/share/nginx/html/index.html优点:同一份构建产物(同一个 Docker 镜像)可以部署到任意环境,staging 验证通过后直接推广至线上,避免构建不一致。
配置服务
通过 Consul、Nacos 等独立服务拉取配置,适合多项目共享或配置变更频繁的场景。
安全管理
- 所有敏感信息(API Key、Sentry DSN 等)不得硬编码或提交到 Git。
- 通过 CI/CD 的 Secrets 机制注入。
- 定期轮换密钥,特别是人员变动时。
- 线上与 staging 环境的密钥严格隔离。
- 任何会打入客户端 bundle 的值都不应是秘密。例如不能将
API_KEY通过构建注入到前端代码中,否则会暴露在浏览器端。
监控与回滚
部署后验证指标
部署完成的标志是线上用户实际体验正常。需关注的指标:
| 指标 | 来源 |
|---|---|
| JS 错误率 | Sentry / 自建埋点 |
| 首屏加载时间(P95) | RUM |
| 页面可用率 | 拨测 |
| API 成功率 | 网关日志 |
| 静态资源加载成功率 | CDN 日志 |
部署后应持续观察至少 15 分钟的指标。部署流程应当是一个闭环:部署 → 验证 → 正常(结束)/ 异常(回滚)。
健康检查流程
text
部署完成
├── 30s:检查 HTML 是否更新(比对 ETag)
├── 1min:关键页面是否返回 200
├── 3min:Sentry 错误率与部署前对比
├── 5min:RUM 首屏时间与页面成功率
└── 15min:用户反馈渠道回滚方案
回滚不应依赖“重新部署一次旧版本”。快速回滚方案包括:
方案 A:Nginx 快速回滚(< 30 秒)
保留上一个版本的构建产物,通过软链接切换:
bash
ln -sfn /var/www/last-stable /var/www/current && nginx -s reload保留最近 N 个版本(如 5 个),用磁盘空间换取回滚稳定性。
方案 B:CDN 回滚(< 5 分钟)
重新发布上一个版本的 HTML 并刷新 CDN。
方案 C:Docker / K8s 回滚
docker service rollback 或 kubectl rollout undo。
回滚能力通常比部署速度更重要:系统能在 30 秒内恢复比 2 分钟内完成部署更具价值。
部署通知
通知必须包含:环境、版本号(git SHA)、变更摘要、部署耗时、回滚命令。
bash
curl -X POST "$WEBHOOK_URL" \
-H "Content-Type: application/json" \
-d '{
"env": "production",
"version": "'${GIT_SHA:0:7}'",
"changelog": "修复首页加载(#234)",
"duration": "8min 32s",
"status": "success",
"rollback_command": "/deploy-rollback production"
}'部署失败的通知比成功的通知重要得多,其中应包含失败步骤、关键错误日志及变更差异链接。
部署检查清单
在每次部署线上前可逐项确认:
text
□ JS/CSS 文件名包含 content hash
□ HTML 未被 CDN 长时间缓存
□ Gzip / Brotli 压缩已启用
□ SSL 证书未在 30 天内到期
□ 敏感信息未提交到 Git
□ Docker 镜像大小合理(纯静态 < 100MB,SSR < 300MB)
□ Dockerfile 使用多阶段构建
□ CI 流水线使用 --frozen-lockfile
□ 部署后有自动健康检查
□ 保留上一版本的产物,可在 30 秒内回滚
□ Sentry 或同等工具已配置
□ 部署通知附带回滚命令
□ .dockerignore 排除了 node_modules 和 .git
□ CDN 配置了正确的 CORS 头
□ CDN 不缓存 4xx/5xx 响应