掌握Rewrite重写技术提升项目路由灵活性

___子萱 工具 阅读 2,475
赞 9 收藏
二维码
手机扫码查看
反馈

又出幺蛾子了,API路径重写不生效

今天上线前最后检查功能,发现前端调用的某个接口死活走不到后端。报404,但后端同学说服务根本没收到请求。我第一反应是:是不是拼错了?查了一遍URL,没问题。然后怀疑代理配置——我们用Nginx做了rewrite重写,把所有 /api/v2/ 开头的请求转发到另一个服务。可就是不生效。

掌握Rewrite重写技术提升项目路由灵活性

最离谱的是,本地开发环境好好的,一上预发环境就挂。这就不是代码问题了,肯定是部署或配置差异。

折腾了半天发现,原来是location匹配顺序搞的鬼

先说结论吧,核心问题是Nginx的 location 匹配优先级导致rewrite规则压根没被执行。我一开始写的配置长这样:

location / {
    proxy_pass http://frontend;
}

location /api/v2/ {
    rewrite ^/api/v2/(.*)$ /v2/$1 break;
    proxy_pass http://backend-service;
}

看起来没啥毛病对吧?但实际上,这个配置在某些情况下会被第一个 location / 拦下来,后面的就进不去。因为虽然正则匹配更精确,但Nginx里普通前缀匹配(比如 location /api/v2/)如果没有加修饰符,默认是按声明顺序来的,除非你明确加上 ^~ 或者用正则写法并提高优先级。

这里我踩了个坑:我以为只要路径匹配就会进入对应块,结果不是。Nginx的匹配规则比想象中复杂一点。后来翻文档才发现:带有 = 精确匹配最高,其次是 ^~ 前缀匹配(不继续正则),再然后是正则 ~ 和 ~*,最后才是普通的前缀匹配

而上面那段配置里的两个都是普通前缀匹配,所以会按“最长匹配”原则选一个,但不会阻止后续正则检查——等等,其实还没完,如果后面还有正则表达式location,还会再比一次!这机制真够绕的。

后来试了下发现,加个 ^~ 就好了

改法很简单,在关键 location 前面加个 ^~,表示一旦匹配成功就不考虑正则了:

location / {
    proxy_pass http://frontend;
}

location ^~ /api/v2/ {
    rewrite ^/api/v2/(.*)$ /v2/$1 break;
    proxy_pass http://backend-service;
}

这么一改,果然通了。请求进来,先匹配 /api/v2/user/info,命中第二个location,直接执行rewrite和proxy_pass,不再往下找任何正则location。完美。

不过这里注意我踩过好几次坑:breaklast 的区别也得搞清楚。当时为了调试,我把 break 改成 last,结果rewrite之后又去重新匹配location,反而跳回了第一个 /,导致proxy到了错误的服务。

  • break:停止rewrite处理,使用当前结果直接proxy
  • last:停止当前阶段,但发起新的location查找

所以这里必须用 break,不然 rewrite 后的路径会被再次路由,可能又掉进别的块里。

还有一种方案是用正则 + if,但我没敢用

网上有人建议这么写:

location / {
    if ($request_uri ~* ^/api/v2/(.*)) {
        rewrite ^/api/v2/(.*)$ /v2/$1 break;
        proxy_pass http://backend-service;
    }
    proxy_pass http://frontend;
}

看着简洁,但亲测不可靠。首先 if 在location里本身就容易出问题,尤其是在复杂的header或querystring场景下。其次 proxy_pass 不能放在if里面当条件执行,Nginx会警告你这是不安全的操作,某些版本甚至直接报错。

而且这种嵌套写法维护起来太恶心了,以后别人来看一脸懵。所以我果断放弃这条路。

最终上线配置是这样的

我现在线上跑的完整配置如下,兼顾清晰和稳定性:

server {
    listen 80;
    server_name example.com;

    # 静态资源缓存优化
    location ~* .(js|css|png|jpg|jpeg|gif|ico|svg)$ {
        expires 1y;
        add_header Cache-Control "public, immutable";
        try_files $uri =404;
    }

    # API v2 路径重写并代理
    location ^~ /api/v2/ {
        rewrite ^/api/v2/(.*)$ /v2/$1 break;
        proxy_pass http://backend-service;
        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;
    }

    # 兜底:其他所有请求交给前端服务
    location / {
        proxy_pass http://frontend;
        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;
    }
}

这里面几个关键点我都标出来了:

  • 静态资源单独处理,避免干扰主流程
  • ^~ /api/v2/ 强制优先匹配,防止被 / 拦截
  • rewrite用 break 终止,保证不二次匹配
  • proxy头信息补全,让后端能拿到真实IP等数据

这套现在跑了两天,日志上看没再出现404或者误转发的情况。算是稳住了。

顺手提一嘴:本地开发怎么模拟?

我们在本地用webpack devServer做代理,也得保持一致逻辑。对应的 vue.config.jsvite.config.ts 得配上类似规则:

// vite.config.ts
export default defineConfig({
  server: {
    proxy: {
      '/api/v2': {
        target: 'http://localhost:9002',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^/api/v2/, '/v2')
      }
    }
  }
})

这里rewrite也是去掉前面那层/api/v2,换成/v2开头。跟Nginx那边逻辑对齐,避免本地正常、上线就崩的尴尬。

改完后还有一两个小问题,但无大碍

目前唯一的小瑕疵是:如果有带 query 参数的请求,比如 /api/v2/user?id=123,rewrite后参数自动保留了,没问题。但日志里看到个别请求出现了双斜杠,像 //v2/user。排查发现是因为原始路径结尾多了一个斜杠,rewrite后变成 /v2//user。不影响功能,HTTP服务器一般都能处理,就没动了。

要是强迫症受不了,可以在rewrite里加个替换:

rewrite ^/api/v2/(.*)$ /v2/$1 break;
rewrite ^(/v2/.*)//(.*)$ $1/$2 break;  # 处理双斜杠

但我觉得没必要,反而增加复杂度。现在的方案不是最优的,但最简单,够用就行。

以上是我踩坑后的总结,如果你有更好的方案欢迎评论区交流

这类rewrite问题每次遇到都得重新翻一遍Nginx文档,尤其是location匹配顺序这块,真心希望官方能把这部分说明写得再直白点。实际项目中很多人都是复制粘贴改改就上线,等到出问题才来查,浪费时间。

顺便提醒大家:别迷信“看起来能跑”的配置,一定要理解背后的匹配机制。特别是上线前最好用curl多测几种路径,确认rewrite和proxy行为符合预期。

这个技巧的拓展用法还有很多,比如根据用户agent分流、按域名重定向、灰度发布路径改写等等,后续会继续分享这类实战记录。

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论