location 匹配规则与优先级
location 匹配规则与优先级
写在前面
上一篇讲 nginx.conf 结构的时候,留了一个悬念——location 修饰符的优先级。这篇专门讲清楚:= ^~ ~ ~* 普通前缀 这五种修饰符到底谁优先。
然后讲我之前那篇《create react app 创建的项目改为使用 rsbuild》里 CSS 文件加载失败的真正根因——就是 location 匹配规则没搞透,同事写的配置让所有 /static/*.css 走到了错误的 location。
一、五种修饰符的含义
1.1 修饰符速查表
| 修饰符 | 含义 | 示例 |
|---|---|---|
= |
精确匹配 URI,完全相同才匹配 | location = /health |
^~ |
前缀匹配,匹配后不再检查正则 | location ^~ /static/ |
~ |
大小写敏感正则 | location ~ \.php$ |
~* |
大小写不敏感正则 | location ~* \.(gif|jpg)$ |
| (无) | 普通前缀匹配 | location /api/ |
1.2 完整匹配算法
这是 Nginx 官方伪代码翻译过来的算法:
对每个请求 URI:
1. 先匹配所有 = 修饰符的 location,完全相等才算
└─ 如果找到,使用这个 location,结束
2. 记录所有"普通前缀匹配"中,最长前缀的 location
如果这个 location 有 ^~ 修饰符:
└─ 使用这个 location,结束(不再检查正则)
否则:
└─ 暂存这个 location,继续往下
3. 按配置文件出现顺序,匹配所有正则 location(~ 和 ~*)
└─ 第一个匹配的正则被使用,结束
4. 如果没有任何正则匹配:
└─ 使用第 2 步暂存的普通前缀 location
1.3 一图看懂优先级
优先级从高到低:
┌─────────────────────────────┐
│ 1. = (精确匹配) │ ← 最高
├─────────────────────────────┤
│ 2. ^~ (前缀匹配,锁定) │
├─────────────────────────────┤
│ 3. ~ / ~* (正则,按顺序) │
├─────────────────────────────┤
│ 4. 普通前缀(最长匹配) │ ← 最低
└─────────────────────────────┘
关键点:
- 正则匹配的优先级是按文件出现顺序,不是按"最长匹配"
^~一旦命中,直接锁定,不会再被正则抢走- 同一个请求可能匹配多个前缀 location,但只匹配一个正则 location
二、实战例子:四种修饰符共存
server {
# 1. = 精确匹配 - 优先级最高
location = /health {
return 200 "ok";
# 注意:这种 location 后面不能再有其他指令
}
# 2. ^~ 前缀匹配,锁定
location ^~ /static/ {
root /var/www/assets;
expires 30d;
}
# 3. ~ 大小写敏感正则 - 按出现顺序
location ~ \.(gif|jpg|png)$ {
expires 7d;
}
# 4. ~* 大小写不敏感正则
location ~* \.(txt|md)$ {
default_type text/plain;
}
# 5. 普通前缀匹配 - 优先级最低
location / {
proxy_pass http://backend;
}
}
测试各种 URL 的命中:
| URL | 命中 | 原因 |
|---|---|---|
/health |
location = /health |
精确匹配 |
/static/logo.png |
location ^~ /static/ |
^~ 锁定,不再检查正则 |
/image.jpg |
location ~ \.(gif|jpg|png)$ |
正则命中 |
/README.TXT |
location ~* \.(txt|md)$ |
大小写不敏感正则 |
/api/users |
location / |
普通前缀兜底 |
注意:/static/logo.png 会命中 ^~ /static/,而不是 ~ \.(gif|jpg|png)$,因为 ^~ 一旦命中就锁定了。
三、案例复现:rsbuild CSS 404 的真正根因
3.1 问题回顾
之前的文章里,我们的现象是:rsbuild 构建的项目 CSS 文件返回 404,但是 webpack 构建的没问题。
当时的判断是:rsbuild 把 CSS 文件的 content-type 设成了 text/plain,nginx 根据 mime.types 返回 text/plain,浏览器拒绝解析,样式丢失。
但这只是表象。我后来重新排查,发现根因是同事写的 nginx 配置有问题。
3.2 同事写的 nginx 配置
server {
listen 80;
server_name example.com;
root /usr/share/nginx/html;
index index.html;
# 公司反向代理 - 把所有 API 请求转给后端
location /api {
proxy_pass http://backend;
}
# 静态资源 - 本应走 root,但...
location ~* \.(css|js)$ {
root /usr/share/nginx/html;
}
# 前端路由
location / {
try_files $uri $uri/ /index.html;
}
}
3.3 实际请求的命中过程
请求:GET /static/css/main.css
- 检查
=location —— 没有 - 记录普通前缀最长匹配 ——
location /,长度 1 - 按顺序匹配正则:
location ~* \.(css|js)$—— 命中!
应该匹配到 location ~*,返回 /usr/share/nginx/html/static/css/main.css,对吧?
但是,同事的 dockerfile 里压根没有 /usr/share/nginx/html/static/ 目录——前端 build 产物直接放在 /usr/share/nginx/html/ 下。
3.4 真相
Nginx 命中 location ~* \.(css|js)$,但文件不存在,返回 404。
更糟的是:同事在 location / 里有 try_files,本应回退到 index.html,但 location ~* 不会执行外层 try_files,所以 404 直接返回给浏览器。
3.5 修复方案
方案 A:让正则 location 也走 try_files
location ~* \.(css|js)$ {
root /usr/share/nginx/html;
try_files $uri =404; # 显式 404
}
方案 B(推荐):用 ^~ 锁定,简化匹配
location ^~ /static/ {
root /usr/share/nginx/html;
try_files $uri =404;
}
location / {
try_files $uri $uri/ /index.html;
}
3.6 教训
- 正则 location 不要乱用——按文件顺序匹配的特性很容易出错
- 每个 location 都要明确写
try_files——不要假设外层规则会生效 - 测试时要看 404 而不只是 200——200 OK 不代表配置正确
四、调试 location 匹配
Nginx 提供一个调试技巧:用 error_log 看匹配过程。
4.1 设置 debug 日志
error_log /var/log/nginx/debug.log debug;
但要编译时带 --with-debug:
nginx -V 2>&1 | grep -- '--with-debug'
4.2 抓匹配日志
grep "location" /var/log/nginx/debug.log
会看到类似:
test request: "/api/users"
test location: "= /health"
test location: "^~ /static/"
test location: "~ \.(gif|jpg|png)$"
test location: "/"
using location: "/"
五、最佳实践
5.1 用 = 给特殊 URL 精确匹配
location = /health { return 200 "ok"; }
location = /favicon.ico { log_not_found off; }
性能最优——不需要进一步匹配。
5.2 静态资源用 ^~
location ^~ /static/ {
root /var/www;
expires 30d;
access_log off; # 静态资源不打日志,减少 IO
}
不会被正则抢走,匹配确定性强。
5.3 API 转发用前缀 location
location /api/ {
proxy_pass http://backend/; # 注意末尾 /,去掉 location 前缀
}
5.4 前端路由(SPA)用兜底
location / {
try_files $uri $uri/ /index.html;
}
5.5 不要混用正则和前缀
# NG - 太混乱,排查困难
location /api { ... }
location ~ /v[0-9]+/ { ... }
location /api/v1 { ... }
# OK - 清晰
location /api/v1/ { proxy_pass http://api-v1/; }
location /api/v2/ { proxy_pass http://api-v2/; }
location /api/ { proxy_pass http://api-default/; }
六、try_files 详解
location / {
try_files $uri $uri/ /index.html;
}
参数是候选文件列表,Nginx 依次检查,找到第一个存在的就用。最后一个参数特殊:
- 如果存在文件 → 返回文件
- 如果不存在 + 是
/index.html→ 内部重定向到/index.html(走新 location 匹配) - 如果不存在 + 以
=开头 → 直接返回状态码(如=404)
常见模式:
# SPA 路由
try_files $uri $uri/ /index.html;
# 静态资源 + 404
try_files $uri $uri/ =404;
# 缓存优先
try_files /cache/$uri $uri =404;
# 代理兜底
try_files $uri @backend;
七、rewrite 与 location 的关系
rewrite 指令会让 URI 重新走一遍 location 匹配——除非用 last 之外的 flag。
location /old/ {
rewrite ^/old/(.*)$ /new/$1 last; # last:重新匹配 location
}
location /old/ {
rewrite ^/old/(.*)$ /new/$1 break; # break:不再匹配 location
}
location /old/ {
rewrite ^/old/(.*)$ /new/$1 redirect; # 302 重定向
}
location /old/ {
rewrite ^/old/(.*)$ /new/$1 permanent; # 301 重定向
}
最容易踩坑的:break 和 last 的区别。
last:rewrite 后重新匹配 location,但当前请求的变量会更新break:rewrite 后不再匹配 location,在当前 location 内继续处理
实战中90% 的情况应该用 last。
八、本篇小结
| 修饰符 | 优先级 | 适用 |
|---|---|---|
= |
1(最高) | 健康检查、favicon 等固定 URL |
^~ |
2 | 静态资源目录 |
~ / ~* |
3(按顺序) | 文件后缀匹配 |
| (无) | 4(最低) | 兜底 |
| 关键点 | 说明 |
|---|---|
| 正则按顺序 | 不是按"最长匹配" |
^~ 锁定 |
命中后不再被正则抢走 |
try_files |
每个 location 都应明确写 |
rewrite last |
大多数情况用 last |
下一篇讲 反向代理与负载均衡,会拿公司 API 网关的配置做完整拆解。