本教程参考 山月 · 前端部署十五章 体系编写,涵盖从静态资源服务到 CI/CD 完整流程,帮助前端工程师掌握部署技能。
前端部署的本质,就是对静态资源的服务。无论 React/Vue 构建产物,还是纯 HTML 页面,都需要通过 HTTP 服务器对外提供访问。
不使用任何框架,用 Node.js 几行代码即可搭建静态服务器:
// server-fs.js
const http = require('http')
const fs = require('fs')
const path = require('path')
const server = http.createServer((req, res) => {
const filePath = path.join(__dirname, req.url === '/' ? 'index.html' : req.url)
const ext = path.extname(filePath)
const contentTypes = {
'.html': 'text/html',
'.js': 'application/javascript',
'.css': 'text/css',
'.png': 'image/png',
'.json': 'application/json',
}
fs.readFile(filePath, (err, data) => {
if (err) {
res.writeHead(404)
res.end('Not Found')
return
}
res.writeHead(200, { 'Content-Type': contentTypes[ext] || 'text/plain' })
res.end(data)
})
})
server.listen(3000, () => console.log('Server running on port 3000'))运行:
node server-fs.js
# 或者使用 serve 工具
npx serve .但这种方式只适合本地开发,生产环境需要 Nginx。
Nginx 是生产环境最常用的静态资源服务器,具备高性能、高并发、占用资源少等特点。
# macOS
brew install nginx
# Ubuntu/Debian
sudo apt install nginx
# Docker (推荐,快速体验)
docker run -d --rm -p 3000:80 nginx:alpine# deploy/nginx.conf
server {
listen 80;
server_name localhost;
# 静态文件目录
root /usr/share/nginx/html;
index index.html;
location / {
# 尝试读取文件,未找到则返回 index.html (SPA 需要)
try_files $uri $uri/ /index.html;
}
}启动服务:
# Docker 方式
docker run -d --rm -p 3000:80 \
-v $(pwd)/dist:/usr/share/nginx/html \
-v $(pwd)/nginx.conf:/etc/nginx/conf.d/default.conf \
nginx:alpine
# 验证配置语法
nginx -t
# 重载配置
nginx -s reload缓存是前端部署最重要的性能优化手段之一。合理设置缓存,可大幅减少重复请求、加快页面加载。
| 策略 | 配置 | 适用场景 |
|---|---|---|
| 不缓存 | Cache-Control: no-cache, no-store, must-revalidate |
HTML 文件(频繁更新) |
| 协商缓存 | Cache-Control: no-cache |
JS/CSS(配合 ETag) |
| 强缓存 | Cache-Control: public, max-age=31536000 |
静态资源(带 hash 哈希) |
Nginx 配置示例:
server {
root /usr/share/nginx/html;
expires 1y; # 静态资源缓存 1 年(前提是文件名带 hash)
}Nginx 默认开启 ETag,会根据文件内容生成唯一标识:
# 请求示例
curl -I http://localhost/index.js
# ETag: "abc123"
# Cache-Control: no-cache
# 再次请求,带上 ETag
curl -I -H "If-None-Match: \"abc123\"" http://localhost/index.js
# 命中协商缓存,返回 304 Not Modified构建产物文件名带上内容 hash(如 app.abc123.js),设置 max-age=31536000:
# 针对带 hash 的静态资源(JS/CSS/图片),设置一年缓存
location ~* \.[a-f0-9]{8,}\.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# HTML 必须每次拉取最新版本
location ~* \.html$ {
expires -1;
add_header Cache-Control "no-cache, no-store, must-revalidate";
}山月金句:静态资源加 hash、接口加版本、HTML 文件不加缓存,这是前端缓存的三大原则。
单页应用(SPA)所有路由都由前端控制,Nginx 需要将所有路径重定向到 index.html。
server {
listen 80;
server_name example.com;
root /usr/share/nginx/html;
index index.html;
# SPA 路由支持:所有路径回退到 index.html
location / {
try_files $uri $uri/ /index.html;
}
# API 请求代理(如果前后端同域部署)
location /api/ {
proxy_pass http://backend:8080/;
}
}关键点:
try_files $uri $uri/ /index.html是 SPA 部署的核心配置,确保刷新页面不会 404。
Docker 容器化是现代部署的基础技能,核心价值:"Build Once, Run Anywhere"。
适用于纯静态资源,无需构建步骤:
# static.Dockerfile
FROM nginx:alpine
COPY dist/ /usr/share/nginx/html/
COPY nginx.conf /etc/nginx/conf.d/default.conf分离构建层与运行时层,大幅减小镜像体积:
# Dockerfile (多阶段构建)
# ---- 构建阶段 ----
FROM node:18-alpine AS builder
WORKDIR /code
# 分离 package.json,最大限度利用 Docker 层缓存
COPY package.json yarn.lock ./
RUN yarn install
COPY . .
RUN npm run build
# ---- 运行阶段 ----
FROM nginx:alpine
# 从构建阶段复制产物
COPY --from=builder /code/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]构建并运行:
docker build -t my-app .
docker run -d --rm -p 3000:80 my-appdocker-compose.yaml 是 Docker Compose 的配置文件,可同时管理多个容器。
# docker-compose.yaml
version: "3"
services:
# 前端开发服务器
dev:
image: node:18-alpine
working_dir: /app
volumes:
- .:/app
ports:
- "5173:5173"
command: npm run dev -- --host
# 模拟后端 API
api:
image: express:alpine
ports:
- "3000:3000"
volumes:
- ./api:/app
command: node index.js# 启动所有服务
docker-compose up
# 后台运行
docker-compose up -d
# 查看日志
docker-compose logs -f# docker-compose.prod.yaml
version: "3"
services:
web:
build:
context: .
dockerfile: Dockerfile
ports:
- "80:80"
environment:
- NODE_ENV=production
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost/"]
interval: 30s
timeout: 10s
retries: 3# 生产环境启动
docker-compose -f docker-compose.prod.yaml up -d --build把不常变化的依赖层分离出来,优先复制:
# ✅ 正确:先复制依赖文件,安装后再复制源码
COPY package.json yarn.lock ./
RUN yarn install
COPY . .
# ❌ 错误:每次代码变更都会导致依赖重新安装
COPY . .
RUN yarn install| 镜像 | 大小 | 适用场景 |
|---|---|---|
node:18-alpine |
~180MB | Node.js 构建 |
nginx:alpine |
~30MB | 静态资源服务 |
node:18-slim |
~900MB | 需要更多 GNU 工具 |
减少镜像层数,每层都会占用存储空间:
# ✅ 合并多个 RUN,减少层数
RUN apt-get update && \
apt-get install -y curl && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
# ❌ 分开写会产生多层
RUN apt-get update
RUN apt-get install -y curl排除不需要打包进镜像的文件:
node_modules
.git
dist
*.log
.env*
!.env.example
以下是一个生产级 SPA 完整配置:
# ---- 阶段一:构建 ----
FROM node:18-alpine AS builder
WORKDIR /app
# 利用 Docker 层缓存:依赖层优先复制
COPY package.json yarn.lock ./
RUN yarn install --frozen-lockfile
COPY . .
# 构建时注入环境变量
ARG VITE_API_BASE_URL
ENV VITE_API_BASE_URL=$VITE_API_BASE_URL
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
# 安全加固:非 root 用户
RUN chown -R nginx:nginx /usr/share/nginx/html && \
chmod -R 755 /usr/share/nginx/html
USER nginx
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]构建命令:
docker build \
--build-arg VITE_API_BASE_URL=https://api.example.com \
-t my-spa-app:latest .CI/CD(持续集成 / 持续部署)是现代开发标配,核心目标:代码提交后自动构建、自动测试、自动部署。
GitHub Actions 是 GitHub 内置的 CI/CD 工具,免费额度充足(2000分钟/月)。
- Workflow:一个 YAML 配置文件,定义一次 CI/CD 流程
- Job:一组 Step,可并行或串行执行
- Step:具体步骤(Checkout 代码、安装依赖、运行命令等)
- Action:可复用的 Step 单元
# .github/workflows/ci.yaml
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: '18'
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test以下是一个包含构建、测试、Docker 构建、部署的完整 Workflow:
# .github/workflows/deploy.yaml
name: Build, Test & Deploy
on:
push:
branches: [main]
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
# ---- Job 1: 构建与测试 ----
build-and-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: '18'
cache: 'npm'
cache-dependency-path: yarn.lock
- name: Install dependencies
run: yarn install --frozen-lockfile
- name: Lint
run: npm run lint
- name: Run tests
run: npm run test:ci
- name: Build
run: npm run build
- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: dist
path: dist/
retention-days: 7
# ---- Job 2: 构建 Docker 镜像 ----
build-image:
needs: build-and-test
runs-on: ubuntu-latest
outputs:
image-tag: ${{ steps.meta.outputs.tags }}
steps:
- uses: actions/checkout@v4
- name: Download build artifacts
uses: actions/download-artifact@v4
with:
name: dist
path: dist/
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/${{ github.repository }}
tags: |
type=sha,prefix=
type=raw,value=latest
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
cache-from: type=gha
cache-to: type=gha,mode=max
# ---- Job 3: 部署到服务器 ----
deploy:
needs: build-image
runs-on: ubuntu-latest
steps:
- name: Deploy to server
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USER }}
key: ${{ secrets.SERVER_SSH_KEY }}
script: |
docker pull ${{ needs.build-image.outputs.image-tag }}
docker-compose -f docker-compose.prod.yaml up -d
docker image prune -f| 类型 | 设置位置 | 示例 |
|---|---|---|
| 构建时变量 | env 或 arg |
VITE_API_BASE_URL |
| 运行时变量 | docker -e 或 .env |
数据库连接串 |
| 敏感信息 | GitHub Secrets | SECRET_KEY |
- 进入仓库 → Settings → Secrets and variables → Actions
- 添加以下 Secrets:
SERVER_HOST:服务器 IP 或域名SERVER_USER:SSH 用户名SERVER_SSH_KEY:私钥(注意换行符)
- name: Deploy
env:
# 直接注入环境变量
API_SECRET: ${{ secrets.API_SECRET }}
run: |
curl -X POST https://api.example.com/deploy \
-H "Authorization: Bearer $API_SECRET"- name: Build
run: npm run build
env:
VITE_API_BASE_URL: ${{ vars.VITE_API_BASE_URL }}
VITE_VERSION: ${{ github.sha }}生产环境出问题需要快速回滚,以下是几种回滚策略:
# 回滚到上一个稳定版本
deploy:
runs-on: ubuntu-latest
steps:
- name: Rollback to previous image
run: |
docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:stable
docker tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:stable my-app:latest
docker-compose -f docker-compose.prod.yaml up -d# 同时启动蓝绿两套环境,通过切换 nginx upstream 实现瞬时切换
services:
blue:
image: my-app:blue
networks:
- app-net
green:
image: my-app:green
networks:
- app-net
nginx:
image: nginx:alpine
volumes:
- ./nginx-switch.sh:/switch.sh
networks:
- app-net# .github/workflows/rollback.yaml
name: Rollback
on:
workflow_dispatch:
inputs:
tag:
description: 'Image tag to rollback to'
required: true
jobs:
rollback:
runs-on: ubuntu-latest
steps:
- name: Deploy to server
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USER }}
key: ${{ secrets.SERVER_SSH_KEY }}
script: |
docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.event.inputs.tag }}
docker-compose -f docker-compose.prod.yaml up -d
echo "Rolled back to ${{ github.event.inputs.tag }}"山月经验:生产部署一定要有回滚方案,宁可回滚到上一个稳定版本,也不要在线上排查问题。记住:Deploy Fast, Rollback Faster。
生产环境最常见的部署方式:Docker 运行 Nginx 容器。
version: "3"
services:
web:
build:
context: .
dockerfile: Dockerfile
ports:
- "80:80"
- "443:443"
volumes:
# 挂载 SSL 证书(生产环境)
- ./certs:/etc/nginx/certs:ro
# 日志目录
- nginx_logs:/var/log/nginx
environment:
- TZ=Asia/Shanghai
restart: always
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost/"]
interval: 30s
timeout: 5s
retries: 3
# Nginx 日志收集(可选)
# nginx_logs:
# driver: none# nginx.conf
# 运行用户(非 root)
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
# 日志格式
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
# 性能优化
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
gzip on;
gzip_types text/plain text/css application/json application/javascript;
# 安全头
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
server {
listen 80;
server_name example.com;
root /usr/share/nginx/html;
index index.html;
# 强制跳转 HTTPS(生产环境)
# return 301 https://$server_name$request_uri;
location / {
try_files $uri $uri/ /index.html;
}
# 静态资源长缓存
location ~* \.[a-f0-9]{8,}\.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# HTML 不缓存
location ~* \.html$ {
expires -1;
add_header Cache-Control "no-cache, no-store, must-revalidate";
}
# 静态资源合并(雪碧图等)
location /assets/ {
expires 30d;
add_header Cache-Control "public";
}
}
# HTTPS 配置(生产环境)
# server {
# listen 443 ssl http2;
# server_name example.com;
#
# ssl_certificate /etc/nginx/certs/example.com.crt;
# ssl_certificate_key /etc/nginx/certs/example.com.key;
# ssl_session_cache shared:SSL:10m;
# ssl_session_timeout 10m;
#
# location / {
# try_files $uri $uri/ /index.html;
# }
# }
}PM2 是 Node.js 进程管理器,适合部署 SSR 应用、API 服务、BFF 层等。
# 全局安装
npm install -g pm2
# 启动应用
pm2 start server.js --name my-api
# 查看状态
pm2 list
# 查看日志
pm2 logs my-api
# 重启
pm2 restart my-api// ecosystem.config.js
module.exports = {
apps: [
{
name: 'my-api',
script: 'server.js',
instances: 1, // 或 'max' 利用多核
exec_mode: 'cluster', // 集群模式
env: {
NODE_ENV: 'production',
PORT: 3000,
},
// 资源限制
max_memory_restart: '500M',
// 日志
log_file: './logs/combined.log',
error_file: './logs/error.log',
out_file: './logs/out.log',
// 开机自启
autorestart: true,
max_restarts: 10,
min_uptime: '10s',
},
],
}# pm2.Dockerfile
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install -g pm2 && npm install
COPY . .
# 启动 PM2(而非 node server.js)
CMD ["pm2-runtime", "ecosystem.config.js"]# docker-compose.yaml
services:
api:
build:
context: .
dockerfile: pm2.Dockerfile
ports:
- "3000:3000"
environment:
- NODE_ENV=production
restart: unless-stoppedCDN(内容分发网络)将静态资源分发到全球节点,用户就近访问,显著提升加载速度。
用户请求 → CDN 边缘节点(缓存命中)→ 直接返回
↓(未命中)
CDN 源站 → 回源拉取资源 → 缓存 → 返回
| 服务商 | 免费额度 | 特点 |
|---|---|---|
| Cloudflare | 免费版足够个人用 | 抗 DDoS、自动 HTTPS |
| Vercel | 100GB 带宽/月 | 零配置,Git 推送即部署 |
| Netlify | 100GB 带宽/月 | 静态站点首选 |
| 阿里云 OSS + CDN | 按量付费 | 国内速度快 |
只需在项目根目录添加配置文件:
# vercel.json
{
"buildCommand": "npm run build",
"outputDirectory": "dist",
"framework": "vite",
"routes": [
{ "handle": "filesystem" },
{ "src": "/(.*)", "dest": "/index.html" }
]
}然后连接 Git 仓库,每次 push 自动部署。
upstream cdn_origin {
server 127.0.0.1:8080; # 源站地址
}
server {
listen 80;
server_name cdn.example.com;
location / {
proxy_pass http://cdn_origin;
proxy_cache my_cache;
proxy_cache_valid 200 1y;
proxy_cache_key $uri;
add_header X-Cache-Status $upstream_cache_status;
}
}# 查看访问最多的 IP
awk '{print $1}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head 10
# 查看 5xx 错误
awk '$9 ~ /5[0-9]{2}/ {print $0}' /var/log/nginx/access.log
# 查看访问最频繁的路径
awk '{print $7}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head 10
# 实时查看访问日志
tail -f /var/log/nginx/access.log# 查看实时日志
pm2 logs my-api --lines 100
# 查看错误日志
pm2 logs my-api --err
# 日志轮转(防止单个文件过大)
pm2 install pm2-logrotate
pm2 set pm2-logrotate:max_size 10M
pm2 set pm2-logrotate:retain 7# 查看容器日志
docker logs -f <container_id>
# 查看最近 100 行
docker logs --tail 100 <container_id>
# 带时间戳
docker logs -t <container_id>
# 日志驱动配置(docker-compose.yaml)
services:
web:
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"| 工具 | 类型 | 免费额度 |
|---|---|---|
| Sentry | 前端错误监控 | 5000 次/月 |
| Prometheus + Grafana | 基础设施监控 | 开源免费 |
| UptimeRobot | 在线率监控 | 50 个监控点 |
| Better Uptime | 在线率 + 告警 | 1 个监控点免费 |
#!/bin/bash
# health-check.sh
ENDPOINT="http://localhost/"
ALERT_EMAIL="[email protected]"
# 检测 HTTP 状态码
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" $ENDPOINT)
if [ "$HTTP_CODE" -ne 200 ]; then
echo "Health check failed! HTTP code: $HTTP_CODE" | \
mail -s "[ALERT] Service Down" $ALERT_EMAIL
# 可选:触发自动回滚
docker-compose -f docker-compose.prod.yaml pull
docker-compose -f docker-compose.prod.yaml up -d
fi| 问题 | 排查方向 |
|---|---|
| 502 Bad Gateway | Nginx 无法连接后端,检查 upstream 配置和后端服务状态 |
| 403 Forbidden | 文件权限不足,检查 chmod 755 和 Nginx user 配置 |
| 静态资源 404 | 检查 root 路径配置和文件是否正确 COPY 到容器 |
| 刷新页面 404 | SPA 未配置 try_files $uri $uri/ /index.html |
| 部署后页面不变 | 缓存问题,检查 Cache-Control 头或强制刷新 Ctrl+Shift+R |
- 构建产物中 HTML 文件设置
Cache-Control: no-cache - 静态资源(JS/CSS/图片)带 hash,
max-age=31536000 - Nginx 配置
try_files支持 SPA 路由 - Docker 镜像使用多阶段构建,体积最小化
- 生产环境配置健康检查
healthcheck - Secrets 敏感信息使用 GitHub Secrets 管理
- 部署前确认有回滚方案
- 监控告警已配置