Nginx 完全教程
从零开始掌握 Nginx 的安装、配置与运维。
01. Nginx 简介
Nginx (发音 "engine-x")是一个高性能的 HTTP 和反向代理服务器,由 Igor Sysoev 于 2004 年首次发布。它以事件驱动、异步非阻塞的架构闻名,能够以极低的内存消耗处理数万并发连接。
Nginx 能做什么?
- HTTP 服务器 --- 高效提供静态文件服务
- 反向代理 --- 将请求转发给后端应用服务器
- 负载均衡 --- 在多台后端之间分发流量
- SSL/TLS 终端 --- 处理 HTTPS 加密解密
- API 网关 --- 路由、限流、鉴权
- 邮件代理 --- IMAP/POP3/SMTP 代理
Nginx vs Apache
| 特性 | Nginx | Apache |
|---|---|---|
| 架构模型 | 事件驱动 / 异步非阻塞 | 进程 / 线程驱动 |
| 并发性能 | 极高(C10K+) | 中等 |
| 内存占用 | 低 | 相对较高 |
| 静态文件 | 极快 | 良好 |
| 动态内容 | 需借助外部(PHP-FPM等) | 内置 mod_php |
| 配置风格 | 集中式、声明式 | .htaccess 分散式 |
02. Linux 安装与启动
Ubuntu / Debian
bash
# 更新软件源并安装
sudo apt update
sudo apt install -y nginx
# 启动 & 设为开机自启
sudo systemctl start nginx
sudo systemctl enable nginx
# 验证安装
nginx -v
CentOS / RHEL / Fedora
bash
# CentOS 7
sudo yum install -y epel-release
sudo yum install -y nginx
# CentOS 8+ / Fedora
sudo dnf install -y nginx
sudo systemctl start nginx
sudo systemctl enable nginx
macOS (Homebrew)
sql
brew install nginx
brew services start nginx
Docker
bash
docker run -d --name nginx \
-p 80:80 \
-v /path/to/html:/usr/share/nginx/html \
-v /path/to/nginx.conf:/etc/nginx/nginx.conf \
nginx:stable
常用管理命令
r
nginx -t # 测试配置语法
nginx -T # 测试并输出完整配置
nginx -s reload # 平滑重载(不中断服务)
nginx -s stop # 快速停止
nginx -s quit # 优雅停止
nginx -s reopen # 重新打开日志
nginx -V # 查看编译参数
提示 生产环境推荐使用 nginx -s reload 来应用配置变更,该操作是零停机的。
03. Windows 安装与配置
Nginx 官方提供 Windows 预编译包。虽然 Windows 版主要用于开发和测试,但掌握其安装方法对本地开发非常有用。
注意 Windows 版 Nginx 不支持 select 以外的事件模型,性能远不如 Linux 版。 生产环境请使用 Linux。
方法一:下载官方压缩包
- 下载 Nginx访问 nginx.org/download,下载 Stable version 的 Windows zip 包。
- 解压到目标目录建议解压到 C:\nginx,路径中不要包含中文或空格。
- 启动 Nginx
bash
cd C:\nginx
.\nginx.exe
# 验证:浏览器访问 http://localhost
tasklist /fi "imagename eq nginx.exe"
Windows 管理命令
bash
cd C:\nginx
.\nginx.exe -t # 测试配置
.\nginx.exe -s reload # 重载配置
.\nginx.exe -s stop # 快速停止
.\nginx.exe -s quit # 优雅停止
重要 Windows 没有 systemctl ,Nginx 不会自动作为服务运行。关闭命令行后 Nginx 仍在后台,需用 nginx -s stop 或任务管理器终止。
端口冲突排查
bash
netstat -ano | findstr ":80 "
iisreset /stop # 停止 IIS
net stop W3SVC # 停止 WWW 服务
方法二:Chocolatey
choco install nginx -y
方法三:Scoop
csharp
scoop bucket add main
scoop install nginx
方法四:Docker Desktop
bash
version: "3.8"
services:
nginx:
image: nginx:stable
ports: ["80:80", "443:443"]
volumes:
- ./html:/usr/share/nginx/html
- ./nginx.conf:/etc/nginx/nginx.conf:ro
restart: unless-stopped
注册为系统服务(NSSM)
swift
# 下载: https://nssm.cc/download
nssm install Nginx "C:\nginx\nginx.exe"
nssm set Nginx AppDirectory "C:\nginx"
nssm set Nginx Start SERVICE_AUTO_START
nssm start Nginx
Windows 配置示例
ini
worker_processes 1;
events { worker_connections 1024; }
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
gzip on;
server {
listen 80;
server_name localhost;
location / {
root html;
index index.html;
}
location /api/ {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
include conf.d/*.conf;
}
各平台安装方式对比
| 平台 | 推荐方式 | 命令 | 场景 |
|---|---|---|---|
| Ubuntu/Debian | apt | apt install nginx | 生产 |
| CentOS/RHEL | dnf | dnf install nginx | 生产 |
| macOS | Homebrew | brew install nginx | 开发 |
| Windows | zip | 下载解压 | 开发 |
| Windows | Chocolatey | choco install nginx | 开发 |
| Windows | Scoop | scoop install nginx | 开发 |
| Windows | Docker | docker run nginx | 一致性 |
04. 核心概念
请求处理模型
Nginx 采用一个 Master 进程 + 多个 Worker 进程的模型。
scss
Client Request
│
▼
┌──────────────────────────────┐
│ Master Process │
│ (读取配置, 管理 Worker) │
└──────┬───────────┬───────────┘
│ │
┌────▼────┐ ┌────▼────┐
│Worker 1 │ │Worker 2 │
│ (epoll) │ │ (epoll) │
└────┬────┘ └────┬────┘
│ │
┌────▼────┐ ┌────▼────┐
│Backend A│ │Backend B│
└─────────┘ └─────────┘
关键目录
| 路径 | Linux | Windows |
|---|---|---|
| 主配置 | /etc/nginx/nginx.conf | C:\nginx\conf\nginx.conf |
| 附加配置 | /etc/nginx/conf.d/ | C:\nginx\conf.d\ |
| 站点根 | /var/www/ | C:\nginx\html\ |
| 日志 | /var/log/nginx/ | C:\nginx\logs\ |
| MIME | /etc/nginx/mime.types | C:\nginx\conf\mime.types |
05. 配置文件结构
ini
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
use epoll;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
sendfile on;
gzip on;
include /etc/nginx/conf.d/*.conf;
server {
listen 80;
server_name example.com;
root /var/www/example;
location / { index index.html; }
location /api/ { proxy_pass http://127.0.0.1:3000; }
}
}
配置层次
css
main ← 全局
├── events ← 事件
└── http ← HTTP
├── server ← 虚拟主机
│ └── location ← URL
└── upstream ← 后端组
注意 修改配置后务必先执行 nginx -t 测试语法。
06. 虚拟主机
ini
server {
listen 80;
server_name site-a.com www.site-a.com;
root /var/www/site-a;
location / { try_files $uri $uri/ =404; }
}
# 默认服务器 --- 处理未匹配请求
server {
listen 80 default_server;
server_name _;
return 444;
}
07. Location 路由匹配
| 语法 | 类型 | 优先级 |
|---|---|---|
| = /path | 精确匹配 | 最高 |
| ^~ /path | 前缀匹配 | 高 |
| ~ pattern | 正则(区分大小写) | 中 |
| ~* pattern | 正则(不区分) | 中 |
| /path | 普通前缀 | 低 |
实战示例
bash
location = / { proxy_pass http://127.0.0.1:3000/home; }
location ~* .(js|css|png|jpg)$ { expires 30d; access_log off; }
location ^~ /api/ { proxy_pass http://backend; }
location / { try_files $uri $uri/ /index.html; }
location ~ /. { deny all; }
try_files try_files <math xmlns="http://www.w3.org/1998/Math/MathML"> u r i uri </math>uriuri/ /index.html --- 先尝试文件 → 再尝试目录 → 都不存在返回 index.html(适合 SPA)。
08. 反向代理
ini
server {
listen 80;
server_name api.example.com;
location / {
proxy_pass http://127.0.0.1:3000;
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;
proxy_connect_timeout 60s;
proxy_read_timeout 60s;
}
}
09. 静态资源服务
bash
server {
listen 80;
root /var/www/static;
sendfile on;
gzip on;
gzip_types text/plain text/css application/javascript
application/json image/svg+xml font/woff2;
location ~* .(js|css|png|jpg|svg|woff2)$ {
expires 30d;
add_header Cache-Control "public, immutable";
}
location ~ /. { deny all; }
}
10. HTTPS / SSL
Let's Encrypt
bash
sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d example.com
echo "0 3 * * * certbot renew --quiet" | sudo crontab -
手动配置
ini
server {
listen 80;
server_name example.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name example.com;
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_prefer_server_ciphers on;
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
root /var/www/example;
}
11. 负载均衡
| 算法 | 指令 | 说明 |
|---|---|---|
| 轮询 | --- | 按顺序分配 |
| 加权轮询 | weight=N | 按权重分配 |
| IP Hash | ip_hash | 同 IP 同后端 |
| 最少连接 | least_conn | 最少连接优先 |
ini
upstream backend {
server 10.0.0.1:8080 weight=3;
server 10.0.0.2:8080 weight=2;
server 10.0.0.3:8080 backup;
}
server {
listen 80;
location / {
proxy_pass http://backend;
proxy_next_upstream error timeout http_502 http_503;
}
}
12. 缓存配置
ini
proxy_cache_path /var/cache/nginx
levels=1:2 keys_zone=my_cache:10m
max_size=1g inactive=60m use_temp_path=off;
location / {
proxy_pass http://backend;
proxy_cache my_cache;
proxy_cache_valid 200 302 10m;
proxy_cache_valid 404 1m;
add_header X-Cache-Status $upstream_cache_status;
}
13. Rewrite 规则
bash
# 强制 www
server {
listen 80;
server_name example.com;
return 301 http://www.example.com$request_uri;
}
# URL 美化
rewrite ^/article/(\d+)$ /article.php?id=$1 last;
# 旧路径重定向
rewrite ^/old-page$ /new-page permanent;
14. WebSocket 代理
ini
location /ws/ {
proxy_pass http://ws_backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 3600s;
proxy_buffering off;
}
15. 安全加固
安全响应头
scss
server_tokens off;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
限流
ini
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
location /api/ {
limit_req zone=api burst=20 nodelay;
limit_req_status 429;
proxy_pass http://backend;
}
16. 性能优化
ini
worker_processes auto;
worker_rlimit_nofile 65535;
events {
worker_connections 10240;
multi_accept on;
use epoll;
}
http {
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
keepalive_requests 1000;
gzip on;
gzip_comp_level 4;
gzip_static on;
}
17. 日志与监控
JSON 日志
css
log_format json_log escape=json
'{"time":"$time_iso8601","addr":"$remote_addr",'
'"method":"$request_method","uri":"$request_uri",'
'"status":$status,"time_s":$request_time}';
access_log /var/log/nginx/access.json json_log;
常用变量
| 变量 | 说明 |
|---|---|
| $remote_addr | 客户端 IP |
| $request_uri | 完整 URI |
| $status | 状态码 |
| $request_time | 处理时间 |
| $upstream_cache_status | 缓存状态 |
18. 常见问题
Q: 配置修改后不生效?
先 nginx -t 测试语法,再 nginx -s reload 重载。
Q: 502 Bad Gateway?
- 后端服务是否运行
- proxy_pass 地址端口是否正确
- 防火墙是否放行
- 查看 error.log
Q: 504 Gateway Timeout?
ini
proxy_connect_timeout 120s;
proxy_read_timeout 120s;
Q: 413 Request Entity Too Large?
ini
client_max_body_size 100m;
Q: Windows 启动失败?
- 检查 80 端口占用:netstat -ano | findstr ":80"
- 运行 nginx -t 检查配置
- 路径中不要有中文或空格
- 以管理员权限运行
Q: CORS 跨域配置?
bash
location /api/ {
add_header Access-Control-Allow-Origin $http_origin always;
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always;
add_header Access-Control-Allow-Headers "Authorization, Content-Type" always;
if ($request_method = OPTIONS) { return 204; }
proxy_pass http://backend;
}
学习资源
- Nginx 官方文档
- Nginx GitHub
- Nginx Admin's Handbook
- NSSM
html
<!DOCTYPE html>
<html lang="zh-CN" data-theme="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Nginx 完全教程</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;600;700&family=Noto+Serif+SC:wght@400;600;700&family=DM+Sans:wght@400;500;700&display=swap" rel="stylesheet">
<style>
:root, [data-theme="dark"] {
--bg: #0a0e14; --bg-surface: #111820; --bg-card: #151d28;
--bg-code: #0d1117; --accent: #00e59b; --accent-dim: #00e59b33;
--accent-glow: #00e59b15; --text-primary: #e6edf3;
--text-body: #b0bec5; --text-muted: #5c6b7a;
--border: #1e2a38; --border-accent: #00e59b44;
--tag-blue: #58a6ff; --tag-orange: #f0883e; --tag-purple: #bc8cff;
--tag-red: #ff7b72; --tag-yellow: #e3b341;
--info-bg: #00e59b0d; --info-text: #b0f0d8;
--warn-bg: #e3b3410d; --warn-text: #f0dca0;
--danger-bg: #ff7b720d; --danger-text: #ffc1bb;
--hero-gradient: radial-gradient(ellipse at 30% 0%, #00e59b22 0%, transparent 60%);
--nav-hover-bg: #00e59b10; --table-hover: #00e59b08;
--scrollbar-thumb: #1e2a38; --noise-opacity: 0.03;
--toggle-bg: #1e2a38; --toggle-icon: #e3b341;
--shadow-sm: 0 1px 3px rgba(0,0,0,0.3);
--shadow-md: 0 4px 12px rgba(0,0,0,0.4);
}
[data-theme="light"] {
--bg: #f8f6f1; --bg-surface: #ffffff; --bg-card: #ffffff;
--bg-code: #f4f1ec; --accent: #00875a; --accent-dim: #00875a22;
--accent-glow: #00875a0a; --text-primary: #1a1a1a;
--text-body: #4a4a4a; --text-muted: #8a8a8a;
--border: #e0ddd6; --border-accent: #00875a44;
--tag-blue: #0969da; --tag-orange: #bc4c00; --tag-purple: #8250df;
--tag-red: #cf222e; --tag-yellow: #9a6700;
--info-bg: #00875a10; --info-text: #005a3a;
--warn-bg: #9a670010; --warn-text: #6b4c00;
--danger-bg: #cf222e10; --danger-text: #8c1d18;
--hero-gradient: radial-gradient(ellipse at 30% 0%, #00875a15 0%, transparent 60%);
--nav-hover-bg: #00875a08; --table-hover: #00875a06;
--scrollbar-thumb: #d0cdc6; --noise-opacity: 0.015;
--toggle-bg: #e8e5de; --toggle-icon: #00875a;
--shadow-sm: 0 1px 3px rgba(0,0,0,0.08);
--shadow-md: 0 4px 12px rgba(0,0,0,0.1);
}
* { margin: 0; padding: 0; box-sizing: border-box; }
html { scroll-behavior: smooth; }
body {
font-family: 'DM Sans', sans-serif; background: var(--bg);
color: var(--text-body); line-height: 1.75; font-size: 16px;
overflow-x: hidden; transition: background 0.35s, color 0.35s;
}
body::before {
content: ''; position: fixed; inset: 0;
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='1'/%3E%3C/svg%3E");
opacity: var(--noise-opacity); pointer-events: none; z-index: 9999;
}
.layout { display: grid; grid-template-columns: 280px 1fr; min-height: 100vh; }
.sidebar {
position: sticky; top: 0; height: 100vh; background: var(--bg-surface);
border-right: 1px solid var(--border); padding: 32px 0;
overflow-y: auto; scrollbar-width: thin;
scrollbar-color: var(--scrollbar-thumb) transparent;
display: flex; flex-direction: column;
transition: background 0.35s, border-color 0.35s;
}
.sidebar-logo {
padding: 0 24px 28px; border-bottom: 1px solid var(--border);
margin-bottom: 20px; display: flex; align-items: center;
justify-content: space-between;
}
.sidebar-logo-left h2 {
font-family: 'JetBrains Mono', monospace; font-size: 20px;
font-weight: 700; color: var(--text-primary);
display: flex; align-items: center; gap: 10px;
}
.sidebar-logo-left h2 .icon {
width: 32px; height: 32px; background: var(--accent);
border-radius: 6px; display: flex; align-items: center;
justify-content: center; color: #fff; font-weight: 700; font-size: 16px;
}
.sidebar-logo-left p {
font-size: 12px; color: var(--text-muted); margin-top: 6px;
font-family: 'JetBrains Mono', monospace;
}
.theme-toggle {
width: 44px; height: 24px; border-radius: 12px;
background: var(--toggle-bg); border: 1px solid var(--border);
cursor: pointer; position: relative; flex-shrink: 0;
transition: background 0.3s;
}
.theme-toggle::after {
content: ''; position: absolute; top: 2px; left: 2px;
width: 18px; height: 18px; border-radius: 50%;
background: var(--toggle-icon);
transition: transform 0.3s ease; box-shadow: 0 1px 3px rgba(0,0,0,0.2);
}
[data-theme="light"] .theme-toggle::after { transform: translateX(20px); }
.theme-toggle .toggle-label {
position: absolute; top: 50%; transform: translateY(-50%);
font-size: 10px; pointer-events: none;
}
.theme-toggle .sun { right: 5px; opacity: 0.4; }
.theme-toggle .moon { left: 5px; opacity: 1; }
[data-theme="light"] .theme-toggle .sun { opacity: 1; }
[data-theme="light"] .theme-toggle .moon { opacity: 0.4; }
.sidebar-nav-scroll { flex: 1; overflow-y: auto; }
.nav-group { margin-bottom: 8px; }
.nav-group-title {
font-family: 'JetBrains Mono', monospace; font-size: 10px;
font-weight: 600; text-transform: uppercase; letter-spacing: 0.12em;
color: var(--text-muted); padding: 12px 24px 6px;
}
.nav-link {
display: block; padding: 8px 24px; font-size: 13.5px;
color: var(--text-body); text-decoration: none;
border-left: 2px solid transparent;
font-family: 'JetBrains Mono', monospace; font-weight: 400;
transition: color 0.2s, background 0.2s, border-color 0.2s;
}
.nav-link:hover {
color: var(--accent); background: var(--nav-hover-bg);
border-left-color: var(--accent);
}
.nav-link.active {
color: var(--accent); border-left-color: var(--accent);
background: var(--nav-hover-bg);
}
.sidebar-footer {
padding: 16px 24px; border-top: 1px solid var(--border); flex-shrink: 0;
}
.sidebar-export-btn {
display: flex; align-items: center; justify-content: center; gap: 8px;
width: 100%; padding: 9px 16px; background: var(--accent-dim);
border: 1px solid var(--border-accent); border-radius: 6px;
color: var(--accent); font-family: 'JetBrains Mono', monospace;
font-size: 12px; font-weight: 600; cursor: pointer;
transition: background 0.2s, color 0.2s;
}
.sidebar-export-btn:hover { background: var(--accent); color: #fff; }
.sidebar-export-btn svg { flex-shrink: 0; }
.main { max-width: 860px; padding: 56px 64px 120px; }
.hero { margin-bottom: 64px; position: relative; }
.hero::before {
content: ''; position: absolute; top: -56px; left: -64px; right: -64px;
height: 400px; background: var(--hero-gradient); pointer-events: none;
}
.hero-tag {
font-family: 'JetBrains Mono', monospace; font-size: 12px;
font-weight: 600; color: var(--accent); text-transform: uppercase;
letter-spacing: 0.15em; margin-bottom: 16px; position: relative;
}
.hero h1 {
font-family: 'Noto Serif SC', serif; font-size: 48px;
font-weight: 700; color: var(--text-primary); line-height: 1.2;
margin-bottom: 20px; position: relative;
}
.hero p { font-size: 17px; color: var(--text-body); max-width: 600px; position: relative; }
.hero-meta {
margin-top: 28px; display: flex; gap: 24px;
font-family: 'JetBrains Mono', monospace;
font-size: 12px; color: var(--text-muted); position: relative;
}
.hero-meta span { display: flex; align-items: center; gap: 6px; }
.hero-meta .dot { width: 6px; height: 6px; border-radius: 50%; background: var(--accent); }
.hero-actions {
margin-top: 32px; position: relative;
display: flex; gap: 12px; flex-wrap: wrap;
}
.export-btn {
display: inline-flex; align-items: center; gap: 8px;
padding: 11px 28px; background: var(--accent); color: #fff;
border: none; border-radius: 8px;
font-family: 'JetBrains Mono', monospace; font-size: 13px;
font-weight: 600; cursor: pointer; transition: all 0.2s;
}
.export-btn:hover { opacity: 0.88; transform: translateY(-1px); box-shadow: 0 6px 24px var(--accent-dim); }
.export-btn svg { flex-shrink: 0; }
.copy-btn {
display: inline-flex; align-items: center; gap: 8px;
padding: 11px 24px; background: transparent;
color: var(--text-body); border: 1px solid var(--border);
border-radius: 8px; font-family: 'JetBrains Mono', monospace;
font-size: 13px; font-weight: 500; cursor: pointer;
transition: border-color 0.2s, color 0.2s;
}
.copy-btn:hover { border-color: var(--accent); color: var(--accent); }
.copy-btn svg { flex-shrink: 0; }
.section { margin-bottom: 72px; animation: fadeUp 0.5s ease forwards; opacity: 0; }
.section:nth-child(2) { animation-delay: 0.05s; }
.section:nth-child(3) { animation-delay: 0.1s; }
.section:nth-child(4) { animation-delay: 0.15s; }
@keyframes fadeUp { from { opacity:0; transform:translateY(16px); } to { opacity:1; transform:translateY(0); } }
.section-anchor { display: block; position: relative; top: -80px; visibility: hidden; }
.section-header {
display: flex; align-items: center; gap: 14px;
margin-bottom: 24px; padding-bottom: 16px;
border-bottom: 1px solid var(--border);
}
.section-number {
font-family: 'JetBrains Mono', monospace; font-size: 13px;
font-weight: 700; color: #fff; background: var(--accent);
width: 30px; height: 30px; border-radius: 6px;
display: flex; align-items: center; justify-content: center;
}
.section-header h2 {
font-family: 'Noto Serif SC', serif; font-size: 28px;
font-weight: 700; color: var(--text-primary);
}
h3 {
font-family: 'DM Sans', sans-serif; font-size: 20px;
font-weight: 700; color: var(--text-primary); margin: 36px 0 14px;
}
h4 {
font-family: 'JetBrains Mono', monospace; font-size: 14px;
font-weight: 600; color: var(--tag-blue); margin: 28px 0 10px;
text-transform: uppercase; letter-spacing: 0.06em;
}
p { margin-bottom: 16px; }
strong { color: var(--text-primary); font-weight: 600; }
a { color: var(--accent); text-decoration: none; }
a:hover { text-decoration: underline; }
.code-block {
position: relative; margin: 20px 0 28px; border-radius: 10px;
overflow: hidden; border: 1px solid var(--border);
background: var(--bg-code); box-shadow: var(--shadow-sm);
}
.code-header {
display: flex; align-items: center; justify-content: space-between;
padding: 10px 16px; background: var(--bg-surface);
border-bottom: 1px solid var(--border);
}
.code-header .lang {
font-family: 'JetBrains Mono', monospace; font-size: 11px;
font-weight: 600; color: var(--text-muted);
text-transform: uppercase; letter-spacing: 0.08em;
}
.code-header .dots { display: flex; gap: 6px; }
.code-header .dots span { width: 10px; height: 10px; border-radius: 50%; }
.code-header .dots span:nth-child(1) { background: #ff5f57; }
.code-header .dots span:nth-child(2) { background: #ffbd2e; }
.code-header .dots span:nth-child(3) { background: #28c840; }
pre {
padding: 20px; overflow-x: auto;
font-family: 'JetBrains Mono', monospace;
font-size: 13.5px; line-height: 1.7; color: var(--text-primary);
}
pre code { font-family: inherit; }
.cm { color: var(--text-muted); }
.kw { color: var(--tag-purple); }
.fn { color: #d2a8ff; }
.st { color: var(--tag-blue); }
.nb { color: var(--tag-orange); }
.nr { color: var(--tag-yellow); }
.at { color: var(--accent); }
[data-theme="light"] .fn { color: #8250df; }
code:not(pre code) {
font-family: 'JetBrains Mono', monospace; font-size: 0.88em;
background: var(--accent-dim); color: var(--accent);
padding: 2px 7px; border-radius: 4px; font-weight: 500;
}
ul, ol { margin: 0 0 20px 24px; }
li { margin-bottom: 8px; padding-left: 4px; }
li::marker { color: var(--accent); }
.info-box {
padding: 18px 20px; border-radius: 8px; margin: 24px 0;
font-size: 14.5px; border-left: 3px solid;
}
.info-box.tip { background: var(--info-bg); border-color: var(--accent); color: var(--info-text); }
.info-box.warn { background: var(--warn-bg); border-color: var(--tag-yellow); color: var(--warn-text); }
.info-box.danger { background: var(--danger-bg); border-color: var(--tag-red); color: var(--danger-text); }
.info-box .label {
font-family: 'JetBrains Mono', monospace; font-size: 11px;
font-weight: 700; text-transform: uppercase; letter-spacing: 0.1em;
margin-bottom: 6px; display: block;
}
.info-box.tip .label { color: var(--accent); }
.info-box.warn .label { color: var(--tag-yellow); }
.info-box.danger .label { color: var(--tag-red); }
.table-wrap {
overflow-x: auto; margin: 20px 0 28px; border-radius: 8px;
border: 1px solid var(--border); box-shadow: var(--shadow-sm);
}
table { width: 100%; border-collapse: collapse; font-size: 14px; }
thead { background: var(--bg-surface); }
th {
font-family: 'JetBrains Mono', monospace; font-size: 11px;
font-weight: 700; text-transform: uppercase; letter-spacing: 0.08em;
color: var(--text-muted); text-align: left; padding: 12px 16px;
border-bottom: 1px solid var(--border);
}
td {
padding: 12px 16px; border-bottom: 1px solid var(--border);
color: var(--text-body);
}
tr:last-child td { border-bottom: none; }
tr:hover td { background: var(--table-hover); }
td code { font-size: 12.5px; }
.diagram {
background: var(--bg-code); border: 1px solid var(--border);
border-radius: 10px; padding: 28px 24px; margin: 24px 0;
font-family: 'JetBrains Mono', monospace; font-size: 13px;
line-height: 1.6; color: var(--text-body); overflow-x: auto; white-space: pre;
}
.diagram .highlight { color: var(--accent); font-weight: 600; }
.diagram .accent2 { color: var(--tag-blue); }
.steps { counter-reset: step; list-style: none; margin: 20px 0; padding: 0; }
.steps li { counter-increment: step; position: relative; padding-left: 48px; margin-bottom: 20px; }
.steps li::before {
content: counter(step); position: absolute; left: 0; top: 0;
width: 32px; height: 32px; border-radius: 50%;
background: var(--accent-dim); border: 1px solid var(--accent);
color: var(--accent); font-family: 'JetBrains Mono', monospace;
font-size: 13px; font-weight: 700;
display: flex; align-items: center; justify-content: center;
}
.divider { height: 1px; background: var(--border); margin: 48px 0; }
.mobile-toggle {
display: none; position: fixed; top: 16px; left: 16px; z-index: 1000;
width: 40px; height: 40px; border-radius: 8px;
background: var(--bg-surface); border: 1px solid var(--border);
color: var(--text-primary); font-size: 20px; cursor: pointer;
align-items: center; justify-content: center;
}
.mobile-theme-btn {
display: none; position: fixed; top: 16px; right: 16px; z-index: 1000;
width: 40px; height: 40px; border-radius: 8px;
background: var(--bg-surface); border: 1px solid var(--border);
color: var(--text-primary); font-size: 18px; cursor: pointer;
align-items: center; justify-content: center;
}
.sidebar-overlay {
display: none; position: fixed; inset: 0; z-index: 998;
background: rgba(0,0,0,0.4);
}
.sidebar-overlay.show { display: block; }
.toast {
position: fixed; bottom: 32px; right: 32px; z-index: 10000;
padding: 14px 28px; border-radius: 10px;
font-family: 'JetBrains Mono', monospace; font-size: 13px;
font-weight: 600; box-shadow: var(--shadow-md);
display: flex; align-items: center; gap: 10px;
animation: toastIn 0.3s ease;
}
.toast.success { background: var(--accent); color: #fff; }
.toast.info { background: var(--tag-blue); color: #fff; }
@keyframes toastIn { from { transform: translateY(20px); opacity: 0; } to { transform: translateY(0); opacity: 1; } }
@media (max-width: 900px) {
.layout { grid-template-columns: 1fr; }
.sidebar { position: fixed; left: -300px; top: 0; width: 280px; z-index: 999; transition: left 0.3s; }
.sidebar.open { left: 0; }
.mobile-toggle, .mobile-theme-btn { display: flex; }
.main { padding: 80px 24px 80px; }
.hero h1 { font-size: 32px; }
.hero::before { left: -24px; right: -24px; }
}
::-webkit-scrollbar { width: 6px; height: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--scrollbar-thumb); border-radius: 3px; }
</style>
</head>
<body>
<button class="mobile-toggle" id="mobileToggle">☰</button>
<button class="mobile-theme-btn" id="mobileThemeBtn">◑</button>
<div class="sidebar-overlay" id="overlay"></div>
<div class="layout">
<nav class="sidebar" id="sidebar">
<div class="sidebar-logo">
<div class="sidebar-logo-left">
<h2><span class="icon">N</span> Nginx 教程</h2>
<p>v1.24 · 完全指南</p>
</div>
<div class="theme-toggle" id="themeToggle" title="切换主题">
<span class="toggle-label sun">☀</span>
<span class="toggle-label moon">☾</span>
</div>
</div>
<div class="sidebar-nav-scroll" id="navScroll">
<div class="nav-group">
<div class="nav-group-title">入门基础</div>
<a href="#intro" class="nav-link active">简介</a>
<a href="#install-linux" class="nav-link">Linux 安装</a>
<a href="#install-windows" class="nav-link">Windows 安装</a>
<a href="#concepts" class="nav-link">核心概念</a>
</div>
<div class="nav-group">
<div class="nav-group-title">配置详解</div>
<a href="#structure" class="nav-link">配置文件结构</a>
<a href="#virtual-host" class="nav-link">虚拟主机</a>
<a href="#location" class="nav-link">Location 路由</a>
<a href="#proxy" class="nav-link">反向代理</a>
</div>
<div class="nav-group">
<div class="nav-group-title">进阶功能</div>
<a href="#static" class="nav-link">静态资源服务</a>
<a href="#ssl" class="nav-link">HTTPS / SSL</a>
<a href="#loadbalance" class="nav-link">负载均衡</a>
<a href="#cache" class="nav-link">缓存配置</a>
<a href="#rewrite" class="nav-link">Rewrite 规则</a>
</div>
<div class="nav-group">
<div class="nav-group-title">实战与运维</div>
<a href="#websocket" class="nav-link">WebSocket 代理</a>
<a href="#security" class="nav-link">安全加固</a>
<a href="#performance" class="nav-link">性能优化</a>
<a href="#logging" class="nav-link">日志与监控</a>
<a href="#faq" class="nav-link">常见问题</a>
</div>
</div>
<div class="sidebar-footer">
<button class="sidebar-export-btn" id="sidebarExportBtn" title="导出为 Markdown 文件">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
导出 Markdown
</button>
</div>
</nav>
<main class="main">
<div class="hero">
<div class="hero-tag">Open-Source Web Server</div>
<h1>Nginx 完全教程</h1>
<p>从零开始掌握 Nginx 的安装、配置与运维。涵盖反向代理、负载均衡、HTTPS、缓存、安全加固等核心知识,配合大量实战配置示例。</p>
<div class="hero-meta">
<span><span class="dot"></span> 阅读时间约 30 分钟</span>
<span><span class="dot"></span> 最后更新 2024</span>
<span><span class="dot"></span> Nginx 1.24+</span>
</div>
<div class="hero-actions">
<button class="export-btn" id="heroExportBtn">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
导出 Markdown 文档
</button>
<button class="copy-btn" id="copyBtn">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
复制到剪贴板
</button>
</div>
</div>
<!-- 01 -->
<div class="section" id="intro">
<span class="section-anchor"></span>
<div class="section-header"><span class="section-number">01</span><h2>Nginx 简介</h2></div>
<p><strong>Nginx</strong>(发音 "engine-x")是一个高性能的 HTTP 和反向代理服务器,由 Igor Sysoev 于 2004 年首次发布。它以<strong>事件驱动、异步非阻塞</strong>的架构闻名,能够以极低的内存消耗处理数万并发连接。</p>
<h3>Nginx 能做什么?</h3>
<ul>
<li><strong>HTTP 服务器</strong> --- 高效提供静态文件服务</li>
<li><strong>反向代理</strong> --- 将请求转发给后端应用服务器</li>
<li><strong>负载均衡</strong> --- 在多台后端之间分发流量</li>
<li><strong>SSL/TLS 终端</strong> --- 处理 HTTPS 加密解密</li>
<li><strong>API 网关</strong> --- 路由、限流、鉴权</li>
<li><strong>邮件代理</strong> --- IMAP/POP3/SMTP 代理</li>
</ul>
<h3>Nginx vs Apache</h3>
<div class="table-wrap"><table>
<thead><tr><th>特性</th><th>Nginx</th><th>Apache</th></tr></thead>
<tbody>
<tr><td>架构模型</td><td>事件驱动 / 异步非阻塞</td><td>进程 / 线程驱动</td></tr>
<tr><td>并发性能</td><td>极高(C10K+)</td><td>中等</td></tr>
<tr><td>内存占用</td><td>低</td><td>相对较高</td></tr>
<tr><td>静态文件</td><td>极快</td><td>良好</td></tr>
<tr><td>动态内容</td><td>需借助外部(PHP-FPM等)</td><td>内置 mod_php</td></tr>
<tr><td>配置风格</td><td>集中式、声明式</td><td>.htaccess 分散式</td></tr>
</tbody>
</table></div>
</div>
<!-- 02 -->
<div class="section" id="install-linux">
<span class="section-anchor"></span>
<div class="section-header"><span class="section-number">02</span><h2>Linux 安装与启动</h2></div>
<h3>Ubuntu / Debian</h3>
<div class="code-block"><div class="code-header"><div class="dots"><span></span><span></span><span></span></div><span class="lang">bash</span></div>
<pre><code><span class="cm"># 更新软件源并安装</span>
<span class="kw">sudo</span> apt update
<span class="kw">sudo</span> apt <span class="kw">install</span> -y nginx
<span class="cm"># 启动 & 设为开机自启</span>
<span class="kw">sudo</span> systemctl start nginx
<span class="kw">sudo</span> systemctl enable nginx
<span class="cm"># 验证安装</span>
nginx -v</code></pre></div>
<h3>CentOS / RHEL / Fedora</h3>
<div class="code-block"><div class="code-header"><div class="dots"><span></span><span></span><span></span></div><span class="lang">bash</span></div>
<pre><code><span class="cm"># CentOS 7</span>
<span class="kw">sudo</span> yum install -y epel-release
<span class="kw">sudo</span> yum install -y nginx
<span class="cm"># CentOS 8+ / Fedora</span>
<span class="kw">sudo</span> dnf install -y nginx
<span class="kw">sudo</span> systemctl start nginx
<span class="kw">sudo</span> systemctl enable nginx</code></pre></div>
<h3>macOS (Homebrew)</h3>
<div class="code-block"><div class="code-header"><div class="dots"><span></span><span></span><span></span></div><span class="lang">bash</span></div>
<pre><code>brew install nginx
brew services start nginx</code></pre></div>
<h3>Docker</h3>
<div class="code-block"><div class="code-header"><div class="dots"><span></span><span></span><span></span></div><span class="lang">bash</span></div>
<pre><code>docker run -d --name nginx \
-p <span class="nr">80</span>:<span class="nr">80</span> \
-v /path/to/html:/usr/share/nginx/html \
-v /path/to/nginx.conf:/etc/nginx/nginx.conf \
nginx:stable</code></pre></div>
<h3>常用管理命令</h3>
<div class="code-block"><div class="code-header"><div class="dots"><span></span><span></span><span></span></div><span class="lang">bash</span></div>
<pre><code>nginx -t <span class="cm"># 测试配置语法</span>
nginx -T <span class="cm"># 测试并输出完整配置</span>
nginx -s reload <span class="cm"># 平滑重载(不中断服务)</span>
nginx -s stop <span class="cm"># 快速停止</span>
nginx -s quit <span class="cm"># 优雅停止</span>
nginx -s reopen <span class="cm"># 重新打开日志</span>
nginx -V <span class="cm"># 查看编译参数</span></code></pre></div>
<div class="info-box tip"><span class="label">提示</span>
生产环境推荐使用 <code>nginx -s reload</code> 来应用配置变更,该操作是零停机的。
</div>
</div>
<!-- 03 -->
<div class="section" id="install-windows">
<span class="section-anchor"></span>
<div class="section-header"><span class="section-number">03</span><h2>Windows 安装与配置</h2></div>
<p>Nginx 官方提供 Windows 预编译包。虽然 Windows 版主要用于<strong>开发和测试</strong>,但掌握其安装方法对本地开发非常有用。</p>
<div class="info-box warn"><span class="label">注意</span>
Windows 版 Nginx 不支持 <code>select</code> 以外的事件模型,性能远不如 Linux 版。<strong>生产环境请使用 Linux。</strong>
</div>
<h3>方法一:下载官方压缩包</h3>
<ol class="steps">
<li><strong>下载 Nginx</strong><p>访问 <a href="https://nginx.org/en/download.html" target="_blank">nginx.org/download</a>,下载 Stable version 的 Windows zip 包。</p></li>
<li><strong>解压到目标目录</strong><p>建议解压到 <code>C:\nginx</code>,路径中<strong>不要包含中文或空格</strong>。</p></li>
<li><strong>启动 Nginx</strong></li>
</ol>
<div class="code-block"><div class="code-header"><div class="dots"><span></span><span></span><span></span></div><span class="lang">cmd / PowerShell</span></div>
<pre><code><span class="kw">cd</span> C:\nginx
.\nginx.exe
<span class="cm"># 验证:浏览器访问 http://localhost</span>
tasklist /fi <span class="st">"imagename eq nginx.exe"</span></code></pre></div>
<h3>Windows 管理命令</h3>
<div class="code-block"><div class="code-header"><div class="dots"><span></span><span></span><span></span></div><span class="lang">cmd / PowerShell</span></div>
<pre><code><span class="kw">cd</span> C:\nginx
.\nginx.exe -t <span class="cm"># 测试配置</span>
.\nginx.exe -s reload <span class="cm"># 重载配置</span>
.\nginx.exe -s stop <span class="cm"># 快速停止</span>
.\nginx.exe -s quit <span class="cm"># 优雅停止</span></code></pre></div>
<div class="info-box danger"><span class="label">重要</span>
Windows 没有 <code>systemctl</code>,Nginx 不会自动作为服务运行。关闭命令行后 Nginx 仍在后台,需用 <code>nginx -s stop</code> 或任务管理器终止。
</div>
<h3>端口冲突排查</h3>
<div class="code-block"><div class="code-header"><div class="dots"><span></span><span></span><span></span></div><span class="lang">PowerShell (管理员)</span></div>
<pre><code>netstat -ano | findstr <span class="st">":80 "</span>
iisreset /stop <span class="cm"># 停止 IIS</span>
net stop W3SVC <span class="cm"># 停止 WWW 服务</span></code></pre></div>
<h3>方法二:Chocolatey</h3>
<div class="code-block"><div class="code-header"><div class="dots"><span></span><span></span><span></span></div><span class="lang">PowerShell (管理员)</span></div>
<pre><code>choco install nginx -y</code></pre></div>
<h3>方法三:Scoop</h3>
<div class="code-block"><div class="code-header"><div class="dots"><span></span><span></span><span></span></div><span class="lang">PowerShell</span></div>
<pre><code>scoop bucket add main
scoop install nginx</code></pre></div>
<h3>方法四:Docker Desktop</h3>
<div class="code-block"><div class="code-header"><div class="dots"><span></span><span></span><span></span></div><span class="lang">docker-compose.yml</span></div>
<pre><code><span class="kw">version</span>: <span class="st">"3.8"</span>
<span class="kw">services</span>:
<span class="at">nginx</span>:
<span class="at">image</span>: nginx:stable
<span class="at">ports</span>: [<span class="st">"80:80"</span>, <span class="st">"443:443"</span>]
<span class="at">volumes</span>:
- ./html:/usr/share/nginx/html
- ./nginx.conf:/etc/nginx/nginx.conf:ro
<span class="at">restart</span>: unless-stopped</code></pre></div>
<h3>注册为系统服务(NSSM)</h3>
<div class="code-block"><div class="code-header"><div class="dots"><span></span><span></span><span></span></div><span class="lang">PowerShell (管理员)</span></div>
<pre><code><span class="cm"># 下载: https://nssm.cc/download</span>
nssm install Nginx <span class="st">"C:\nginx\nginx.exe"</span>
nssm set Nginx AppDirectory <span class="st">"C:\nginx"</span>
nssm set Nginx Start SERVICE_AUTO_START
nssm start Nginx</code></pre></div>
<h3>Windows 配置示例</h3>
<div class="code-block"><div class="code-header"><div class="dots"><span></span><span></span><span></span></div><span class="lang">C:\nginx\conf\nginx.conf</span></div>
<pre><code><span class="kw">worker_processes</span> <span class="nr">1</span>;
<span class="kw">events</span> { <span class="kw">worker_connections</span> <span class="nr">1024</span>; }
<span class="kw">http</span> {
<span class="kw">include</span> mime.types;
<span class="kw">default_type</span> application/octet-stream;
<span class="kw">sendfile</span> <span class="nb">on</span>;
<span class="kw">gzip</span> <span class="nb">on</span>;
<span class="kw">server</span> {
<span class="kw">listen</span> <span class="nr">80</span>;
<span class="kw">server_name</span> localhost;
<span class="kw">location</span> / {
<span class="kw">root</span> html;
<span class="kw">index</span> index.html;
}
<span class="kw">location</span> /api/ {
<span class="kw">proxy_pass</span> http://<span class="nr">127.0.0.1</span>:<span class="nr">3000</span>;
<span class="kw">proxy_set_header</span> Host $host;
<span class="kw">proxy_set_header</span> X-Real-IP $remote_addr;
}
}
<span class="kw">include</span> conf.d/*.conf;
}</code></pre></div>
<h3>各平台安装方式对比</h3>
<div class="table-wrap"><table>
<thead><tr><th>平台</th><th>推荐方式</th><th>命令</th><th>场景</th></tr></thead>
<tbody>
<tr><td>Ubuntu/Debian</td><td>apt</td><td><code>apt install nginx</code></td><td>生产</td></tr>
<tr><td>CentOS/RHEL</td><td>dnf</td><td><code>dnf install nginx</code></td><td>生产</td></tr>
<tr><td>macOS</td><td>Homebrew</td><td><code>brew install nginx</code></td><td>开发</td></tr>
<tr><td>Windows</td><td>zip</td><td>下载解压</td><td>开发</td></tr>
<tr><td>Windows</td><td>Chocolatey</td><td><code>choco install nginx</code></td><td>开发</td></tr>
<tr><td>Windows</td><td>Scoop</td><td><code>scoop install nginx</code></td><td>开发</td></tr>
<tr><td>Windows</td><td>Docker</td><td><code>docker run nginx</code></td><td>一致性</td></tr>
</tbody>
</table></div>
</div>
<!-- 04 -->
<div class="section" id="concepts">
<span class="section-anchor"></span>
<div class="section-header"><span class="section-number">04</span><h2>核心概念</h2></div>
<h3>请求处理模型</h3>
<p>Nginx 采用<strong>一个 Master 进程 + 多个 Worker 进程</strong>的模型。</p>
<div class="diagram"><span class="highlight">Client Request</span>
│
▼
┌──────────────────────────────┐
│ <span class="accent2">Master Process</span> │
│ (读取配置, 管理 Worker) │
└──────┬───────────┬───────────┘
│ │
┌────▼────┐ ┌────▼────┐
│<span class="highlight">Worker 1</span> │ │<span class="highlight">Worker 2</span> │
│ (epoll) │ │ (epoll) │
└────┬────┘ └────┬────┘
│ │
┌────▼────┐ ┌────▼────┐
│Backend A│ │Backend B│
└─────────┘ └─────────┘</div>
<h3>关键目录</h3>
<div class="table-wrap"><table>
<thead><tr><th>路径</th><th>Linux</th><th>Windows</th></tr></thead>
<tbody>
<tr><td>主配置</td><td><code>/etc/nginx/nginx.conf</code></td><td><code>C:\nginx\conf\nginx.conf</code></td></tr>
<tr><td>附加配置</td><td><code>/etc/nginx/conf.d/</code></td><td><code>C:\nginx\conf.d\</code></td></tr>
<tr><td>站点根</td><td><code>/var/www/</code></td><td><code>C:\nginx\html\</code></td></tr>
<tr><td>日志</td><td><code>/var/log/nginx/</code></td><td><code>C:\nginx\logs\</code></td></tr>
<tr><td>MIME</td><td><code>/etc/nginx/mime.types</code></td><td><code>C:\nginx\conf\mime.types</code></td></tr>
</tbody>
</table></div>
</div>
<!-- 05 -->
<div class="section" id="structure">
<span class="section-anchor"></span>
<div class="section-header"><span class="section-number">05</span><h2>配置文件结构</h2></div>
<div class="code-block"><div class="code-header"><div class="dots"><span></span><span></span><span></span></div><span class="lang">nginx.conf</span></div>
<pre><code><span class="kw">user</span> nginx;
<span class="kw">worker_processes</span> auto;
<span class="kw">error_log</span> /var/log/nginx/error.log <span class="nb">warn</span>;
<span class="kw">pid</span> /var/run/nginx.pid;
<span class="kw">events</span> {
<span class="kw">worker_connections</span> <span class="nr">1024</span>;
<span class="kw">use</span> epoll;
}
<span class="kw">http</span> {
<span class="kw">include</span> /etc/nginx/mime.types;
<span class="kw">default_type</span> application/octet-stream;
<span class="kw">sendfile</span> <span class="nb">on</span>;
<span class="kw">gzip</span> <span class="nb">on</span>;
<span class="kw">include</span> /etc/nginx/conf.d/*.conf;
<span class="kw">server</span> {
<span class="kw">listen</span> <span class="nr">80</span>;
<span class="kw">server_name</span> example.com;
<span class="kw">root</span> /var/www/example;
<span class="kw">location</span> / { <span class="kw">index</span> index.html; }
<span class="kw">location</span> /api/ { <span class="kw">proxy_pass</span> http://<span class="nr">127.0.0.1</span>:<span class="nr">3000</span>; }
}
}</code></pre></div>
<h3>配置层次</h3>
<div class="diagram"><span class="highlight">main</span> ← 全局
├── <span class="accent2">events</span> ← 事件
└── <span class="accent2">http</span> ← HTTP
├── <span class="accent2">server</span> ← 虚拟主机
│ └── <span class="accent2">location</span> ← URL
└── <span class="accent2">upstream</span> ← 后端组</div>
<div class="info-box warn"><span class="label">注意</span>
修改配置后务必先执行 <code>nginx -t</code> 测试语法。
</div>
</div>
<!-- 06 -->
<div class="section" id="virtual-host">
<span class="section-anchor"></span>
<div class="section-header"><span class="section-number">06</span><h2>虚拟主机</h2></div>
<div class="code-block"><div class="code-header"><div class="dots"><span></span><span></span><span></span></div><span class="lang">site-a.conf</span></div>
<pre><code><span class="kw">server</span> {
<span class="kw">listen</span> <span class="nr">80</span>;
<span class="kw">server_name</span> site-a.com www.site-a.com;
<span class="kw">root</span> /var/www/site-a;
<span class="kw">location</span> / { <span class="kw">try_files</span> $uri $uri/ =<span class="nr">404</span>; }
}
<span class="cm"># 默认服务器 --- 处理未匹配请求</span>
<span class="kw">server</span> {
<span class="kw">listen</span> <span class="nr">80</span> <span class="nb">default_server</span>;
<span class="kw">server_name</span> _;
<span class="kw">return</span> <span class="nr">444</span>;
}</code></pre></div>
</div>
<!-- 07 -->
<div class="section" id="location">
<span class="section-anchor"></span>
<div class="section-header"><span class="section-number">07</span><h2>Location 路由匹配</h2></div>
<div class="table-wrap"><table>
<thead><tr><th>语法</th><th>类型</th><th>优先级</th></tr></thead>
<tbody>
<tr><td><code>= /path</code></td><td>精确匹配</td><td>最高</td></tr>
<tr><td><code>^~ /path</code></td><td>前缀匹配</td><td>高</td></tr>
<tr><td><code>~ pattern</code></td><td>正则(区分大小写)</td><td>中</td></tr>
<tr><td><code>~* pattern</code></td><td>正则(不区分)</td><td>中</td></tr>
<tr><td><code>/path</code></td><td>普通前缀</td><td>低</td></tr>
</tbody>
</table></div>
<h3>实战示例</h3>
<div class="code-block"><div class="code-header"><div class="dots"><span></span><span></span><span></span></div><span class="lang">nginx.conf</span></div>
<pre><code><span class="kw">location</span> = / { <span class="kw">proxy_pass</span> http://<span class="nr">127.0.0.1</span>:<span class="nr">3000</span>/home; }
<span class="kw">location</span> ~* \.(js|css|png|jpg)$ { <span class="kw">expires</span> <span class="nr">30d</span>; <span class="kw">access_log</span> <span class="nb">off</span>; }
<span class="kw">location</span> ^~ /api/ { <span class="kw">proxy_pass</span> http://backend; }
<span class="kw">location</span> / { <span class="kw">try_files</span> $uri $uri/ /index.html; }
<span class="kw">location</span> ~ /\. { <span class="kw">deny</span> all; }</code></pre></div>
<div class="info-box tip"><span class="label">try_files</span>
<code>try_files $uri $uri/ /index.html</code> --- 先尝试文件 → 再尝试目录 → 都不存在返回 index.html(适合 SPA)。
</div>
</div>
<!-- 08 -->
<div class="section" id="proxy">
<span class="section-anchor"></span>
<div class="section-header"><span class="section-number">08</span><h2>反向代理</h2></div>
<div class="code-block"><div class="code-header"><div class="dots"><span></span><span></span><span></span></div><span class="lang">proxy.conf</span></div>
<pre><code><span class="kw">server</span> {
<span class="kw">listen</span> <span class="nr">80</span>;
<span class="kw">server_name</span> api.example.com;
<span class="kw">location</span> / {
<span class="kw">proxy_pass</span> http://<span class="nr">127.0.0.1</span>:<span class="nr">3000</span>;
<span class="kw">proxy_set_header</span> Host $host;
<span class="kw">proxy_set_header</span> X-Real-IP $remote_addr;
<span class="kw">proxy_set_header</span> X-Forwarded-For $proxy_add_x_forwarded_for;
<span class="kw">proxy_set_header</span> X-Forwarded-Proto $scheme;
<span class="kw">proxy_connect_timeout</span> <span class="nr">60s</span>;
<span class="kw">proxy_read_timeout</span> <span class="nr">60s</span>;
}
}</code></pre></div>
</div>
<!-- 09 -->
<div class="section" id="static">
<span class="section-anchor"></span>
<div class="section-header"><span class="section-number">09</span><h2>静态资源服务</h2></div>
<div class="code-block"><div class="code-header"><div class="dots"><span></span><span></span><span></span></div><span class="lang">static.conf</span></div>
<pre><code><span class="kw">server</span> {
<span class="kw">listen</span> <span class="nr">80</span>;
<span class="kw">root</span> /var/www/static;
<span class="kw">sendfile</span> <span class="nb">on</span>;
<span class="kw">gzip</span> <span class="nb">on</span>;
<span class="kw">gzip_types</span> text/plain text/css application/javascript
application/json image/svg+xml font/woff2;
<span class="kw">location</span> ~* \.(js|css|png|jpg|svg|woff2)$ {
<span class="kw">expires</span> <span class="nr">30d</span>;
<span class="kw">add_header</span> Cache-Control <span class="st">"public, immutable"</span>;
}
<span class="kw">location</span> ~ /\. { <span class="kw">deny</span> all; }
}</code></pre></div>
</div>
<!-- 10 -->
<div class="section" id="ssl">
<span class="section-anchor"></span>
<div class="section-header"><span class="section-number">10</span><h2>HTTPS / SSL</h2></div>
<h3>Let's Encrypt</h3>
<div class="code-block"><div class="code-header"><div class="dots"><span></span><span></span><span></span></div><span class="lang">bash</span></div>
<pre><code><span class="kw">sudo</span> apt install certbot python3-certbot-nginx
<span class="kw">sudo</span> certbot --nginx -d example.com
<span class="kw">echo</span> <span class="st">"0 3 * * * certbot renew --quiet"</span> | <span class="kw">sudo</span> crontab -</code></pre></div>
<h3>手动配置</h3>
<div class="code-block"><div class="code-header"><div class="dots"><span></span><span></span><span></span></div><span class="lang">ssl.conf</span></div>
<pre><code><span class="kw">server</span> {
<span class="kw">listen</span> <span class="nr">80</span>;
<span class="kw">server_name</span> example.com;
<span class="kw">return</span> <span class="nr">301</span> https://$host$request_uri;
}
<span class="kw">server</span> {
<span class="kw">listen</span> <span class="nr">443</span> ssl http2;
<span class="kw">server_name</span> example.com;
<span class="kw">ssl_certificate</span> /etc/letsencrypt/live/example.com/fullchain.pem;
<span class="kw">ssl_certificate_key</span> /etc/letsencrypt/live/example.com/privkey.pem;
<span class="kw">ssl_protocols</span> TLSv1.2 TLSv1.3;
<span class="kw">ssl_prefer_server_ciphers</span> <span class="nb">on</span>;
<span class="kw">add_header</span> Strict-Transport-Security <span class="st">"max-age=63072000; includeSubDomains"</span> always;
<span class="kw">root</span> /var/www/example;
}</code></pre></div>
</div>
<!-- 11 -->
<div class="section" id="loadbalance">
<span class="section-anchor"></span>
<div class="section-header"><span class="section-number">11</span><h2>负载均衡</h2></div>
<div class="table-wrap"><table>
<thead><tr><th>算法</th><th>指令</th><th>说明</th></tr></thead>
<tbody>
<tr><td>轮询</td><td>---</td><td>按顺序分配</td></tr>
<tr><td>加权轮询</td><td><code>weight=N</code></td><td>按权重分配</td></tr>
<tr><td>IP Hash</td><td><code>ip_hash</code></td><td>同 IP 同后端</td></tr>
<tr><td>最少连接</td><td><code>least_conn</code></td><td>最少连接优先</td></tr>
</tbody>
</table></div>
<div class="code-block"><div class="code-header"><div class="dots"><span></span><span></span><span></span></div><span class="lang">upstream.conf</span></div>
<pre><code><span class="kw">upstream</span> backend {
<span class="kw">server</span> <span class="nr">10.0.0.1</span>:<span class="nr">8080</span> weight=<span class="nr">3</span>;
<span class="kw">server</span> <span class="nr">10.0.0.2</span>:<span class="nr">8080</span> weight=<span class="nr">2</span>;
<span class="kw">server</span> <span class="nr">10.0.0.3</span>:<span class="nr">8080</span> backup;
}
<span class="kw">server</span> {
<span class="kw">listen</span> <span class="nr">80</span>;
<span class="kw">location</span> / {
<span class="kw">proxy_pass</span> http://backend;
<span class="kw">proxy_next_upstream</span> error timeout http_502 http_503;
}
}</code></pre></div>
</div>
<!-- 12 -->
<div class="section" id="cache">
<span class="section-anchor"></span>
<div class="section-header"><span class="section-number">12</span><h2>缓存配置</h2></div>
<div class="code-block"><div class="code-header"><div class="dots"><span></span><span></span><span></span></div><span class="lang">cache.conf</span></div>
<pre><code><span class="kw">proxy_cache_path</span> /var/cache/nginx
levels=<span class="nr">1</span>:<span class="nr">2</span> keys_zone=my_cache:<span class="nr">10m</span>
max_size=<span class="nr">1g</span> inactive=<span class="nr">60m</span> use_temp_path=off;
<span class="kw">location</span> / {
<span class="kw">proxy_pass</span> http://backend;
<span class="kw">proxy_cache</span> my_cache;
<span class="kw">proxy_cache_valid</span> <span class="nr">200</span> <span class="nr">302</span> <span class="nr">10m</span>;
<span class="kw">proxy_cache_valid</span> <span class="nr">404</span> <span class="nr">1m</span>;
<span class="kw">add_header</span> X-Cache-Status $upstream_cache_status;
}</code></pre></div>
</div>
<!-- 13 -->
<div class="section" id="rewrite">
<span class="section-anchor"></span>
<div class="section-header"><span class="section-number">13</span><h2>Rewrite 规则</h2></div>
<div class="code-block"><div class="code-header"><div class="dots"><span></span><span></span><span></span></div><span class="lang">rewrite.conf</span></div>
<pre><code><span class="cm"># 强制 www</span>
<span class="kw">server</span> {
<span class="kw">listen</span> <span class="nr">80</span>;
<span class="kw">server_name</span> example.com;
<span class="kw">return</span> <span class="nr">301</span> http://www.example.com$request_uri;
}
<span class="cm"># URL 美化</span>
<span class="kw">rewrite</span> ^/article/(\d+)$ /article.php?id=$<span class="nr">1</span> <span class="nb">last</span>;
<span class="cm"># 旧路径重定向</span>
<span class="kw">rewrite</span> ^/old-page$ /new-page <span class="nb">permanent</span>;</code></pre></div>
</div>
<!-- 14 -->
<div class="section" id="websocket">
<span class="section-anchor"></span>
<div class="section-header"><span class="section-number">14</span><h2>WebSocket 代理</h2></div>
<div class="code-block"><div class="code-header"><div class="dots"><span></span><span></span><span></span></div><span class="lang">websocket.conf</span></div>
<pre><code><span class="kw">location</span> /ws/ {
<span class="kw">proxy_pass</span> http://ws_backend;
<span class="kw">proxy_http_version</span> <span class="nr">1.1</span>;
<span class="kw">proxy_set_header</span> Upgrade $http_upgrade;
<span class="kw">proxy_set_header</span> Connection <span class="st">"upgrade"</span>;
<span class="kw">proxy_read_timeout</span> <span class="nr">3600s</span>;
<span class="kw">proxy_buffering</span> <span class="nb">off</span>;
}</code></pre></div>
</div>
<!-- 15 -->
<div class="section" id="security">
<span class="section-anchor"></span>
<div class="section-header"><span class="section-number">15</span><h2>安全加固</h2></div>
<h3>安全响应头</h3>
<div class="code-block"><div class="code-header"><div class="dots"><span></span><span></span><span></span></div><span class="lang">security.conf</span></div>
<pre><code><span class="kw">server_tokens</span> <span class="nb">off</span>;
<span class="kw">add_header</span> X-Frame-Options <span class="st">"SAMEORIGIN"</span> always;
<span class="kw">add_header</span> X-Content-Type-Options <span class="st">"nosniff"</span> always;
<span class="kw">add_header</span> X-XSS-Protection <span class="st">"1; mode=block"</span> always;
<span class="kw">add_header</span> Referrer-Policy <span class="st">"strict-origin-when-cross-origin"</span> always;</code></pre></div>
<h3>限流</h3>
<div class="code-block"><div class="code-header"><div class="dots"><span></span><span></span><span></span></div><span class="lang">limit.conf</span></div>
<pre><code><span class="kw">limit_req_zone</span> $binary_remote_addr zone=api:<span class="nr">10m</span> rate=<span class="nr">10r</span>/s;
<span class="kw">location</span> /api/ {
<span class="kw">limit_req</span> zone=api burst=<span class="nr">20</span> nodelay;
<span class="kw">limit_req_status</span> <span class="nr">429</span>;
<span class="kw">proxy_pass</span> http://backend;
}</code></pre></div>
</div>
<!-- 16 -->
<div class="section" id="performance">
<span class="section-anchor"></span>
<div class="section-header"><span class="section-number">16</span><h2>性能优化</h2></div>
<div class="code-block"><div class="code-header"><div class="dots"><span></span><span></span><span></span></div><span class="lang">performance.conf</span></div>
<pre><code><span class="kw">worker_processes</span> auto;
<span class="kw">worker_rlimit_nofile</span> <span class="nr">65535</span>;
<span class="kw">events</span> {
<span class="kw">worker_connections</span> <span class="nr">10240</span>;
<span class="kw">multi_accept</span> <span class="nb">on</span>;
<span class="kw">use</span> epoll;
}
<span class="kw">http</span> {
<span class="kw">sendfile</span> <span class="nb">on</span>;
<span class="kw">tcp_nopush</span> <span class="nb">on</span>;
<span class="kw">tcp_nodelay</span> <span class="nb">on</span>;
<span class="kw">keepalive_timeout</span> <span class="nr">65</span>;
<span class="kw">keepalive_requests</span> <span class="nr">1000</span>;
<span class="kw">gzip</span> <span class="nb">on</span>;
<span class="kw">gzip_comp_level</span> <span class="nr">4</span>;
<span class="kw">gzip_static</span> <span class="nb">on</span>;
}</code></pre></div>
</div>
<!-- 17 -->
<div class="section" id="logging">
<span class="section-anchor"></span>
<div class="section-header"><span class="section-number">17</span><h2>日志与监控</h2></div>
<h3>JSON 日志</h3>
<div class="code-block"><div class="code-header"><div class="dots"><span></span><span></span><span></span></div><span class="lang">nginx.conf</span></div>
<pre><code><span class="kw">log_format</span> json_log escape=json
<span class="st">'{"time":"$time_iso8601","addr":"$remote_addr",'</span>
<span class="st">'"method":"$request_method","uri":"$request_uri",'</span>
<span class="st">'"status":$status,"time_s":$request_time}'</span>;
<span class="kw">access_log</span> /var/log/nginx/access.json json_log;</code></pre></div>
<h3>常用变量</h3>
<div class="table-wrap"><table>
<thead><tr><th>变量</th><th>说明</th></tr></thead>
<tbody>
<tr><td><code>$remote_addr</code></td><td>客户端 IP</td></tr>
<tr><td><code>$request_uri</code></td><td>完整 URI</td></tr>
<tr><td><code>$status</code></td><td>状态码</td></tr>
<tr><td><code>$request_time</code></td><td>处理时间</td></tr>
<tr><td><code>$upstream_cache_status</code></td><td>缓存状态</td></tr>
</tbody>
</table></div>
</div>
<!-- 18 -->
<div class="section" id="faq">
<span class="section-anchor"></span>
<div class="section-header"><span class="section-number">18</span><h2>常见问题</h2></div>
<h3>Q: 配置修改后不生效?</h3>
<p>先 <code>nginx -t</code> 测试语法,再 <code>nginx -s reload</code> 重载。</p>
<h3>Q: 502 Bad Gateway?</h3>
<ul>
<li>后端服务是否运行</li>
<li><code>proxy_pass</code> 地址端口是否正确</li>
<li>防火墙是否放行</li>
<li>查看 error.log</li>
</ul>
<h3>Q: 504 Gateway Timeout?</h3>
<div class="code-block"><div class="code-header"><div class="dots"><span></span><span></span><span></span></div><span class="lang">nginx.conf</span></div>
<pre><code><span class="kw">proxy_connect_timeout</span> <span class="nr">120s</span>;
<span class="kw">proxy_read_timeout</span> <span class="nr">120s</span>;</code></pre></div>
<h3>Q: 413 Request Entity Too Large?</h3>
<div class="code-block"><div class="code-header"><div class="dots"><span></span><span></span><span></span></div><span class="lang">nginx.conf</span></div>
<pre><code><span class="kw">client_max_body_size</span> <span class="nr">100m</span>;</code></pre></div>
<h3>Q: Windows 启动失败?</h3>
<ul>
<li>检查 80 端口占用:<code>netstat -ano | findstr ":80"</code></li>
<li>运行 <code>nginx -t</code> 检查配置</li>
<li>路径中不要有中文或空格</li>
<li>以管理员权限运行</li>
</ul>
<h3>Q: CORS 跨域配置?</h3>
<div class="code-block"><div class="code-header"><div class="dots"><span></span><span></span><span></span></div><span class="lang">cors.conf</span></div>
<pre><code><span class="kw">location</span> /api/ {
<span class="kw">add_header</span> Access-Control-Allow-Origin $http_origin always;
<span class="kw">add_header</span> Access-Control-Allow-Methods <span class="st">"GET, POST, PUT, DELETE, OPTIONS"</span> always;
<span class="kw">add_header</span> Access-Control-Allow-Headers <span class="st">"Authorization, Content-Type"</span> always;
<span class="kw">if</span> ($request_method = OPTIONS) { <span class="kw">return</span> <span class="nr">204</span>; }
<span class="kw">proxy_pass</span> http://backend;
}</code></pre></div>
<div class="divider"></div>
<div class="info-box tip"><span class="label">学习资源</span>
<ul style="margin:8px 0 0 16px">
<li><a href="https://nginx.org/en/docs/">Nginx 官方文档</a></li>
<li><a href="https://github.com/nginx/nginx">Nginx GitHub</a></li>
<li><a href="https://github.com/trimstray/nginx-admins-handbook">Nginx Admin's Handbook</a></li>
<li><a href="https://nssm.cc/">NSSM</a></li>
</ul>
</div>
</div>
</main>
</div>
<script>
/* ============================================================
1) THEME TOGGLE --- 完全独立
============================================================ */
(function() {
var saved = localStorage.getItem('nginx-theme');
if (saved) {
document.documentElement.setAttribute('data-theme', saved);
}
})();
function toggleTheme() {
var html = document.documentElement;
var next = html.getAttribute('data-theme') === 'dark' ? 'light' : 'dark';
html.setAttribute('data-theme', next);
localStorage.setItem('nginx-theme', next);
}
var themeBtn = document.getElementById('themeToggle');
if (themeBtn) {
themeBtn.addEventListener('click', toggleTheme);
}
var mobileThemeBtn = document.getElementById('mobileThemeBtn');
if (mobileThemeBtn) {
mobileThemeBtn.addEventListener('click', toggleTheme);
}
/* ============================================================
2) SIDEBAR TOGGLE --- 完全独立
============================================================ */
function openSidebar() {
var sb = document.getElementById('sidebar');
var ov = document.getElementById('overlay');
if (sb) sb.classList.add('open');
if (ov) ov.classList.add('show');
}
function closeSidebar() {
var sb = document.getElementById('sidebar');
var ov = document.getElementById('overlay');
if (sb) sb.classList.remove('open');
if (ov) ov.classList.remove('show');
}
var mobileToggle = document.getElementById('mobileToggle');
if (mobileToggle) {
mobileToggle.addEventListener('click', openSidebar);
}
var overlay = document.getElementById('overlay');
if (overlay) {
overlay.addEventListener('click', closeSidebar);
}
/* ============================================================
3) SCROLL SPY --- 完全独立
============================================================ */
(function() {
try {
var sections = document.querySelectorAll('.section-anchor');
var links = document.querySelectorAll('.nav-link');
if (!sections.length || !links.length) return;
var observer = new IntersectionObserver(function(entries) {
entries.forEach(function(entry) {
if (entry.isIntersecting) {
var parentId = entry.target.parentElement.id;
links.forEach(function(link) {
var href = link.getAttribute('href');
if (href === '#' + parentId) {
link.classList.add('active');
} else {
link.classList.remove('active');
}
});
}
});
}, { rootMargin: '-80px 0px -70% 0px' });
sections.forEach(function(s) { observer.observe(s); });
links.forEach(function(link) {
link.addEventListener('click', function() {
if (window.innerWidth <= 900) {
closeSidebar();
}
});
});
} catch (e) {
console.warn('Scroll spy error:', e);
}
})();
/* ============================================================
4) TOAST --- 通知工具
============================================================ */
function showToast(message, type) {
type = type || 'success';
var toast = document.createElement('div');
toast.className = 'toast ' + type;
var icon = type === 'success'
? '\u2713'
: '\u2398';
toast.textContent = icon + ' ' + message;
document.body.appendChild(toast);
setTimeout(function() {
toast.style.opacity = '0';
toast.style.transition = 'opacity 0.3s';
setTimeout(function() {
if (toast.parentNode) toast.parentNode.removeChild(toast);
}, 300);
}, 2500);
}
/* ============================================================
5) MARKDOWN EXPORT --- 独立 + 安全包装
============================================================ */
// 递归遍历 DOM,把一个元素转换为 Markdown
function nodeToMarkdown(node) {
if (!node) return '';
// 文本节点
if (node.nodeType === 3) {
return node.textContent || '';
}
// 只处理元素节点
if (node.nodeType !== 1) return '';
var tag = node.tagName;
var cls = node.className || '';
if (typeof cls !== 'string') cls = '';
// 跳过这些
if (cls.indexOf('section-anchor') !== -1) return '';
if (cls.indexOf('code-header') !== -1) return '';
if (cls.indexOf('dots') !== -1) return '';
if (cls.indexOf('toggle-label') !== -1) return '';
// DIV 容器
if (tag === 'DIV') {
if (cls.indexOf('code-block') !== -1) {
var langEl = node.querySelector('.code-header .lang');
var codeEl = node.querySelector('pre code');
var lang = langEl ? langEl.textContent.trim().toLowerCase() : '';
var code = codeEl ? codeEl.textContent.trimEnd() : '';
return '```' + lang + '\n' + code + '\n```\n\n';
}
if (cls.indexOf('table-wrap') !== -1) {
var table = node.querySelector('table');
if (!table) return '';
var md = '';
var rows = table.querySelectorAll('tr');
for (var ri = 0; ri < rows.length; ri++) {
var cells = rows[ri].querySelectorAll('th, td');
md += '| ';
for (var ci = 0; ci < cells.length; ci++) {
md += cells[ci].textContent.trim().replace(/\|/g, '\\|') + ' | ';
}
md += '\n';
if (ri === 0) {
md += '| ';
for (var ci2 = 0; ci2 < cells.length; ci2++) { md += '--- | '; }
md += '\n';
}
}
return md + '\n';
}
if (cls.indexOf('info-box') !== -1) {
var labelEl = node.querySelector('.label');
var labelText = labelEl ? labelEl.textContent.trim() : 'Note';
var md2 = '> **' + labelText + '**\n';
var childNodes = node.childNodes;
for (var i = 0; i < childNodes.length; i++) {
var cn = childNodes[i];
if (cn.nodeType === 3) {
var t = cn.textContent.trim();
if (t) md2 += '> ' + t + '\n';
} else if (cn.nodeType === 1) {
var ccls = cn.className || '';
if (typeof ccls === 'string' && ccls.indexOf('label') !== -1) continue;
if (cn.tagName === 'UL') {
var lis = cn.querySelectorAll(':scope > li');
for (var li = 0; li < lis.length; li++) {
md2 += '> - ' + lis[li].textContent.trim() + '\n';
}
} else if (cn.tagName === 'P') {
md2 += '> ' + cn.textContent.trim() + '\n';
} else {
var t2 = cn.textContent.trim();
if (t2) md2 += '> ' + t2 + '\n';
}
}
}
return md2 + '\n';
}
if (cls.indexOf('diagram') !== -1) {
return '```\n' + node.textContent.trim() + '\n```\n\n';
}
if (cls.indexOf('divider') !== -1) {
return '\n---\n\n';
}
if (cls.indexOf('hero') !== -1) return '';
if (cls.indexOf('hero-') !== -1) return '';
if (cls.indexOf('section-header') !== -1) return '';
if (cls.indexOf('sidebar') !== -1) return '';
if (cls.indexOf('main') !== -1) {
// 递归处理 main 的子节点
var result = '';
for (var m = 0; m < node.children.length; m++) {
result += nodeToMarkdown(node.children[m]);
}
return result;
}
if (cls.indexOf('section') !== -1) {
// 处理 section
var sResult = '';
var headerEl = node.querySelector('.section-header');
if (headerEl) {
var numEl = headerEl.querySelector('.section-number');
var h2El = headerEl.querySelector('h2');
if (numEl && h2El) {
sResult += '## ' + numEl.textContent.trim() + '. ' + h2El.textContent.trim() + '\n\n';
} else if (h2El) {
sResult += '## ' + h2El.textContent.trim() + '\n\n';
}
}
for (var s = 0; s < node.children.length; s++) {
var child = node.children[s];
var childCls = child.className || '';
if (typeof childCls === 'string' && childCls.indexOf('section-anchor') !== -1) continue;
if (typeof childCls === 'string' && childCls.indexOf('section-header') !== -1) continue;
sResult += nodeToMarkdown(child);
}
sResult += '---\n\n';
return sResult;
}
// 其他 div,递归子节点
var dResult = '';
for (var d = 0; d < node.children.length; d++) {
dResult += nodeToMarkdown(node.children[d]);
}
return dResult;
}
// 标题
if (tag === 'H1') return '# ' + node.textContent.trim() + '\n\n';
if (tag === 'H2') return '## ' + node.textContent.trim() + '\n\n';
if (tag === 'H3') return '### ' + node.textContent.trim() + '\n\n';
if (tag === 'H4') return '#### ' + node.textContent.trim() + '\n\n';
// 段落
if (tag === 'P') {
var pResult = '';
for (var p = 0; p < node.childNodes.length; p++) {
pResult += inlineToMarkdown(node.childNodes[p]);
}
return pResult.trim() + '\n\n';
}
// 列表
if (tag === 'UL') {
var ulResult = '';
var ulItems = node.querySelectorAll(':scope > li');
for (var ui = 0; ui < ulItems.length; ui++) {
ulResult += '- ' + ulItems[ui].textContent.trim() + '\n';
}
return ulResult + '\n';
}
if (tag === 'OL') {
var olResult = '';
var olStart = parseInt(node.getAttribute('start') || '1', 10);
var olItems = node.querySelectorAll(':scope > li');
for (var oi = 0; oi < olItems.length; oi++) {
olResult += (olStart + oi) + '. ' + olItems[oi].textContent.trim() + '\n';
}
return olResult + '\n';
}
// SPAN / A / STRONG 等行内元素
var inlineResult = '';
for (var x = 0; x < node.childNodes.length; x++) {
inlineResult += inlineToMarkdown(node.childNodes[x]);
}
return inlineResult;
}
function inlineToMarkdown(node) {
if (!node) return '';
if (node.nodeType === 3) return node.textContent || '';
if (node.nodeType !== 1) return '';
var tag = node.tagName;
var text = node.textContent || '';
if (tag === 'STRONG' || tag === 'B') return '**' + text + '**';
if (tag === 'CODE') return '`' + text + '`';
if (tag === 'EM' || tag === 'I') return '*' + text + '*';
if (tag === 'A') return '[' + text + '](' + (node.getAttribute('href') || '#') + ')';
if (tag === 'BR') return ' \n';
// 递归处理子节点
var result = '';
for (var i = 0; i < node.childNodes.length; i++) {
result += inlineToMarkdown(node.childNodes[i]);
}
return result;
}
function generateMarkdown() {
var md = '# Nginx 完全教程\n\n';
md += '> 从零开始掌握 Nginx 的安装、配置与运维。\n\n';
md += '---\n\n';
// 目录
md += '## 目录\n\n';
var navGroups = document.querySelectorAll('.nav-group');
for (var g = 0; g < navGroups.length; g++) {
var groupTitle = navGroups[g].querySelector('.nav-group-title');
if (groupTitle) md += '**' + groupTitle.textContent.trim() + '**\n\n';
var navItems = navGroups[g].querySelectorAll('.nav-link');
for (var n = 0; n < navItems.length; n++) {
var text = navItems[n].textContent.trim();
var href = navItems[n].getAttribute('href') || '';
md += '- [' + text + '](' + href + ')\n';
}
md += '\n';
}
md += '---\n\n';
// 正文
var mainEl = document.querySelector('.main');
if (mainEl) {
var sections = mainEl.querySelectorAll(':scope > .section');
for (var s = 0; s < sections.length; s++) {
md += nodeToMarkdown(sections[s]);
}
}
return md;
}
function exportMarkdown() {
try {
var md = generateMarkdown();
var blob = new Blob([md], { type: 'text/markdown;charset=utf-8' });
var url = URL.createObjectURL(blob);
var a = document.createElement('a');
a.href = url;
a.download = 'nginx-tutorial.md';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
showToast('已导出 nginx-tutorial.md', 'success');
} catch (e) {
console.error('Export error:', e);
showToast('导出失败: ' + e.message, 'info');
}
}
function copyMarkdown() {
try {
var md = generateMarkdown();
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(md).then(function() {
showToast('已复制到剪贴板', 'info');
}).catch(function() {
fallbackCopy(md);
});
} else {
fallbackCopy(md);
}
} catch (e) {
console.error('Copy error:', e);
showToast('复制失败', 'info');
}
}
function fallbackCopy(text) {
var ta = document.createElement('textarea');
ta.value = text;
ta.style.position = 'fixed';
ta.style.left = '-9999px';
document.body.appendChild(ta);
ta.select();
try {
document.execCommand('copy');
showToast('已复制到剪贴板', 'info');
} catch (e) {
showToast('复制失败', 'info');
}
document.body.removeChild(ta);
}
// 绑定按钮事件
var heroExportBtn = document.getElementById('heroExportBtn');
if (heroExportBtn) heroExportBtn.addEventListener('click', exportMarkdown);
var sidebarExportBtn = document.getElementById('sidebarExportBtn');
if (sidebarExportBtn) sidebarExportBtn.addEventListener('click', exportMarkdown);
var copyBtn = document.getElementById('copyBtn');
if (copyBtn) copyBtn.addEventListener('click', copyMarkdown);
</script>
</body>
</html>