静态站点部署:root、alias 与 SPA 回退
前端项目上线在 Nginx 侧只做两件事:把构建产物放到一个目录,并告诉 Nginx “请求这个路径时去读那个文件”。难点全在细节上:root 与 alias 拼接路径的规则不同,try_files 少写一个 /index.html 就会让单页应用刷新后 404,权限少一个 x 位就是 403。本章把这三处讲透。
部署全流程
# 1) 本地构建产物(产物目录通常是 dist/ 或 build/)
pnpm install --frozen-lockfile && pnpm build
# 2) 先传到临时目录,再原子替换,避免上传中途用户看到半成品
rsync -avz --delete dist/ prod:/srv/www/site-next/
ssh prod 'sudo rm -rf /srv/www/site-prev && sudo mv /srv/www/site /srv/www/site-prev \
&& sudo mv /srv/www/site-next /srv/www/site'
# 3) 权限:目录 755、文件 644
sudo find /srv/www/site -type d -exec chmod 755 {} \; -o -type f -exec chmod 644 {} \;
# 4) 出 403 时逐级查看权限,一眼看出哪一层少了 x 位
namei -l /srv/www/site/index.html
最小静态站点配置
# /etc/nginx/conf.d/static.example.com.conf
server {
listen 443 ssl;
server_name static.example.com;
ssl_certificate /etc/nginx/certs/example.com.pem;
ssl_certificate_key /etc/nginx/certs/example.com.key;
root /srv/www/site; # 站点根目录,只写目录,不带末尾斜杠
index index.html;
location / {
try_files $uri $uri/ =404;
}
}
try_files 三种典型用法
# 1) 纯静态站:按 文件 → 目录 → 404 依次尝试
location / { try_files $uri $uri/ =404; }
# 2) SPA 单页应用:找不到的路径统一交给 index.html,由前端路由接管
location / { try_files $uri $uri/ /index.html; }
# 3) 自定义 404 页面:先声明 error_page,再用 internal 阻止直接访问
error_page 404 /404.html;
location = /404.html { internal; root /srv/www/site; }
SPA 必须用第 2 种写法:/user/123 这类路由在服务器上没有对应文件,若不回退到 index.html,用户刷新页面就会看到 404。回退只应针对 HTML 路由,/api/ 要用单独的 location /api/ { proxy_pass ...; },别被 location / 吃掉。
root 与 alias 的区别
| 维度 | root | alias |
|---|---|---|
| 路径拼接 | root 值 + 完整 URI | alias 值替换掉 location 前缀 |
| 结尾斜杠 | 无影响(但别写尾斜杠) | 规则不同,极易出错 |
| 常见用途 | 整站根目录、SPA | 把某个 URL 前缀映射到另一个目录 |
与 try_files 配合 | 可靠 | 历史上有已知怪异行为,尽量避开 |
# root:实际路径 = /srv/www + 完整 URI
location /static/ { root /srv/www; } # /static/a.png → /srv/www/static/a.png
# alias:location 前缀被替换掉,两种写法结果完全不同
location /static/ { alias /srv/www/assets/; } # 正确 → /srv/www/assets/a.png
location /static/ { alias /srv/www/assets; } # 错误 → /srv/www/assetsa.png
结尾斜杠的两条经验:alias 的值与 location 前缀要么都带 /,要么都不带;两者不一致时拼接结果就会多一层或少一层目录。root 则不要带尾斜杠,写成 /srv/www/site/ 会拼出双斜杠,和 try_files 组合时容易出现难查的路径问题。
location 匹配优先级
| 形式 | 示例 | 优先级 | 说明 |
|---|---|---|---|
| 精确匹配 | location = /healthz | 最高 | 命中即停,不再看其他规则 |
前缀 + ^~ | location ^~ /static/ | 次高 | 匹配后不再尝试正则 |
正则 ~ / ~* | location ~* \.(js|css)$ | 中 | 先于普通前缀,按配置顺序取第一个 |
| 默认 | location / | 最低 | 兜底 |
第 17 章会展开完整优先级与嵌套规则,这里记住一条就够:精确匹配最优先,^~ 优先于正则,正则优先于普通前缀。
缓存头与 gzip
# 带哈希指纹的资源可以长缓存;HTML 必须不缓存,否则发布不生效
location ~* \.(js|css|png|jpg|jpeg|gif|svg|woff2?|ico)$ {
expires 30d;
add_header Cache-Control "public, immutable";
}
location = /index.html { add_header Cache-Control "no-cache, must-revalidate"; }
gzip 在主配置 http 块里开启(见第 14 章),text/css、application/javascript、image/svg+xml 都值得压;图片、字体、woff2 本身已压缩,不要再压,省不下多少带宽还多耗 CPU。
验证方法
curl -I https://static.example.com/ # 首页状态码与 Content-Type
curl -I https://static.example.com/assets/app.abc123.js # 静态资源缓存头
curl -s https://static.example.com/user/123 | head -5 # SPA 路由应返回 index.html
curl -I -H 'Accept-Encoding: gzip' https://static.example.com/assets/app.abc123.js | grep -i content-encoding
curl -I --resolve static.example.com:443:47.98.x.x https://static.example.com/ # 绕过 DNS/CDN 直测
看响应头要抓三样:HTTP/1.1 200、Content-Type 正确(CSS 被当成 text/html 说明 mime.types 没加载)、静态资源带 Cache-Control。
403 与 404 的排查顺序
| 现象 | 优先检查 | 常见原因 |
|---|---|---|
| 403 Forbidden | namei -l /path/to/file | 目录缺 x 位、index 文件不存在、user 与属主不匹配 |
| 403 且日志 Permission denied | getenforce、ls -Z | SELinux 上下文不对(CentOS 系常见) |
| 404 Not Found | nginx -T 找生效的 root | root 路径拼错、产物未上传、alias 斜杠拼错 |
| 200 但页面空白 | 看静态资源请求状态码 | 资源 404 或构建的 base/publicPath 配错 |
排查动作固定为五步:
curl -I https://static.example.com/ # 1. 先看状态码与 Server
sudo tail -100 /var/log/nginx/error.log # 2. 看具体原因(Permission denied / No such file)
namei -l /srv/www/site/index.html # 3. 逐级权限
nginx -T | grep -A8 'server_name static.example.com' # 4. 确认生效的 server 与 root
sudo getenforce && sudo ls -Z /srv/www/site | head # 5. SELinux 检查与修复
常见坑
| 坑 | 现象 | 做法 |
|---|---|---|
alias 与 location 尾斜杠不一致 | 404 或路径多一层目录 | 两边都带 / 或都不带 |
| SPA 没配回退 | 刷新子路由变 404 | try_files $uri $uri/ /index.html |
目录权限少 x 位 | 403,且只有该目录下的文件失败 | 目录 755、文件 644 |
| HTML 也被长缓存 | 发布后用户看到的还是旧页面 | HTML 用 no-cache,带指纹资源才长缓存 |
| 上传途中直接覆盖线上目录 | 用户访问到半成品 | 传临时目录再 mv 原子替换 |
| 资源路径前缀与 Nginx 不匹配 | 页面 200 但 JS/CSS 全 404 | 构建的 base 与部署路径保持一致 |
小结:静态站点的三件事是把产物按“临时目录 + 原子替换”部署、把权限设成目录 755 文件 644、把 try_files 写对——纯静态用 =404、SPA 用 /index.html 回退;root 是拼接、alias 是替换,尾斜杠在 alias 上必须与 location 前缀一致;发布后在服务器上用 curl -I 核对状态码与 Content-Type,403 先查权限与 SELinux,404 先查 root 拼接与回退规则。