年前帮一个项目做站点迁移,旧目录/old-news/要整体搬到/news/,我在 Nginx 里写了一条 rewrite:
rewrite ^/old-news/(.*)$ /news/$1 permanent;重启完一测,旧链接确实 301 跳到了新地址,但点进去之后页面 404,而且只挂了一部分。当时盯着规则看了很久都没发现问题,后来把每个 URL 拆开对比才发现,源路径的父子层级和新目录的命名规则根本不是简单的“换前缀”,固定的正则捕获根本套不住所有情况。
这次折腾让我把rewrite从头到尾认真啃了一遍。如果你也在用 Nginx 改写 URL、做 301 跳转或者伪静态,这篇文章值得花十分钟读完整。我会把 rewrite 的匹配原理、四条 flag 的区别、与 location / proxy_pass / try_files 配合的关键细节,以及几个我踩过的坑一次性讲清楚,刚入门的人能看懂,已经写过若干条 rewrite 规则的人大概率也能补上几个盲区。
1. 从一次“改写后 404”的事故说起:先搞懂 rewrite 到底在改什么
1.1 事故复盘:规则语法没错,错在没弄清改的是 URI 还是 URL
先还原一下当时的配置环境。站点原来有一个伪静态路径,结构是/old-news/2023/08/31/slug.html,运维同事做了内容迁移,新的规范 URL 去掉了中间的日期层级,改成/news/slug.html。我当时想当然地用了“降级替换”的思路,以为把/old-news/前缀换成/news/就行,于是写了上面那条规则。
问题就出在“替换”这两个字上。rewrite 的第二个参数replacement并不是在原始 URL 上做字符串替换,而是把你匹配到的整段内容变成一个新的目标。^/old-news/(.*)$捕获到的是2023/08/31/slug.html这一整段,然后原封不动地拼到/news/后面,结果是/news/2023/08/31/slug.html。但新站根本没有这个层级,自然 404。
这是 rewrite 新手最容易犯的误区:rewrite 的本质是“重写 URI 字符串”,不是“重定向页面”。它给你的是一个内部处理用的 URI,至于浏览器地址栏变不变,取决于你用的是哪个 flag、replacement 里写的是不是完整 URL。
1.2 rewrite 的输入、输出与作用对象
搞清楚 rewrite 的输入输出非常重要。Nginx 在收到请求后,会经过一系列 phase(处理阶段),其中 rewrite 模块主要工作在 server 级别的 rewrite phase 和 location 内部的 rewrite phase 里。输入是规范化后的 URI,也就是不包含?后面参数的路径部分;输出是一个新的 URI,或者一个新的完整 URL。
举几个例子:
- 请求是
https://example.com/post?id=123&page=2,rewrite 正则能匹配到的部分只有/post - 如果 replacement 写成
/article/123,Nginx 只在内部把 URI 改成/article/123,然后继续后续阶段 - 如果 replacement 写成
https://example.com/article/123,Nginx 会直接返回一条 301/302 响应,让浏览器去访问新地址
之所以很多人分不清重写和重定向,是因为 rewrite 的redirect、permanent这两个 flag 让它在某些情况下表现得像重定向。实际上,不带 flag 或带last/break的 rewrite 是内部跳转,外部看起来地址栏根本没变,只是资源从别的地方返回了。
1.3 什么时候该用 rewrite,什么时候别用
我现在的判断标准很简单:
| 需求 | 推荐方案 | 原因 |
|---|---|---|
| http 跳 https、主域名跳 www | return 301或return 302 | 没有正则需求,return 开销更小 |
| 旧路径迁移到新路径,规则明确 | rewrite ... permanent | 需要正则捕获参数 |
| 动态 URL 转伪静态 | try_files/rewrite ... last | 后者更灵活,前者更省心 |
| 反代场景改写上游路径 | proxy_pass自带 URI 改写或rewrite ... break | 配合 break 可避免循环 |
| 复杂业务规则判断 | 交给应用层处理 | Nginx 层做业务逻辑非常痛苦 |
如果你拿不准一条规则该不该用 rewrite,我的建议是先想想:这规则是不是只涉及“把 A 变成 B”这种简单映射?是的话 rewrite 没问题;涉及到多条件、会话状态、权限判断,还是让后端程序去处理更合适。
2. 一条 rewrite 指令的完整解剖:正则、替换串与四个 flag
2.1 匹配部分:PCRE 正则里最容易踩的三处地雷
rewrite 的正则基于 PCRE 库,语法和大部分编程语言的正则类似,但在 Nginx 这种环境里有几个坑很隐蔽。
第一个坑:正则默认匹配的是不含 query string 的 URI。
很多人写rewrite ^/post/?id=123$ /article/123 permanent;然后发现完全不生效,因为?以后的内容根本不会参与匹配。如果你确实要针对参数做跳转,通常要用$args或$arg_xxx来拿到参数值,而不是试图在正则里匹配它:
location ^~ /post/ { if ($args ~ "^id=(\d+)$") { rewrite ^/post/$ /article/$1.html? permanent; } }注意这里的?是个小技巧,后面讲替换串时会细说。
第二个坑:开头的^和结尾的$一定要谨慎写。
我见过不少规则写成:
rewrite ^/old-news/(.*) /news/$1 permanent;少了结尾的$在某些情况下不会出问题,但一旦新路径本身还可能被后续规则匹配,就会产生意料之外的二次改写。反过来,如果你原本想匹配整条路径,却把$写丢了,(.*)会把/news/...之后的内容也吃进去,导致替换结果变形。
第三个坑:贪婪匹配和捕获组的关系。
(.*)默认是贪婪的,会尽可能多地匹配。比如:
rewrite ^/old-news/(.*)/(.*)$ /news/$1/$2 permanent;请求/old-news/2023/08/slug.html时,第一个(.*)会贪婪地匹配到2023/08,第二个捕获组匹配slug.html。如果你想要的是第一层目录作为$1,就得用([^/]+)这种非贪婪写法。
我整理了一张表,记录我常用的一些正则片段:
| 需求 | 推荐正则 | 说明 |
|---|---|---|
| 匹配数字 ID | ^/article/(\d+)$ | 捕获纯数字 |
| 匹配 slug | ^/news/([\w-]+)\.html$ | 允许字母、数字、下划线、中划线 |
| 匹配多级路径 | ^/old-news/(.+)$ | 非贪婪的.不会跨目录?实际.+会,注意用(.*)配合 |
| 非贪婪捕获一小段 | ^/cat/(.*?)/(.*)$ | 先用非贪婪停下 |
| 忽略大小写 | 末尾加i,如rewrite ... last;放在if里时 | 或不加i让 PCRE 区分大小写 |
2.2 替换串的行为:内部 URI、外部 URL 和参数的去留
replacement参数可以分成两类:
- 以
/开头:内部 URI,匹配完成后 Nginx 会在内部继续处理,不会产生新的 HTTP 响应 - 以
http://或https://开头:外部 URL,Nginx 会返回重定向响应,让浏览器去请求新地址
一个经常被忽略的细节是替换串里带不带?对原始参数的影响。默认情况下,如果 replacement 里没有 query string,原请求的参数会被保留并拼到新 URI 后面。如果 replacement 里出现了?,则以?后面的内容为准,原参数会丢掉。
看这个例子:
rewrite ^/old/.*$ /new permanent;请求/old/page?id=123会 301 到/new?id=123,原参数还在。但如果你写:
rewrite ^/old/(\d+)$ /new?id=$1 permanent;请求/old/123?foo=bar跳过去之后是/new?id=123,foo=bar丢失。想保留原参数,有两个办法:
在 replacement 末尾显式加回
$args:rewrite ^/old/(\d+)$ /new?id=$1&$args permanent;在 replacement 末尾用
?表示“保留原 query”:rewrite ^/old/(\d+)$ /new?id=$1? permanent;
不过第二种写法在部分版本的 Nginx 中表现略有差异,我更推荐第一种,语义最直白。
2.3 last、break、redirect、permanent:四个 flag 的真正区别
这是 rewrite 整个知识点里最核心、最容易被绕晕的部分。先给结论,后面再展开:
| flag | 作用 | 是否重新匹配 location | 是否发起新请求 | 典型场景 |
|---|---|---|---|---|
| 无 | 截至当前指令,继续执行剩余 rewrite;完成后继续处理 | 否 | 否 | 较少单独用 |
last | 停止当前 rewrite 模块的处理,用新 URI 重新进入 location 匹配阶段 | 是 | 否 | 伪静态,内部改写路径 |
break | 停止当前 rewrite 模块的处理,不重新匹配 location,继续当前请求处理 | 否 | 否 | 反代改写路径 |
redirect | 返回 302 临时跳转 | - | 是 | 临时活动页 |
permanent | 返回 301 永久跳转 | - | 是 | 永久路径迁移、协议跳转 |
last和break是绝大多数人搞不清的一对。简单说:
last就像你在一张纸上改了地址,然后拿着新地址重新去查一次“该走哪个门”break则是改了地址之后直接继续往前走,不管这个新地址是不是应该匹配另一个 location
举个例子,假设你有一个 location/old/,里面写了:
location /old/ { rewrite ^/old/(.*)$ /new/$1 last; }改写后的/new/xxx会重新走进 location 匹配流程,如果存在location /new/,那请求最终由它处理。如果你改成break,那么请求不会重新匹配/new/这个 location,而是留在location /old/的上下文里继续处理,比如接着执行这个 location 里的 proxy_pass 等指令。这很容易让人懵:既然我改成了/new/xxx,为什么不走/new/的逻辑?
原因就是break明确告诉 Nginx:“别找新 location 了,就用当前这堆配置继续。”这个特性在反向代理场景特别有用,尤其是当你不想让改写后的 URI 再次触发其他 location 的规则时。
redirect和permanent则是真正的客户端跳转。前者返回 302,后者返回 301。一个最常遇到的坑是:301 会被浏览器以及各类缓存服务长期缓存,测试阶段不小心发了 301,后面想改成 302 很难立刻生效,改完规则后浏览器可能还在用旧跳转结果,得强制刷新甚至等缓存过期。
还需要注意一个隐含限制:Nginx 内部对于 rewrite 重定向循环有保护,默认最多 10 次内部重新匹配,超过之后会报rewrite or internal redirection cycle错误,直接返回 500。出现这个错误,本质就是你的 rewrite 规则让同一请求反复改写、反复重新匹配,形成了死循环。
3. 高频场景的配置样本:为什么这么写,坑在哪
3.1 协议跳转:http 到 https,用 return 还是 rewrite
最经典的跳转需求。网上搜出来的配置有一半是这么写的:
server { listen 80; server_name example.com; rewrite ^/(.*)$ https://example.com/$1 permanent; }这个写法能工作,但有两个没必要付出的代价:
- 多执行了一次正则匹配,虽然单次影响很小,但在大流量入口,每毫秒的 CPU 都是成本
- 如果请求里有 query string,
rewrite ^/(.*)$ https://example.com/$1 permanent;这种写法并没有把原参数带过去,很多人会漏掉$args
更好的方案是用 return:
server { listen 80; server_name example.com; return 301 https://$host$request_uri; }$request_uri是完整原始请求行里的 URI(包含参数),用它做跳转天然保留所有参数,也不用写正则。如果只是协议从 http 换到 https,路径完全不变,return 是最优选。
如果遇到要跳转到不同端口的场景,记住$host只表示主机名,不含端口。你可以写https://$host:8443$request_uri,但要注意$host不包含端口,手动拼端口不会重复。
3.2 域名与路径迁移:保留参数的 301
这类需求常见于内容改版。比如旧地址/post?id=123要迁移到新站/article/123.html。
由于 rewrite 不直接匹配 query string,一般用$arg_xxx来取参数:
location ^~ /post { if ($arg_id ~ "^(\d+)$") { set $article_id $1; rewrite ^/post$ /article/$article_id.html permanent; } }这里用set先把捕获的$1存起来,因为如果直接在 rewrite 里用$1,rewrite 的正则捕获组会和 if 条件里的捕获组产生干扰。Nginx 的变量作用域有时候很反直觉,if 块内部的$1在退出 if 作用域之后不一定还可用,所以先set成自定义变量最保险。
如果你要保留原参数,可以在 rewrite 的 replacement 末尾手动追加$args:
rewrite ^/post$ /article/$article_id.html?$args permanent;但我一般不建议保留,因为迁移后的 URL 应当干净、唯一,带着旧参数反而容易造成重复内容。
3.3 伪静态:动态 URL 改写成脚本入口
伪静态是 rewrite 在传统 PHP 站点里用得最多的场景。目标是把/category/tech/page/2这种一看就是语义化路径的 URL,在内部改成/index.php?cat=tech&page=2,再交给 PHP-FPM 处理。
常见写法:
location / { rewrite ^/category/([\w-]+)/page/(\d+)$ /index.php?cat=$1&page=$2 last; rewrite ^/category/([\w-]+)$ /index.php?cat=$1 last; }为什么这里一定用last?因为改写后的/index.php?...需要重新匹配到 PHP 的 location(通常是一个由location ~ \.php$定义的正则 location),只有last才允许 Nginx 重新进入 location 匹配阶段。如果用了break,请求会留在这个location /的上下文里,根本不会进 PHP 处理,结果就是下载一个 404 页面。
新一代 Nginx 更推荐用try_files处理纯伪静态:
location / { try_files $uri $uri/ /index.php?$query_string; }try_files的逻辑是:先看/category/tech/page/2这个文件是否存在,再看目录是否存在,都不存在就把请求内部重定向到/index.php。它不需要写正则,也不用担心循环,误伤面小得多。所以我的建议是:新项目优先try_files,老项目在不改动大量 rewrite 的前提下可以继续沿用 rewrite。rewrite 更适合那种需要精确捕获参数、并且要把参数语义传给后端的场景。
3.4 移动端适配与地域跳转
有些站点会按 User-Agent 把手机用户导到 m 站。一条比较经典的写法是:
location / { if ($http_user_agent ~* "(Mobile|Android|iPhone)") { rewrite ^/(.*)$ /mobile/$1 last; } }这里有三个雷区:
if是 Nginx 里出了名的“邪恶指令”,在 location 里使用 if 要格外克制,它可能引发奇怪行为。如果只是做移动端跳转,建议把 if 和 rewrite 尽量放在 server 级或独立的 location 里,控制影响面。- rewrite 之后的
/mobile/$1最好落在一个不含 if 的具体 location,避免二次匹配又触发 UA 判断,形成循环。 - 移动端跳转还要考虑用户手动点击“查看完整版”的诉求,最好给这种 URL 加个
?pc=1之类的参数,在后端判断并跳过跳转。
如果只是纯前端展示,我更推荐响应式布局而不是服务端跳转,服务端跳转牵涉缓存、SEO、用户切换等更复杂的问题,不是非必要不要上。
3.5 数据校验、规范 URL 之类:rewrite 不是万能胶水
rewrite 最容易让人上头,因为感觉什么 URL 问题都能用它“绕”过去。实际上我建议在写一条 rewrite 之前先问自己三个问题:
- 这个改写是 Nginx 层面能做且应该做的吗?
- 用
try_files或return能不能达到同样目的? - 业务规则未来会不会变?会不会需要后端参与计算?
比如“URL 中参数值不合法,直接 404”这类需求,rewrite 虽然也能写,但可维护性很差。遇到这种需求,直接在应用层判断更清晰。rewrite 是 Nginx 的“手术刀”,适合精准切割,不适合大面积缝补。
4. rewrite 如何在 location、proxy_pass、try_files 之间“接力”
4.1 location 优先级与 rewrite 落点的“追击游戏”
要让 rewrite 不翻车,必须理解它和 location 之间的先后关系与循环风险。Nginx 内部处理流程大致是:
- 解析请求,得到规范化后的 URI
- 执行 server 级别的 rewrite 规则(先用正则改写 URI)
- 根据当前 URI 进行 location 匹配
- 进入选中的 location,执行 location 内的 rewrite 规则
- 如果 rewrite 使用了
last,则新 URI 会重新触发 location 匹配,重复上述第 3 步 - 重复次数达到上限就报循环错误
所以从外部看,rewrite 就像一个“内部处理器”,它可能让同一个请求在多个 location 之间来回跳动。location 匹配有自己的优先级:精确匹配=最高,然后是^~前缀匹配,再是正则匹配~/~*,最后是普通前缀匹配。如果你 rewrite 出来的新 URI 恰好匹配到一个更优先的 location,请求的执行路径和你原本预想的可能完全不一样。
我遇到过一个印象很深的例子。某项目伪静态规则写成:
location / { rewrite ^/news/(.*)$ /index.php?id=$1 last; }但项目里同时存在一个更精确的前缀:
location ^~ /news { # 这里做静态资源缓存 }结果 rewrite 后的/index.php?id=xxx没有去匹配 PHP 的 location,因为/index.php走了普通前缀匹配,落到了一个不太对的 location 里,导致请求处理结果诡异。这种问题很难一眼看出,只能靠日志定位。
4.2 last 和 break 在 location 内外的正确摆放位置
很多教程会把 rewrite 写在 server 级或 location 级,但没说清楚两种写法在语义上的差异。我总结一套自己的摆放原则:
- server 级 rewrite:通常用于域名跳转、协议跳转、路径整体迁移,这类规则一般和具体 location 无关,并且以
permanent/redirect为主 - location 内 rewrite:用于针对特定目录或模式的内部改写,最后用
last让新 URI 进入正确 location,或用break在当前 location 内继续处理 - 代理场景 rewrite:优先
break,因为代理后端通常只需要一个明确的最终 URI,不希望它再触发其他 location 规则
一个反代场景的标准写法:
location ~ ^/api/v1/(.*)$ { rewrite ^/api/v1/(.*)$ /internal/svc/$1 break; proxy_pass http://backend; }这里如果用last,rewrite 后的/internal/svc/xxx会去重新匹配 location。假设你还有一个location /internal/,请求就会进入那个 location 并执行它自己的业务逻辑,原来的proxy_pass http://backend可能根本不会执行,也可能执行两遍。用break能确保改写后的 URI 只被当前 location 的后续指令使用,不重新匹配。
4.3 proxy_pass 自带 URI 改写能力:很多时候根本不需要 rewrite
rewrite 和 proxy_pass 的关系很容易被过度使用。实际上 proxy_pass 本身就具备“把请求路径的一部分替换掉”的能力,比如:
location /api/ { proxy_pass http://backend; }请求/api/user转发到后端时,URI 仍然是/api/user。如果改成:
location /api/ { proxy_pass http://backend/; }请求/api/user转发到后端时,匹配location /api/的那一段前缀/api/会被替换成/,后端收到的 URI 是/user。
这种内置改写足以应对大量“去掉前缀或换前缀”的场景,完全不需要 rewrite。只有当你要做的不是简单前缀替换,而是“捕获中间某一段再做复杂拼接”时,rewrite + break 才更有优势。
另外提醒一点:proxy_pass 如果使用了变量(比如proxy_pass http://$upstream$request_uri;),它后面必须带 URI(即使只是一个/),否则 Nginx 会拒绝启动。
4.4 try_files 与 rewrite 的分工:伪静态的最优解
try_files和 rewrite 在伪静态场景看起来很接近,但定位不同。try_files更像“兜底路由”,它按顺序检查文件是否存在,最后给一个 fallback URI;rewrite 更像“明确改写”,不管目标存不存在,先把 URL 变了再说。
两者配合的例子:
location / { try_files $uri $uri/ @router; } location @router { rewrite ^/(.*)$ /index.php?$1 last; }先用try_files拦截所有真实存在的文件和目录,剩下的动态路由统一走 rewrite 进 PHP。这样既保留了 rewrite 的灵活性,又避免了 rewrite 把所有请求都拦截一遍的性能损耗。
如果你发现一条 rewrite 规则同时需要匹配很多不同形态的 URL,与其写一堆正则分支,不如考虑用map在进入 location 之前做一次映射:
map $uri $real_uri { ~^/old/(.*)$ /new/$1; ~^/legacy/(.*)$ /archived/$1; default $uri; }map是纯变量映射,比 rewrite 阶段的逐条正则匹配更直观,也更适合规则较多的情况。这个技巧我知道的人不多,但对维护大量跳转规则的人来说非常好用。
5. 翻车现场与排查手段:rewrite 调试、性能损耗与大坑名单
5.1 打开 rewrite_log,看内部跳转每一步
排查 rewrite 问题最有效的工具就是 Nginx 自带的 rewrite 日志。配置方法很简单:
error_log /var/log/nginx/error.log notice; rewrite_log on;打开之后,每次请求经过 rewrite 模块,error.log 里会输出类似这样的信息:
[notice] 1234#0: *7 rewrite "^/old-news/(.*)$" matches "/old-news/2023/08/slug.html", client: 192.168.1.1, server: example.com [notice] 1234#0: *7 rewritten uri: "/news/2023/08/slug.html" [notice] 1234#0: *7 rewritten redirect: "/news/2023/08/slug.html"看到rewritten uri说明是内部改写;看到rewritten redirect说明客户端会收到跳转响应。这个日志能直接告诉你规则有没有匹配、改出来的 URI 长什么样,比凭肉眼猜正则高效太多。
线上排查时,边看日志边用 curl 复现是最推荐的方式:
curl -I "https://example.com/old-news/123"看返回的Location头和状态码,就能判断是 301、302 还是内部改写。我习惯在测试环境把permanent临时改成redirect或者直接加一个时间戳参数来避免浏览器缓存:
curl -I "https://example.com/old-news/123?ts=20250101"顺便说一句,rewrite_log 千万不要在生产常开,开一天能写好几个 GB,只在排查时短暂开启,查完立刻关。
5.2 性能影响:正则不是免费的
rewrite 每多一条,每个请求就可能多一次正则匹配。正则匹配是 CPU 密集型操作,尤其规则集中在 server 级时,意味着所有请求都会经历一遍正则,不只是命中伪静态的请求。
优化点有几个:
打开 PCRE JIT。Nginx 编译时带
--with-pcre-jit,然后在配置里加一行:pcre_jit on;这会显著降低正则匹配的耗时。
能不用正则就不用。
return 301 https://...这种纯协议跳转,绝对不要写成正则 rewrite。减少规则数量。可以通过
^~精确前缀把完全不相关的 location 提前拦截掉,让 rewrite 规则只作用于需要改写的路径。复杂规则拆成
map。多条长正则挤在 rewrite 阶段很丑也难维护,map更适合做一对一的固定映射。
我实测过一个很典型的场景:某个入口有 20 条 rewrite 规则全写在 server 级,整个站每个请求都要跑 20 次正则,高峰期 Nginx CPU 直接吃紧。改成map+try_files组合之后,CPU 占用下降了将近四分之一。当然具体数值和机器配置、规则复杂度都有关,但方向是明确的。
5.3 高频大坑名单:我记下来的五条教训
大坑一:参数没了。rewrite 默认不匹配 query string,replacement 里带?又会把原参数冲掉。几乎每个刚开始写 rewrite 的人都会踩。处理时要明确自己想保留哪些参数,需要就显式写$args或$arg_xxx。
大坑二:死循环。比如写了:
rewrite ^/(.*)$ /index.php last;如果/index.php又被同一个 location 里的 rewrite 匹配到,相当于每次请求都改写成同一个 URI,再次进入 location,再次匹配,rules 循环,Nginx 到第 10 次直接返回 500。凡是遇到 500 且 error.log 里有rewrite or internal redirection cycle,优先排查是不是这个原因。
大坑三:break 放错位置。在 server 级使用rewrite ... break和rewrite ... last的差异不容易直观感知,但放在 location 里区别就会被放大。反代场景用break,伪静态场景用last,这是我目前最稳妥的实践。
大坑四:301 被浏览器和 CDN 缓存。测试阶段尽量用 302 或 307,确认没问题再切 301。如果已经发了 301,给你的规则 URI 加个查询参数(哪怕是无意义的?v=2)也能绕开部分 URL 级缓存。
大坑五:if 环境下的变量捕获。在if里写的正则捕获组,出了 if 块大概率不可用。我会先把捕获值set到自定义变量:
if ($request_uri ~ "^/old/(\d+)") { set $page_id $1; } rewrite ^/old/.*$ /new/$page_id permanent;用自定义变量把值提前存下来,就不会出现“日志里能看到匹配,跳转却拿不到值”的诡异问题。
5.4 我自己的几条防坑习惯
每个 rewrite 规则都加注释。这听起来很简单,但很多人不写。我现在会在规则上方写清楚“原 URL 长什么样、新 URL 长什么样、为什么是用 last 而不是 break”,方便几个月后的自己和接手的人。
任何修改都先用nginx -t做语法检查,再在测试环境用 curl 验证状态码和 Location 头。上线前我还会准备一组“新旧 URL 对照表”,写个脚本循环 curl,确保每一条规则都符合预期。
如果遇到特别复杂的路径转换需求,我会优先考虑用map做映射,而不是堆 rewrite 规则。这是我在维护一个几百条跳转规则的老项目时总结出的最舒服的解法:规则集中、可控、像查表一样清晰,出问题也好定位。
rewrite 这个东西,说难不难,说简单也不简单。它的核心就三件事:正则匹配、替换串语义、flag 的选取。你把这三点吃透了,再加上日志这个工具兜底,绝大多数 URL 改写需求都能稳稳拿捏。