Nginx 完全教程

0 阅读26分钟

Nginx 完全教程

从零开始掌握 Nginx 的安装、配置与运维。


01. Nginx 简介

Nginx(发音 "engine-x")是一个高性能的 HTTP 和反向代理服务器,由 Igor Sysoev 于 2004 年首次发布。它以事件驱动、异步非阻塞的架构闻名,能够以极低的内存消耗处理数万并发连接。

Nginx 能做什么?

  • HTTP 服务器 — 高效提供静态文件服务
  • 反向代理 — 将请求转发给后端应用服务器
  • 负载均衡 — 在多台后端之间分发流量
  • SSL/TLS 终端 — 处理 HTTPS 加密解密
  • API 网关 — 路由、限流、鉴权
  • 邮件代理 — IMAP/POP3/SMTP 代理

Nginx vs Apache

特性NginxApache
架构模型事件驱动 / 异步非阻塞进程 / 线程驱动
并发性能极高(C10K+)中等
内存占用相对较高
静态文件极快良好
动态内容需借助外部(PHP-FPM等)内置 mod_php
配置风格集中式、声明式.htaccess 分散式

02. Linux 安装与启动

Ubuntu / Debian

# 更新软件源并安装
sudo apt update
sudo apt install -y nginx
​
# 启动 & 设为开机自启
sudo systemctl start nginx
sudo systemctl enable nginx
​
# 验证安装
nginx -v

CentOS / RHEL / Fedora

# 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)

brew install nginx
brew services start nginx

Docker

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

常用管理命令

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。

方法一:下载官方压缩包

  1. 下载 Nginx访问 nginx.org/download,下载 Stable version 的 Windows zip 包。
  2. 解压到目标目录建议解压到 C:\nginx,路径中不要包含中文或空格。
  3. 启动 Nginx
cd C:\nginx
.\nginx.exe
​
# 验证:浏览器访问 http://localhost
tasklist /fi "imagename eq nginx.exe"

Windows 管理命令

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 或任务管理器终止。

端口冲突排查

netstat -ano | findstr ":80 "
iisreset /stop             # 停止 IIS
net stop W3SVC             # 停止 WWW 服务

方法二:Chocolatey

choco install nginx -y

方法三:Scoop

scoop bucket add main
scoop install nginx

方法四:Docker Desktop

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)

# 下载: 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 配置示例

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/Debianaptapt install nginx生产
CentOS/RHELdnfdnf install nginx生产
macOSHomebrewbrew install nginx开发
Windowszip下载解压开发
WindowsChocolateychoco install nginx开发
WindowsScoopscoop install nginx开发
WindowsDockerdocker run nginx一致性

04. 核心概念

请求处理模型

Nginx 采用一个 Master 进程 + 多个 Worker 进程的模型。

Client Request
   │
   ▼
┌──────────────────────────────┐
│       Master Process         │
│   (读取配置, 管理 Worker)     │
└──────┬───────────┬───────────┘
       │           │
  ┌────▼────┐ ┌────▼────┐
  │Worker 1 │ │Worker 2 │
  │ (epoll) │ │ (epoll) │
  └────┬────┘ └────┬────┘
       │           │
  ┌────▼────┐ ┌────▼────┐
  │Backend A│ │Backend B│
  └─────────┘ └─────────┘

关键目录

路径LinuxWindows
主配置/etc/nginx/nginx.confC:\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.typesC:\nginx\conf\mime.types

05. 配置文件结构

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; }
    }
}

配置层次

main        ← 全局
├── events    ← 事件
└── http      ← HTTP
      ├── server  ← 虚拟主机
      │   └── location ← URL
      └── upstream ← 后端组

注意 修改配置后务必先执行 nginx -t 测试语法。


06. 虚拟主机

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普通前缀

实战示例

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 uriuri uri/ /index.html — 先尝试文件 → 再尝试目录 → 都不存在返回 index.html(适合 SPA)。


08. 反向代理

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. 静态资源服务

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

sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d example.com
echo "0 3 * * * certbot renew --quiet" | sudo crontab -

手动配置

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 Haship_hash同 IP 同后端
最少连接least_conn最少连接优先
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. 缓存配置

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 规则

# 强制 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 代理

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. 安全加固

安全响应头

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;

限流

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. 性能优化

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 日志

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?

proxy_connect_timeout 120s;
proxy_read_timeout    120s;

Q: 413 Request Entity Too Large?

client_max_body_size 100m;

Q: Windows 启动失败?

  • 检查 80 端口占用:netstat -ano | findstr ":80"
  • 运行 nginx -t 检查配置
  • 路径中不要有中文或空格
  • 以管理员权限运行

Q: CORS 跨域配置?

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

<!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"># 启动 &amp; 设为开机自启</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>