前后端项目部署与运行机制全流程详解
适合人群:前端/后端初学者、希望了解从开发到上线完整链路的工程师 示例技术栈:Vue 3 + Node.js/Express + Nginx
目录
一、项目打包阶段
1. 前端打包(以 Vue + Vite/Webpack 项目为例)
打包命令
bash
npm run build
这条命令背后,package.json 中通常配置了:
json
{
"scripts": {
"build": "vite build"
// 或 Webpack 项目:
// "build": "vue-cli-service build"
}
}
构建工具做了什么?
Vite 构建流程(现代项目主流):
css
源代码(.vue / .ts / .js / .scss)
↓
1. 依赖分析(Rollup 为底层)
↓
2. Tree Shaking(去掉未使用的代码)
↓
3. 代码分割(Code Splitting)
↓
4. 资源处理(CSS 提取、图片压缩、字体内联)
↓
5. 代码压缩(Terser 压缩 JS,cssnano 压缩 CSS)
↓
6. 生成 Hash 文件名
↓
dist/ 目录(最终产物)
Webpack 构建流程(Vue CLI 项目):
javascript
入口文件(main.js)
↓
Webpack 递归分析 import/require 依赖图
↓
Loader 转换:
- babel-loader:ES6+ → ES5(兼容老浏览器)
- vue-loader:.vue 文件 → JS + CSS
- css-loader + style-loader / MiniCssExtractPlugin
- file-loader / url-loader:处理图片、字体
↓
Plugin 优化:
- HtmlWebpackPlugin:生成 index.html 并注入 script 标签
- TerserPlugin:JS 压缩混淆
- OptimizeCSSAssetsPlugin:CSS 压缩
- SplitChunksPlugin:代码分割
↓
输出 dist/
核心概念解析
Tree Shaking(摇树优化)
把项目比作一棵树,Tree Shaking 会把没用的"叶子"摇掉:
js
// utils.js
export function usedFunction() { return 'used' }
export function unusedFunction() { return 'unused' } // 打包后会被删除
// main.js
import { usedFunction } from './utils' // 只导入了 usedFunction
前提:需要使用 ES Module(
import/export),CommonJS(require)无法 Tree Shaking。
代码分割(Code Splitting)
将大 bundle 拆分成多个小文件,实现按需加载:
js
// 路由懒加载(Vue Router)
const routes = [
{
path: '/book/detail',
component: () => import('./views/BookDetail.vue') // 独立打包成一个 chunk
}
]
打包后生成:
index.js(主入口,很小)BookDetail.abc123.js(访问该路由时才加载)
Hash 文件名生成
app.a1b2c3.js ← 文件内容的 MD5 哈希(前8位)
原理:对文件内容做哈希计算,内容不变则 hash 不变,内容变了则 hash 变化。
好处:
- 同一文件 hash 相同 → 浏览器缓存命中,无需重新下载
- 文件更新后 hash 变化 → 强制浏览器下载新版本(解决缓存问题)
静态资源处理
| 资源类型 | 处理方式 |
|---|---|
| CSS | 提取为独立 .css 文件(生产环境),避免阻塞 JS 加载 |
| 小图片(<4KB) | 内联为 Base64,减少 HTTP 请求 |
| 大图片 | 复制到 dist/assets,生成 hash 文件名 |
| 字体文件 | 同图片处理,复制+hash |
| SVG | 可内联为 Vue 组件,也可作为静态资源 |
打包输出目录结构
css
dist/
├── index.html ← 入口 HTML(所有 JS/CSS 引用已注入)
├── assets/
│ ├── index.a1b2c3.js ← 主 JS bundle
│ ├── index.d4e5f6.css ← 主 CSS bundle
│ ├── BookDetail.789abc.js ← 懒加载 chunk
│ ├── vendor.def012.js ← 第三方库(vue, axios 等)单独打包
│ ├── logo.345678.png ← 图片资源
│ └── iconfont.901234.woff2 ← 字体文件
└── favicon.ico
index.html 内容示例:
html
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="/assets/index.d4e5f6.css">
</head>
<body>
<div id="app"></div>
<script type="module" src="/assets/index.a1b2c3.js"></script>
</body>
</html>
静态资源上传 CDN
打包完成后,将 dist/assets/ 中的静态资源(JS/CSS/图片)上传到 CDN:
bash
# 示例:使用 aws-cli 上传到 S3(CDN 源站)
aws s3 sync dist/assets/ s3://my-bucket/assets/ \
--cache-control "max-age=31536000,immutable"
# 或使用自定义 CDN 工具
cdn-cli upload --dir dist/assets/ --bucket my-cdn-bucket
上传后,修改 Vite/Webpack 配置,让资源引用地址指向 CDN 域名:
js
// vite.config.js
export default {
base: 'https://cdn.example.com/' // 生产环境资源基础路径
}
打包后 index.html 中的引用会变为:
html
<link href="https://cdn.example.com/assets/index.d4e5f6.css">
<script src="https://cdn.example.com/assets/index.a1b2c3.js"></script>
2. 后端打包(以 Node.js + Express 为例)
是否需要编译?
Node.js 原生情况(使用 CommonJS,不需要编译):
bash
# 直接运行,无需编译步骤
node app.js
TypeScript 项目(需要编译):
bash
# 安装编译器
npm install -D typescript
# 编译 TS → JS
npx tsc
# 输出到 dist/
node dist/app.js
tsconfig.json 配置:
json
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"outDir": "./dist",
"rootDir": "./src",
"strict": true
}
}
依赖管理:生产环境只安装必要依赖
bash
# 开发时安装所有依赖
npm install
# 生产部署只安装 dependencies(不含 devDependencies)
npm install --production
# 或
npm ci --production
package.json 中的区别:
json
{
"dependencies": {
"express": "^4.18.0", // 生产需要
"mysql2": "^3.0.0" // 生产需要
},
"devDependencies": {
"typescript": "^5.0.0", // 仅开发需要(编译用)
"jest": "^29.0.0", // 仅开发需要(测试用)
"nodemon": "^3.0.0" // 仅开发需要(热重载)
}
}
启动配置与环境变量
使用 .env 文件管理环境变量(不提交到代码库):
bash
# .env.production
NODE_ENV=production
PORT=3000
DB_HOST=localhost
DB_NAME=myapp
DB_USER=appuser
DB_PASS=your_password_here
在代码中读取:
js
// 安装 dotenv
npm install dotenv
// app.js
require('dotenv').config()
const PORT = process.env.PORT || 3000
const DB_HOST = process.env.DB_HOST
启动命令配置(package.json):
json
{
"scripts": {
"start": "node src/app.js",
"start:prod": "NODE_ENV=production node dist/app.js",
"dev": "nodemon src/app.js"
}
}
二、部署架构与流程
1. 服务器部署架构
less
┌─────────────────────────────────┐
│ 服务器 │
[用户浏览器] │ │
│ │ ┌──────────────┐ │
│ HTTPS 请求 │ │ │ │
├──────────────────────→│ Nginx │ │
│ │ │ (反向代理) │ │
│ │ └──────┬───────┘ │
│ │ │ │
│ │ ┌─────┴──────┐ │
│ │ ↓ ↓ │
│ │ 静态文件 API 转发 │
│ │ /var/www/ ↓ │
│ │ frontend/ ┌──────────────┐ │
│ │ dist/ │ Node.js │ │
│ │ │ (PM2 管理) │ │
│ │ │ :3000 │ │
│ │ └──────┬────────┘ │
│ │ │ │
│ │ ┌──────↓────────┐ │
│ │ │ 数据库/缓存 │ │
│ │ │ MySQL / Redis │ │
│ │ └───────────────┘ │
│ └─────────────────────────────────┘
│
[CDN 边缘节点]
← 静态资源就近返回
2. Nginx 的核心作用
反向代理
正向代理 :客户端知道目标服务器,代理帮客户端发请求(如科学上网) 反向代理:客户端不知道后端服务器,由代理决定转发到哪里
css
[浏览器] → [Nginx :80] → [后端服务 :3000]
↑
浏览器只知道 Nginx 的地址
不知道后端在哪、有几台
使用反向代理的原因:
- 安全:后端服务不直接暴露到公网
- 统一入口:一个 IP/域名,服务多个应用
- 负载均衡:请求分发到多个后端实例
- SSL 终止:HTTPS 在 Nginx 层处理,后端用 HTTP 即可
静态资源服务
Nginx 直接从文件系统读取静态文件返回,无需经过 Node.js,性能极高:
nginx
# Nginx 原生处理静态文件
location /assets/ {
root /var/www/frontend/dist;
# 内部处理:sendfile 系统调用,零拷贝传输
# 吞吐量:可达数万 QPS
}
对比:如果让 Node.js 服务静态文件,每次请求都要走 JS 运行时,性能差 10-100 倍。
负载均衡
nginx
# 配置上游服务器组
upstream backend_cluster {
server 127.0.0.1:3000 weight=3; # 权重3(分配更多请求)
server 127.0.0.1:3001 weight=1;
server 127.0.0.1:3002 weight=1;
# least_conn; # 最少连接数策略
# ip_hash; # 同一IP始终路由到同一服务器
}
server {
location /api/ {
proxy_pass http://backend_cluster;
}
}
SSL/HTTPS 配置
nginx
server {
listen 443 ssl http2;
server_name example.com;
# SSL 证书(Let's Encrypt 免费证书)
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
# 安全配置
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
# HSTS:告诉浏览器后续只用 HTTPS
add_header Strict-Transport-Security "max-age=31536000" always;
}
# HTTP 强制跳转 HTTPS
server {
listen 80;
server_name example.com;
return 301 https://$host$request_uri;
}
缓存策略
nginx
# 静态资源(有 hash 文件名):超长缓存
location ~* \.(js|css|png|jpg|woff2)$ {
expires 1y; # 缓存1年
add_header Cache-Control "public, immutable";
# immutable 告诉浏览器:文件内容永远不会变(配合 hash 文件名使用)
}
# HTML 文件:不缓存(确保用户总能获取最新的 JS/CSS 引用)
location = /index.html {
expires -1;
add_header Cache-Control "no-cache, no-store, must-revalidate";
}
# API 接口:不缓存
location /api/ {
expires -1;
add_header Cache-Control "no-cache";
proxy_pass http://localhost:3000;
}
跨域处理
nginx
location /api/ {
# CORS 配置
add_header Access-Control-Allow-Origin "https://www.example.com";
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS";
add_header Access-Control-Allow-Headers "Authorization, Content-Type";
add_header Access-Control-Allow-Credentials "true";
# 处理 OPTIONS 预检请求
if ($request_method = 'OPTIONS') {
return 204;
}
proxy_pass http://localhost:3000;
}
请求转发规则:区分静态资源和 API
nginx
# 规则优先级:精确匹配 > 前缀匹配 > 正则匹配
# API 请求 → 转发到后端
location /api/ {
proxy_pass http://localhost:3000;
}
# 静态资源(正则匹配扩展名)→ 添加缓存头
location ~* \.(js|css|png|jpg|svg|woff2|ico)$ {
root /var/www/frontend/dist;
expires 1y;
add_header Cache-Control "public, immutable";
}
# 所有其他请求 → 返回 index.html(前端路由接管)
location / {
root /var/www/frontend/dist;
try_files $uri $uri/ /index.html;
}
3. Nginx 配置示例逐行解析
nginx
server {
listen 80; # 监听 80 端口(HTTP)
server_name example.com; # 匹配的域名
# 静态资源根目录(dist 就是前端打包输出目录)
root /var/www/frontend/dist;
index index.html; # 默认首页文件
# ─────────────────────────────────────────
# 静态资源缓存配置
# 正则匹配:所有 js/css/图片/字体 文件
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 1y; # 告诉浏览器缓存1年
add_header Cache-Control "public, immutable";
# public:允许中间代理(CDN)缓存
# immutable:内容不会改变,不要发条件请求验证
}
# ─────────────────────────────────────────
# 前端路由 history 模式支持
# Vue Router history 模式:URL 是真实路径,如 /book/detail/123
# 但服务器上没有这个物理文件,直接请求会 404
location / {
try_files $uri $uri/ /index.html;
# 含义:
# 1. 先尝试 $uri(完整路径的文件是否存在,如 /assets/logo.png)
# 2. 再尝试 $uri/(是否是目录)
# 3. 都不存在则返回 /index.html(让前端路由处理)
}
# ─────────────────────────────────────────
# API 请求转发到后端服务
location /api/ {
proxy_pass http://localhost:3000; # 转发到后端 3000 端口
# proxy_pass 末尾有无 / 的区别:
# proxy_pass http://localhost:3000; → /api/book → http://localhost:3000/api/book
# proxy_pass http://localhost:3000/; → /api/book → http://localhost:3000/book
proxy_set_header Host $host; # 传递原始 Host 头(域名)
proxy_set_header X-Real-IP $remote_addr; # 传递用户真实 IP
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# X-Forwarded-For:经过多个代理时,记录完整的 IP 链
# 如:用户IP → CDN IP → Nginx IP → 后端
}
}
三、用户访问完整流程
场景 1:访问页面(静态资源请求)
bash
用户输入:https://example.com/book/detail/123
│
▼
① DNS 解析
本机 DNS 缓存 → 运营商 DNS → 根域名服务器 → 权威 DNS
返回:example.com → 服务器 IP(如 1.2.3.4)
│
▼
② 建立 TCP 连接(三次握手)
浏览器 → SYN → 服务器
浏览器 ← SYN+ACK ← 服务器
浏览器 → ACK → 服务器
│
▼
③ TLS 握手(HTTPS)
协商加密算法 → 证书验证 → 生成会话密钥
(完成后所有通信加密)
│
▼
④ 浏览器发送 HTTP 请求
GET /book/detail/123 HTTP/2
Host: example.com
Accept: text/html
│
▼
⑤ Nginx 接收请求
匹配 location 规则:
/book/detail/123 → 没有对应物理文件
→ try_files 策略 → 返回 /index.html
│
▼
⑥ Nginx 返回 index.html
HTTP/2 200 OK
Content-Type: text/html
Cache-Control: no-cache
[index.html 内容]
│
▼
⑦ 浏览器解析 HTML
发现 <script src="/assets/index.a1b2c3.js">
发现 <link href="/assets/index.d4e5f6.css">
│
▼
⑧ 并行请求 JS/CSS 等资源
请求 /assets/index.a1b2c3.js
├─ Nginx 检查:~* \.js$ → 匹配缓存规则
├─ 文件来自 CDN 或本地 dist/assets/
└─ 返回文件 + Cache-Control: public, immutable
(如果用户之前访问过,且浏览器有缓存,直接用缓存,不发请求)
│
▼
⑨ Vue 应用初始化
JS 执行 → 创建 Vue 实例 → 初始化路由
→ 路由解析 /book/detail/123
→ 触发组件懒加载(请求 BookDetail.789abc.js)
→ 组件挂载,渲染 DOM
│
▼
⑩ 组件 mounted 钩子
调用 API 获取书籍数据(见场景2)
→ 数据返回 → 更新 DOM → 用户看到页面内容
场景 2:API 请求(数据接口调用)
dart
前端代码:fetch('/api/book/123')
│
▼
① 浏览器发起 HTTP 请求
GET /api/book/123 HTTP/2
Host: example.com
Authorization: Bearer eyJhbGciOi...
│
▼
② Nginx 匹配 location /api/
→ 进入 proxy_pass 规则
→ 转发请求到 http://localhost:3000
│
▼
③ Node.js(Express)接收请求
Express 路由匹配:
router.get('/api/book/:id', async (req, res) => {
const { id } = req.params // id = '123'
│
▼
④ 业务逻辑处理
├─ 身份验证中间件:解析 JWT Token
├─ 参数校验:id 是否合法
├─ 查询缓存(Redis):
│ 存在 → 直接返回缓存数据
│ 不存在 → 查询 MySQL
├─ 数据库查询:
│ SELECT * FROM books WHERE id = 123
├─ 写入 Redis 缓存(下次不用查库)
└─ 构造响应 JSON
│
▼
⑤ 后端返回响应给 Nginx
HTTP/1.1 200 OK
Content-Type: application/json
{"id": 123, "title": "...", "author": "..."}
│
▼
⑥ Nginx 将响应转发给浏览器
(可在此层做响应头添加、gzip 压缩等)
│
▼
⑦ 前端 JavaScript 处理响应
const data = await response.json()
bookStore.setBook(data) // 更新状态
// 触发 Vue 响应式更新 → 重新渲染 DOM
四、关键概念深度解析
1. CDN 加速机制
什么是 CDN?
CDN(Content Delivery Network,内容分发网络)是一套分布在全球各地的服务器网络。
没有 CDN 时的问题:
北京用户 → 深圳服务器(物理距离远,延迟高)
美国用户 → 深圳服务器(跨洋请求,延迟极高)
有 CDN 后:
北京用户 → 北京 CDN 节点(就近获取,延迟低)
美国用户 → 美国 CDN 节点(本地获取,延迟低)
CDN 工作原理
markdown
1. 上传资源到源站(Origin Server)
开发者 → 上传 dist/assets/ 到 CDN 源站
2. 用户首次请求
用户 → 请求 cdn.example.com/assets/app.abc123.js
→ DNS 智能解析(根据用户 IP 返回最近节点 IP)
→ 就近 CDN 节点:没有缓存 → 回源(请求源站)
→ 源站返回文件 → CDN 节点缓存文件 → 返回给用户
3. 后续用户请求同一资源
→ 就近 CDN 节点:有缓存 → 直接返回
→ 源站无感知(节省带宽和请求压力)
静态资源如何上传 CDN
bash
# 方式一:CLI 工具上传
cdn-cli sync ./dist/assets/ --bucket prod-static --region cn-north-1
# 方式二:构建工具插件(Vite 插件自动上传)
# vite.config.js
import { defineConfig } from 'vite'
import cdnUpload from 'vite-plugin-cdn-upload'
export default defineConfig({
plugins: [
cdnUpload({
bucket: 'prod-static',
accessKey: process.env.CDN_ACCESS_KEY,
secretKey: process.env.CDN_SECRET_KEY
})
]
})
2. 反向代理 vs 正向代理
| 对比项 | 正向代理 | 反向代理 |
|---|---|---|
| 代理对象 | 代理客户端 | 代理服务端 |
| 谁知道目标 | 客户端知道目标服务器 | 客户端不知道后端服务器 |
| 典型用途 | 翻墙、访问控制 | 负载均衡、安全隔离 |
| 对服务端是否透明 | 服务端不知道真实客户端 | 客户端不知道真实服务端 |
css
正向代理:
[客户端] → [正向代理] → [目标服务器]
知道目标 隐藏了客户端
反向代理:
[客户端] → [反向代理(Nginx)] → [后端服务]
不知道后端 隐藏了后端
生产环境为什么要用反向代理?
- 安全:Node.js 不暴露在公网,攻击者无法直接攻击
- 统一管理:SSL、限流、日志、压缩在 Nginx 层统一处理
- 性能:Nginx 处理静态文件、高并发连接效率远高于 Node.js
- 灵活:后端服务可以任意重启、扩容,客户端无感知
3. 进程管理(PM2 / Docker)
PM2(Node.js 进程管理器)
bash
# 安装
npm install -g pm2
# 启动应用(基本用法)
pm2 start app.js --name my-api
# 集群模式(自动利用多核 CPU)
pm2 start app.js --name my-api --instances max
# instances=max 表示按 CPU 核数创建进程
# 查看状态
pm2 status
pm2 monit # 实时监控(CPU/内存/日志)
# 查看日志
pm2 logs my-api
pm2 logs my-api --lines 200
# 重启
pm2 restart my-api
pm2 reload my-api # 零停机重启(逐个重启实例)
# 设置开机自启
pm2 startup
pm2 save
PM2 配置文件 ecosystem.config.js:
js
module.exports = {
apps: [{
name: 'my-api',
script: 'dist/app.js',
instances: 'max', // 多实例(充分利用 CPU)
exec_mode: 'cluster', // 集群模式
max_memory_restart: '1G', // 内存超过1G自动重启
env: {
NODE_ENV: 'development'
},
env_production: {
NODE_ENV: 'production',
PORT: 3000
},
// 日志配置
out_file: '/var/log/myapp/out.log',
error_file: '/var/log/myapp/error.log',
log_date_format: 'YYYY-MM-DD HH:mm:ss'
}]
}
// 启动:pm2 start ecosystem.config.js --env production
Docker 容器化部署
dockerfile
# Dockerfile(后端服务)
FROM node:18-alpine
# 设置工作目录
WORKDIR /app
# 先复制 package.json(利用 Docker 层缓存)
COPY package*.json ./
RUN npm ci --production
# 复制源代码
COPY dist/ ./dist/
# 暴露端口
EXPOSE 3000
# 健康检查
HEALTHCHECK --interval=30s --timeout=3s \
CMD curl -f http://localhost:3000/health || exit 1
# 启动命令
CMD ["node", "dist/app.js"]
yaml
# docker-compose.yml(本地/简单部署)
version: '3.8'
services:
api:
build: .
ports:
- "3000:3000"
environment:
NODE_ENV: production
DB_HOST: db
depends_on:
- db
restart: unless-stopped
db:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASS}
MYSQL_DATABASE: myapp
volumes:
- db_data:/var/lib/mysql
restart: unless-stopped
volumes:
db_data:
五、实际操作步骤
完整上线流程(Vue + Node.js)
前期准备(首次部署)
bash
# 1. 服务器初始化
sudo apt update && sudo apt upgrade -y
# 2. 安装 Node.js(推荐 nvm 管理版本)
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash
nvm install 18
nvm use 18
# 3. 安装 Nginx
sudo apt install nginx -y
sudo systemctl enable nginx
sudo systemctl start nginx
# 4. 安装 PM2
npm install -g pm2
# 5. 安装 MySQL
sudo apt install mysql-server -y
sudo mysql_secure_installation
# 6. 创建应用目录
sudo mkdir -p /var/www/frontend/dist
sudo chown -R $USER:$USER /var/www/frontend
mkdir -p /home/ubuntu/backend
前端部署
bash
# 本地执行:
# 1. 安装依赖
npm install
# 2. 打包
npm run build
# 3. 上传静态资源到 CDN(如果有)
cdn-cli sync ./dist/assets/ --bucket prod-cdn
# 4. 上传 dist/ 到服务器
scp -r dist/* ubuntu@1.2.3.4:/var/www/frontend/dist/
# 或使用 rsync(更高效,只同步变更文件)
rsync -avz --delete dist/ ubuntu@1.2.3.4:/var/www/frontend/dist/
后端部署
bash
# 服务器上执行:
# 1. 拉取最新代码
cd /home/ubuntu/backend
git pull origin main
# 2. 安装生产依赖
npm ci --production
# 3. TypeScript 项目需要编译
npm run build # tsc 编译 src/ → dist/
# 4. 设置环境变量
cp .env.example .env.production
nano .env.production # 填写真实配置
# 5. 数据库迁移(如有)
npm run migrate:prod
# 6. 重启/启动 PM2
pm2 start ecosystem.config.js --env production
# 或已在运行时:
pm2 reload my-api # 零停机重载
# 7. 保存 PM2 状态
pm2 save
Nginx 配置与生效
bash
# 1. 编写 Nginx 配置
sudo nano /etc/nginx/sites-available/myapp
# 2. 启用站点
sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/
# 3. 检查配置语法
sudo nginx -t
# 输出:nginx: configuration file /etc/nginx/nginx.conf test is successful
# 4. 重载配置(零停机)
sudo nginx -s reload
# 或
sudo systemctl reload nginx
# 5. 申请 SSL 证书(Let's Encrypt 免费)
sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d example.com -d www.example.com
# 证书自动续期(certbot 会自动配置 cron)
完整配置文件(生产级)
nginx
# /etc/nginx/sites-available/myapp
# HTTP → HTTPS 强制跳转
server {
listen 80;
server_name example.com www.example.com;
return 301 https://$server_name$request_uri;
}
# HTTPS 主配置
server {
listen 443 ssl http2;
server_name example.com www.example.com;
# SSL 证书(Let's Encrypt)
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
# 安全头
add_header X-Frame-Options "SAMEORIGIN";
add_header X-Content-Type-Options "nosniff";
add_header Strict-Transport-Security "max-age=31536000" always;
# Gzip 压缩(减少传输大小 60-80%)
gzip on;
gzip_types text/plain application/json application/javascript text/css;
gzip_min_length 1024;
# 前端静态文件根目录
root /var/www/frontend/dist;
index 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; # 静态资源不记录访问日志(节省 IO)
}
# index.html:不缓存
location = /index.html {
expires -1;
add_header Cache-Control "no-cache, no-store, must-revalidate";
}
# 前端路由支持
location / {
try_files $uri $uri/ /index.html;
}
# API 代理
location /api/ {
proxy_pass http://localhost:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade; # WebSocket 支持
proxy_set_header Connection 'upgrade';
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; # 传递 http/https
proxy_cache_bypass $http_upgrade;
# 超时设置
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
# Nginx 访问日志
access_log /var/log/nginx/myapp_access.log;
error_log /var/log/nginx/myapp_error.log;
}
六、常见问题与解决方案
1. 部署后页面空白
排查步骤:
bash
# 1. 打开浏览器 DevTools → Console
# 常见错误:
# - "Failed to load resource" → 静态资源 404
# - JavaScript 报错 → 检查兼容性(babel 配置)
# 2. 检查 Network 面板
# - index.html 是否正常返回(200)?
# - JS/CSS 文件是否正常加载?
# 3. 检查打包配置
# vite.config.js / vue.config.js 中 base 路径是否正确?
常见原因:
| 症状 | 原因 | 解决方案 |
|---|---|---|
| HTML 正常,JS 404 | base 路径配置错误 |
确认 vite.config.js 中 base 与部署路径一致 |
| 所有请求返回 index.html | Nginx try_files 配置问题 | 检查静态文件 location 是否在前面 |
| 控制台有 JS 错误 | 代码兼容性问题 | 检查 babel/browserslist 配置 |
| 白屏但无报错 | Vue 路由初始化失败 | 检查路由 history mode 与 nginx try_files 是否配合 |
2. API 请求 404
bash
# 判断是 Nginx 问题还是后端问题
# 直接请求后端(绕过 Nginx)
curl http://localhost:3000/api/book/123
# 如果有响应 → Nginx 配置问题
# 如果也 404 → 后端路由问题
Nginx 问题排查:
nginx
# 错误示例:proxy_pass 结尾的 / 会改变路径
location /api/ {
proxy_pass http://localhost:3000/;
# 请求 /api/book → 转发到 http://localhost:3000/book(丢失 /api/)
}
# 正确示例:保留原始路径
location /api/ {
proxy_pass http://localhost:3000;
# 请求 /api/book → 转发到 http://localhost:3000/api/book
}
后端路由排查:
bash
# 查看应用日志
pm2 logs my-api --lines 100
# 确认路由是否注册
curl -v http://localhost:3000/api/book/123
3. 静态资源 404
bash
# 检查文件是否存在
ls /var/www/frontend/dist/assets/
# 检查 Nginx 日志
tail -n 50 /var/log/nginx/myapp_error.log
# 常见提示:
# "Permission denied" → 文件权限问题
# "No such file" → 路径配置错误
权限问题修复:
bash
# Nginx 默认以 www-data 用户运行
sudo chown -R www-data:www-data /var/www/frontend/dist/
sudo chmod -R 755 /var/www/frontend/dist/
4. 跨域问题
nginx
# Nginx 添加 CORS 头
location /api/ {
# 允许的来源
add_header Access-Control-Allow-Origin "https://example.com" always;
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always;
add_header Access-Control-Allow-Headers "Authorization, Content-Type, X-Requested-With" always;
add_header Access-Control-Max-Age 86400; # 预检请求缓存24小时
if ($request_method = OPTIONS) {
return 204;
}
proxy_pass http://localhost:3000;
}
注意:如果后端也设置了 CORS 头,会与 Nginx 冲突(浏览器收到重复 CORS 头会报错)。建议只在一处设置。
5. 缓存问题(用户看到旧版本)
原理分析:
diff
index.html(不缓存)→ 引用 app.a1b2c3.js
新版本上线:
index.html(不缓存)→ 引用 app.d4e5f6.js(新 hash)
正确做法(Hash 文件名 + HTML 不缓存):
index.html:Cache-Control: no-cache(每次请求都验证)app.abc123.js:Cache-Control: max-age=31536000, immutable(永久缓存)
如果没有 hash 文件名(紧急情况):
bash
# 方案1:在资源 URL 加版本号
<script src="/assets/app.js?v=20260506">
# 方案2:Nginx 强制不缓存(牺牲性能)
location /assets/ {
expires -1;
add_header Cache-Control "no-cache";
}
# 方案3:让用户强制刷新
# 通知用户按 Ctrl+F5 / Cmd+Shift+R 硬刷新
七、监控与运维
1. 服务状态监控(Prometheus + Grafana)
架构概览:
csharp
[Node.js 服务] → 暴露 /metrics 端点
↓
[Prometheus] → 定时采集指标数据
↓
[Grafana] → 可视化展示、告警
Node.js 接入 Prometheus:
bash
npm install prom-client
js
// metrics.js
const client = require('prom-client')
// 收集默认指标(CPU、内存、GC 等)
client.collectDefaultMetrics()
// 自定义业务指标
const httpRequestDuration = new client.Histogram({
name: 'http_request_duration_seconds',
help: 'HTTP 请求响应时间',
labelNames: ['method', 'route', 'status'],
buckets: [0.1, 0.5, 1, 2, 5]
})
// Express 中间件:记录每次请求耗时
app.use((req, res, next) => {
const end = httpRequestDuration.startTimer()
res.on('finish', () => {
end({
method: req.method,
route: req.route?.path || req.path,
status: res.statusCode
})
})
next()
})
// 暴露 metrics 端点
app.get('/metrics', async (req, res) => {
res.set('Content-Type', client.register.contentType)
res.end(await client.register.metrics())
})
Prometheus 配置(prometheus.yml):
yaml
global:
scrape_interval: 15s
scrape_configs:
- job_name: 'my-api'
static_configs:
- targets: ['localhost:3000']
metrics_path: '/metrics'
2. 日志查看
bash
# Nginx 日志
sudo tail -f /var/log/nginx/myapp_access.log
sudo tail -f /var/log/nginx/myapp_error.log
# 日志格式解析(默认 combined 格式)
# 1.2.3.4 - - [06/May/2026:12:00:00 +0800] "GET /api/book/123 HTTP/2.0" 200 1234 "-" "Mozilla/5.0..."
# IP地址 时间戳 请求行 状态码 响应大小
# PM2 应用日志
pm2 logs my-api # 实时日志
pm2 logs my-api --lines 200 # 最近200行
pm2 logs my-api --err # 只看错误日志
# 系统日志
journalctl -u nginx -f # Nginx 系统日志
结构化日志(推荐):
bash
npm install winston
js
const winston = require('winston')
const logger = winston.createLogger({
level: 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
),
transports: [
new winston.transports.File({ filename: 'error.log', level: 'error' }),
new winston.transports.File({ filename: 'combined.log' })
]
})
// 使用
logger.info('请求处理完成', { userId: 123, duration: 45 })
logger.error('数据库连接失败', { error: err.message })
3. 灰度发布
灰度发布(金丝雀发布):先让一小部分用户使用新版本,确认无问题后全量发布。
nginx
# Nginx 实现灰度发布(基于 Cookie)
upstream prod_backend {
server 127.0.0.1:3000; # 旧版本
}
upstream canary_backend {
server 127.0.0.1:3001; # 新版本
}
server {
location /api/ {
# 如果 Cookie 中有灰度标记 → 走新版本
set $upstream prod_backend;
if ($cookie_canary = "true") {
set $upstream canary_backend;
}
proxy_pass http://$upstream;
}
}
更完整的灰度策略(按比例分流):
nginx
# 随机 5% 的请求走新版本
split_clients "${remote_addr}" $variant {
5% "canary";
* "prod";
}
location /api/ {
if ($variant = "canary") {
proxy_pass http://canary_backend;
break;
}
proxy_pass http://prod_backend;
}
4. 版本回滚
bash
# 方案一:保留上一个版本的 dist(前端回滚最快)
# 部署时备份当前版本
cp -r /var/www/frontend/dist /var/www/frontend/dist.backup.$(date +%Y%m%d%H%M%S)
rsync -avz --delete new_dist/ /var/www/frontend/dist/
# 需要回滚时
rsync -avz --delete /var/www/frontend/dist.backup.20260506120000/ /var/www/frontend/dist/
sudo nginx -s reload
# 方案二:Git Tag 标记发布版本(后端回滚)
git tag -a v1.2.0 -m "Release 1.2.0"
git push origin v1.2.0
# 回滚到指定版本
git checkout v1.1.0
npm ci --production
npm run build
pm2 reload my-api
# 方案三:Docker 镜像版本管理(最佳实践)
docker tag my-api:latest my-api:v1.2.0
# 回滚
docker pull my-api:v1.1.0
docker stop my-api && docker run -d --name my-api my-api:v1.1.0
附录:百度内部技术栈简介
以下为百度内部技术栈在标准部署流程中的对应角色,供参考。
iPipe(CI/CD 流水线)
对应标准流程中的 "打包 + 自动化部署" 环节。提交代码后自动触发:
- 单元测试 / 集成测试
- 前端
npm run build打包 - 后端编译打包
- 将产物推送到目标服务器或发布系统
Pandora(应用部署平台)
对应标准流程中的 PM2 / Docker 角色。提供:
- 应用实例管理(启动、停止、扩缩容)
- 滚动发布、灰度发布
- 流量分配与切换
- 健康检查与自动恢复
RAL(Remote Application Layer,服务调用框架)
对应标准流程中后端服务间的 HTTP/RPC 通信层。提供:
- 服务注册与发现(不需要手写 IP:Port)
- 负载均衡(在服务框架层,不依赖 Nginx)
- 超时、重试、熔断
- 链路追踪
iCode + Git
对应标准流程中的 代码仓库 + CI 触发器,与 iPipe 集成,代码 push 即触发流水线。
文档生成时间:2026-05-06 | 如有疑问欢迎在群内交流