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. 普通前缀(最长匹配)       │  ← 最低
└─────────────────────────────┘

关键点:

二、实战例子:四种修饰符共存

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

  1. 检查 = location —— 没有
  2. 记录普通前缀最长匹配 —— location /,长度 1
  3. 按顺序匹配正则:
    • 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 教训

  1. 正则 location 不要乱用——按文件顺序匹配的特性很容易出错
  2. 每个 location 都要明确写 try_files——不要假设外层规则会生效
  3. 测试时要看 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 依次检查,找到第一个存在的就用。最后一个参数特殊:

常见模式:

# 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 的区别。

实战中90% 的情况应该用 last。

八、本篇小结

修饰符 优先级 适用
= 1(最高) 健康检查、favicon 等固定 URL
^~ 2 静态资源目录
~ / ~* 3(按顺序) 文件后缀匹配
(无) 4(最低) 兜底
关键点 说明
正则按顺序 不是按"最长匹配"
^~ 锁定 命中后不再被正则抢走
try_files 每个 location 都应明确写
rewrite last 大多数情况用 last

下一篇讲 反向代理与负载均衡,会拿公司 API 网关的配置做完整拆解。

参考资料