主题
第 25 章 · 部署与运维
一句话开篇:写完的代码就像做好的菜,部署 = 把菜端上桌、保证每个客人都吃到热乎的、不串味、出 bug 时还能立刻撤回。
0. 生活类比(先建立直觉)
把"前端项目部署"想象成开一家连锁奶茶店:
| 真实场景 | 奶茶店 |
|---|---|
| 静态部署平台 | 加盟连锁总部包装好的"开店套餐" |
| Vercel/Netlify | 高端连锁(VC 加持,一键开店) |
| Nginx 自建 | 自己租店面、装修、运营 |
| Docker | 标准化的"集装箱店面",搬到哪都能开 |
| CDN | 全国连锁分店(用户就近取货) |
| CI/CD | 中央厨房 → 自动配送 → 各分店上架 |
| 灰度发布 | 新品先在 5 家店试卖,没问题再全国推 |
| A/B 测试 | A 配方和 B 配方各发一半客人,看谁好评高 |
| 回滚 | 客户投诉新品不好喝,立刻换回旧品 |
| 监控告警 | 总部仪表盘,哪家店销量异常立刻报警 |
部署本质就 4 件事:把代码变成产物 → 把产物送到服务器 → 让用户访问 → 出事能撤回。
1. 部署方式总览
┌──────────────────────┐
│ 你的前端代码 │
└─────────┬────────────┘
│ npm run build
▼
┌──────────────────────┐
│ dist/ 静态产物 │
└─────────┬────────────┘
│
┌──────────────────────┼──────────────────────┐
▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ 静态平台 │ │ 自建 Nginx │ │ Docker 镜像 │
│ Vercel │ │ + CDN │ │ + K8s │
│ Netlify │ │ │ │ + Serverless │
│ Cloudflare │ │ │ │ │
│ GitHub Pages │ │ │ │ │
└──────────────┘ └──────────────┘ └──────────────┘
零运维 / 一键 中等成本 / 灵活 企业级 / 复杂
适合:个人 / 小团队 适合:中小项目 适合:大厂1.1 静态部署平台对比
| 平台 | 免费额度 | 优势 | 劣势 |
|---|---|---|---|
| Vercel | 100GB/月 | Next.js 亲爹、最快、Edge Func | 国内访问慢、商业用要付费 |
| Netlify | 100GB/月 | Form / Identity 一站式 | 国内访问慢 |
| Cloudflare Pages | 无限带宽 ✅ | 全球最快 CDN、Workers | UI 没那么友好 |
| GitHub Pages | 1GB / 100GB 月 | 免费、和 GitHub 集成 | 仅静态、国内极慢 |
| 腾讯 / 阿里 OSS + CDN | 按量付费 | 国内速度好 | 要自己配 CDN/HTTPS |
选型经验:
- 个人作品集/简历项目 → Cloudflare Pages(最快、免费、CN 也能访问)
- 国内商业项目 → 腾讯 EdgeOne / 阿里云 OSS + CDN
- 全球 SaaS/Next.js → Vercel
1.2 部署一个项目到 Vercel(手把手)
bash
# 1. 装 Vercel CLI
npm i -g vercel
# 2. 在项目根目录
vercel login
vercel # 跟着提示走,自动检测框架
# 3. 后续每次部署
vercel --prod或者最优方案:把代码推到 GitHub → 在 Vercel 后台 Import → 之后每次 git push 自动部署。
2. 自建 Nginx 部署
2.1 Nginx 是什么
Nginx = 高性能 反向代理 / Web 服务器 / 负载均衡器。
对前端的核心用途:
- 托管静态文件(HTML/CSS/JS/图片)
- 反向代理 API(前端请求
/api/*转发到后端) - gzip / brotli 压缩
- HTTPS 终止(前端拿证书,后端纯 HTTP)
- SPA history 模式 fallback
2.2 生产可用的完整配置
详见 examples/nginx.conf。核心结构:
nginx
server {
listen 80;
server_name app.example.com;
return 301 https://$host$request_uri; # HTTP 全部跳 HTTPS
}
server {
listen 443 ssl http2;
server_name app.example.com;
ssl_certificate /etc/nginx/certs/app.crt;
ssl_certificate_key /etc/nginx/certs/app.key;
root /var/www/app/dist;
index index.html;
# 关键 1:SPA history 模式 fallback
location / {
try_files $uri $uri/ /index.html;
}
# 关键 2:带 hash 的静态资源 → 强缓存 1 年
location ~* \.[a-f0-9]{8,}\.(js|css|png|jpg|webp)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# 关键 3:HTML → 不缓存
location ~* \.html$ {
add_header Cache-Control "no-cache";
}
# 关键 4:API 反向代理
location /api/ {
proxy_pass http://backend:8080/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
# 关键 5:gzip
gzip on;
gzip_types text/plain text/css application/json application/javascript;
}2.3 SPA history 模式必须 fallback
SPA 用 history 路由时,URL 是 /about、/users/123,但服务器上没有这些文件。直接刷新 → 404。
解决:所有不存在的路径都 fallback 到 index.html,让前端路由接管。
nginx
location / {
try_files $uri $uri/ /index.html;
}⚠️ 这是 SPA 部署最常见的坑,刚学的人 100% 踩。
3. CDN 原理与运营
3.1 工作原理
┌──────── 边缘节点(北京) ◄── 北京用户
源站 ──► │ ┌──── 边缘节点(上海) ◄── 上海用户
(你的服务器)│ │ ┌── 边缘节点(广州) ◄── 广州用户
│ │ │
└───┴─┴── 内容同步用户访问域名 → DNS 解析到最近的边缘节点 → 命中缓存直接返回,未命中回源。
3.2 三个核心运营动作
| 动作 | 含义 | 何时用 |
|---|---|---|
| 预热(Push) | 主动把资源推到所有边缘节点 | 大型发布前,避免首次回源风暴 |
| 刷新(Purge) | 把指定资源从所有节点驱逐 | 紧急修复(如错误图片) |
| 回源 | 边缘没命中时去源站拉取 | 自动行为 |
3.3 CDN 缓存策略
CDN 看的是响应头:
Cache-Control: public, max-age=31536000, immutablepublic:CDN 可缓存private:仅浏览器可缓存(CDN 会跳过)s-maxage=N:专给 CDN 的过期时间,覆盖 max-age
3.4 部署顺序的"教科书坑"
正确顺序:
1. 先发静态资源到 CDN(带 hash 的 JS/CSS)
2. 等 CDN 同步完
3. 再发 HTML 到源站错误顺序:
1. 先发 HTML(引用了新 hash)
2. 用户立刻请求 → CDN 上还没新 JS → 404更好的方案:配合 hash 文件名,新旧版本可以共存几小时。
4. Docker 入门
4.1 为什么要用 Docker
经典痛点:"在我电脑上能跑啊"。
Docker = 把"代码 + 依赖 + 操作系统"打成一个集装箱,搬到哪台机器都一样。
4.2 三大核心概念
| 概念 | 解释 | 类比 |
|---|---|---|
| Dockerfile | 食谱(构建说明书) | 烹饪菜谱 |
| 镜像 Image | 按食谱做出来的成品(不可变) | 罐头 |
| 容器 Container | 运行中的镜像实例 | 打开罐头吃 |
4.3 用 Nginx 镜像部署 SPA(标准模板)
Dockerfile(详见 examples/Dockerfile):
dockerfile
# ===== 阶段 1:构建(多阶段构建省镜像体积) =====
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# ===== 阶段 2:用 Nginx 当 web 服务器 =====
FROM nginx:1.25-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;"]构建 + 运行:
bash
# 构建镜像
docker build -t my-spa .
# 运行容器(80 → 8080 端口映射)
docker run -d -p 8080:80 --name web my-spa
# 访问 http://localhost:80804.4 多阶段构建的好处
| 单阶段 | 多阶段 |
|---|---|
| 1 个 image,包含 node_modules | 2 个阶段,最终 image 只有 nginx + dist |
| 几百 MB | 30 MB 左右 |
| 包含构建工具,攻击面大 | 干净 |
5. CI/CD 与 GitHub Actions
5.1 概念
- CI(Continuous Integration)持续集成:代码 push → 自动跑 lint / test / build
- CD(Continuous Deployment)持续部署:CI 通过 → 自动部署到环境
5.2 GitHub Actions 入门
仓库下 .github/workflows/deploy.yml,详见 examples/.github/workflows/deploy.yml。
简化版:
yaml
name: Deploy
on:
push:
branches: [main]
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '20', cache: 'npm' }
- run: npm ci
- run: npm run lint
- run: npm run test
- run: npm run build
- name: Deploy to S3 / OSS / SCP
run: |
# 用 aws-cli / coscmd / scp 上传 dist
echo "uploading..."5.3 完整的 CI/CD 流水线
git push origin main
│
▼
┌──────────────────┐
│ GitHub Actions │
│ ① checkout 代码 │
│ ② cache 依赖 │
│ ③ npm ci │
│ ④ lint + test │
│ ⑤ build │
│ ⑥ 上传产物 │
└─────────┬────────┘
│
▼
┌──────────────────┐ ┌──────────────────┐
│ 测试环境部署 │ ────► │ 测试环境冒烟测试 │
└─────────┬────────┘ └─────────┬────────┘
│ │ 通过
│ ▼
│ ┌──────────────────┐
│ │ 等待人工 approve │
│ └─────────┬────────┘
│ │
│ ▼
│ ┌──────────────────┐
└───────────────►│ 生产环境部署 │
│(灰度 → 全量) │
└──────────────────┘6. 灰度发布与 A/B 测试
6.1 灰度发布(Canary Release)
问题:直接 100% 上线新版本,万一有 bug,全员崩。
方案:先放给 1% 用户 → 观察 → 没问题再 5% → 20% → 100%。
实现方式:
| 维度 | 怎么做 |
|---|---|
| 按 IP 哈希 | Nginx split_clients,按 IP 算落到 A 或 B |
| 按 Cookie | 网关识别 Cookie 里的"灰度组",路由到对应版本 |
| 按地域 | CDN 边缘节点按用户 IP 地理位置切流 |
| 按用户 ID | 后端按 user_id % 100 < 5 命中灰度 |
Nginx 灰度示例:
nginx
# 5% 用户去新版本
split_clients "${remote_addr}${http_user_agent}" $version {
5% "v2";
* "v1";
}
upstream v1 { server 10.0.0.1; }
upstream v2 { server 10.0.0.2; }
server {
location / {
proxy_pass http://$version;
}
}6.2 A/B 测试
灰度发布是为了"安全发布",A/B 测试是为了"验证假设"。
| 对比项 | 灰度发布 | A/B 测试 |
|---|---|---|
| 目的 | 风险控制 | 数据驱动决策 |
| 比例 | 慢慢放量到 100% | 长期 50/50(直到出结论) |
| 关注指标 | 错误率、性能 | 转化率、CTR、留存 |
工具:Google Optimize(已停)、Optimizely、自建(埋点 + 统计平台)。
7. 监控与回滚
7.1 监控三大支柱
┌────────────────────────────────────────────┐
│ Logs 日志 什么时候出了什么事 │
│ Metrics 指标 PV / 错误率 / 性能 / 资源│
│ Traces 链路追踪 一个请求经过哪些服务 │
└────────────────────────────────────────────┘前端常用工具:Sentry(错误监控)、SkyWalking/Datadog(链路追踪)、自建埋点 + 数仓。
7.2 关键告警
| 指标 | 阈值 | 含义 |
|---|---|---|
| 5xx 错误率 | > 1% | 服务端可能挂了 |
| LCP P75 | > 4s | 用户感知到的加载明显变慢 |
| JS Error 率 | > 0.5% | 新版有 bug |
| API 超时率 | > 0.5% | 上游慢 |
7.3 回滚
最快的回滚:保留上一版本的产物,把 CDN/Nginx 指回去。
GitHub Actions 配置时,每次发布带版本号:
/cdn/dist/v20251018-1023/ ←── 本次发布
/cdn/dist/v20251015-0912/ ←── 上一版本(保留 7 天)回滚 = 把 /cdn/dist/current 软链指回上一版本。
8. ⚠️ 易踩的坑
SPA 部署没配 fallback:刷新
/about→ 404。永远记得try_files $uri /index.html。HTML 也设强缓存:发了新版本用户死活刷不出来。HTML 必须
no-cache。部署顺序反了:先发 HTML 再发静态资源 → 用户拿到的 HTML 引用了 CDN 还没同步的 JS → 404。
Docker 镜像里包含 .env:把生产数据库密码打进了镜像,传到了公共仓库。
.dockerignore必加.env*、node_modules。Nginx 没开 gzip:白白浪费 70% 带宽。
HTTPS 证书过期:90 天的 Let's Encrypt 没自动续,凌晨告警。用
certbot --renew --auto或交给 Vercel/CF 全托管。CDN 跨域:图片在 CDN 域名上,
<canvas>想读图发现 CORS 报错。crossorigin="anonymous"+ CDN 配Access-Control-Allow-Origin: *。GitHub Actions 没 cache:每次
npm install跑 5 分钟。开cache: 'npm'或actions/cache@v4。生产环境 console.log / source-map 全暴露:生产构建务必移除 console、不传 sourcemap 到 CDN(保留私有上传 Sentry)。
CI 里
npm install:会改 lockfile。永远用npm ci严格按锁文件装。回滚预案没演练:真出事 30 分钟搞不定。每月演练一次"5 分钟回滚"。
环境变量在前端打包时硬编码:
VITE_API_URL=https://api.dev.com打进了生产产物。注意构建时区分环境,或运行时通过<script>window.__CONFIG__ = ...</script>注入。
9. 一句话总结
部署 = 把代码变成可访问的服务:静态平台(Vercel/Cloudflare)一键搞定个人项目,Nginx 自建适合中小公司,Docker + CI/CD 是工业标准;CDN 让全球用户都快,灰度发布让上线不翻车,监控 + 回滚是兜底——这一章决定了你写的代码能不能"活"在线上。
10. 延伸阅读
- nginx 官方文档
- Docker 官方教程
- GitHub Actions 文档
- 12-Factor App — 现代部署的圣经
- Cloudflare Pages 文档
- 《SRE:Google 运维解密》