前后端项目部署与运行机制全流程详解

前后端项目部署与运行机制全流程详解

适合人群:前端/后端初学者、希望了解从开发到上线完整链路的工程师 示例技术栈:Vue 3 + Node.js/Express + Nginx


目录

  1. 项目打包阶段
  2. 部署架构与流程
  3. 用户访问完整流程
  4. 关键概念深度解析
  5. 实际操作步骤
  6. 常见问题与解决方案
  7. 监控与运维

一、项目打包阶段

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.jsbase 与部署路径一致
所有请求返回 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.htmlCache-Control: no-cache(每次请求都验证)
  • app.abc123.jsCache-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 | 如有疑问欢迎在群内交流

相关推荐
本末倒置1831 小时前
Vue 3 开发者转型 React 指南:保姆级教程
前端·javascript·vue.js
Reart1 小时前
从0解构tinyWeb项目--(Day:10)
前端·后端·架构
SamDeepThinking2 小时前
程序员如何接受工作内容毫无意义?
java·后端·程序员
_Evan_Yao2 小时前
一文搞懂:AI编程辅助工具——从GitHub Copilot到通义灵码,不同人群如何驾驭AI编程助手?
人工智能·后端·copilot·ai编程
木雷坞2 小时前
边缘视频分析节点断网恢复排查记录
后端
牛蛙点点申请出战2 小时前
IconFontViewer -- 一个可以在 Android Studio 中实时预览 IconFont 的插件
android·前端·intellij idea
2301_771717212 小时前
最近在刷牛客:使用Spring AOP实现性能监控时
java·后端·spring
Java水解2 小时前
深入浅出多包架构(Monorepo)
后端