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

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

适合人群:前端/后端初学者、希望了解从开发到上线完整链路的工程师 示例技术栈: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 | 如有疑问欢迎在群内交流

相关推荐
用户600071819107 小时前
【翻译】简化 TSRX
前端
luckdewei7 小时前
那个用 passlib 做认证的新同事,上线第一天就把用户密码写进了日志
后端
IT乐手8 小时前
佛德角逼平西班牙,国足还有啥借口?
前端
ping某9 小时前
为什么 Nginx 明明监听了 80,转发后端时却用了 4xxxx 端口?
后端·nginx
JustHappy9 小时前
我汇总了身边朋友的经历才发现,其实第一份实习是最难找的......
前端·后端·面试
uhakadotcom9 小时前
在python 的 工程化架构中 ,什么是 薄包装器层?
后端·面试·github
星栈9 小时前
Dioxus 的响应式系统:`Signal`、`Memo`、`Effect` 和异步状态到底该怎么分工
前端·前端框架
yingyima9 小时前
Java 正则表达式:比你想象的更强大
前端
yuanyxh12 小时前
macOS 应用 - 纯对话生成
前端·macos·ai编程
大家的林语冰12 小时前
ES5 凉凉,Babel 8 正式发布,默认不再编译为 ES5 和 CJS......
前端·javascript·前端工程化