先说明一下,这个标题里的“代理”,说的不是大家平时折腾的那种东西,而是开发流程里天天见的三类:环境变量里的 HTTP_PROXY、调试时用来转发请求的本地代理、还有 Nginx 这类反向代理入口。它们的共同点是——数量一多,如果还靠手敲 export、靠人脑记端口、靠翻聊天记录找转发规则,一定会乱。上个月我就在同一天被这三件事轮番坑过,最后下定决心把代理这件事当成工程问题来管,而不是当成“记忆题”来背。
这篇文章就把我整理的三个工具分享出来:direnv 管代理环境变量,whistle 管调试转发规则,Nginx 管多服务统一入口。三件事各管一摊,不越权、不重复,搭起来之后,新增一个项目或者切换一个环境,基本不会再把代理搞乱。适合每天在多个项目、多套后端环境之间来回切换的前后端开发、测试和运维同学参考,也能给团队定一套可执行的代理管理约定。
1. 先搞清楚代理乱在哪里,才能选对工具
1.1 一个下午的真实现场
上个月有个下午,我同时在改三个项目。项目 A 要从公司内网依赖仓库拉包,必须走内网代理;项目 B 的前端要把所有 /api 请求转发到测试环境的网关;项目 C 的本地服务要被隔壁小组的同学访问,需要暴露一个统一入口。
三个项目对代理的诉求完全不一样,而我只有一个终端、一套环境变量。切到项目 A 的时候顺手 export 了 HTTP_PROXY,等切到项目 B 忘了取消,结果所有请求都走了代理,局域网里的联调机访问不到,接口报错全是超时。更气的是,这个状态还污染了旁边正在跑的 CI 脚本,整个下午都在排查“代码没问题为什么环境不对”。
后来我把这个下午的乱象复盘了一下,发现不是记性差,而是代理配置天然就是“全局副作用”。你只要在一个窗口里 export 一次,它就赖在那个进程里不走了;你只要测试环境地址在某个文件里写死一次,下次换预发环境就要全网搜索替换。这根本不是在考验技术,是在考验人的纪律性,而人又恰恰是最不可靠的一环。
1.2 把“乱”拆成三类:环境变量、转发规则、路由入口
代理一多会乱,但乱的层面不一样。我把它拆成三层,下面这个表格基本概括了日常能遇到的情况:
| 乱的层面 | 典型表现 | 根本原因 |
|---|---|---|
| 环境变量层 | HTTP_PROXY、HTTPS_PROXY、NO_PROXY 在多个项目之间互相污染,切项目后忘记取消 | 全局 export 无法随项目边界自动复位 |
| 转发规则层 | 同一个 /api 路径要在本地、测试、预发之间反复切换,靠注释代码来切换 | 规则散落在代码和工具里,没有独立管理 |
| 路由入口层 | 多个后端服务端口难记,环境一多更混乱,前端要人肉改地址 | 缺少统一入口,每个服务直接暴露端口 |
大多数人的做法是出了问题再排查,但更理性的做法是在一开始就意识到:这三层相互独立,所以也应该用三个独立的工具来管。环境变量交给 direnv,转发规则交给 whistle,路由入口交给 Nginx。
1.3 为什么靠记性根本管不住
如果你也是“记得关代理、记得切环境”的那类人,我劝你早点放弃这个念头。代理配置的效果是全局的、累积的、难以察觉的。它的特点是:错误不会立刻报错,而是让你在几分钟后才隐约觉得“好像哪里不对”。
我见过不少同学在团队群里发“我这个环境怎么这么奇怪,你们谁能帮我看看”,最后定位到是某个终端窗口里残留了一个 export。这不是技术能力问题,是人类注意力带宽问题。所以解决方案不是“记得更牢”,而是把代理配置交给能感知项目边界的工具,让它在进目录的时候自动加载、离开目录的时候自动清理。这种“自动化代替记忆”的思路,才是管住代理的第一步。
2. 三个工具的分工:管配置、管转发、管路由
2.1 按代理的生命周期做选型
代理从产生到生效,大体上走这样一条链路:先有配置(我要不要走代理、NO_PROXY 排除哪些域名),再有转发(请求到了某个入口之后往哪里发),最后是路由(多个上游服务如何聚合到同一个入口)。
顺着这条链路去选工具,逻辑就顺了。direnv 管第一段,它只负责把环境变量加载到当前 Shell;whistle 管第二段,它接收代理流量,再按规则转发到本地、内网或者远端;Nginx 管第三段,它作为反向代理,把多个后端服务统一到一个地址下。
这里我并不推荐一个工具试图包办三层。以前我用过“一个脚本同时设置环境变量、启动本地代理、改 Nginx 配置”的思路,结果脚本本身变成了新的维护负担,报错都不知道从哪一层开始查。分层之后,哪一层出问题,就只盯哪个工具,排查路径清晰得多。
2.2 三者对比与边界
下面这张表是我实际使用中总结出的分工边界,方便你判断什么场景该用哪个:
| 工具 | 管什么 | 典型场景 | 上手成本 |
|---|---|---|---|
| direnv | 环境变量加载与卸载 | 进入项目目录自动设置 HTTP_PROXY、NO_PROXY,离开自动清理 | 半小时内能跑通 |
| whistle | 请求转发、mock、域名重定向 | 前后端联调时把 /api 转到不同后端、模拟接口返回、远程调试 | 半小时内能跑通 |
| Nginx | 反向代理、多服务聚合、路由分发 | 把用户服务、订单服务、支付服务统一到一个 dev.local:8080 入口 | 需要理解 location 配置,入门约半天 |
这三个工具之间不冲突,可以独立使用,也可以串起来用。串起来的时候,direnv 把流量指向 whistle,whistle 再把不同域名和路径转发到 Nginx 统一入口或某个后端服务。这种串联在下面的章节里会具体演示。
2.3 为什么不上更重的网关方案
问得最多的一个问题是:现在不都讲 API 网关吗,用 Kong、Traefik 不是更完善?我的观点是,工具选择要看维护成本。
对于三五个人、一两台开发机的团队,上一套完整的 API 网关,需要额外维护数据库、控制台、路由配置同步,学习成本和运维成本远远超过收益。而 Nginx 是一个真正的“轻量网关”,单文件配置、零依赖、默认系统自带,出了问题网上随便一搜就有答案。选型的第一原则应该是:工具要在 30 分钟内跑通,并且不改变现有代码结构。Kong 和 Traefik 都做不到这一点,Nginx 可以。
当然,如果你们的服务已经容器化,并且本身就跑在 Kubernetes 里,那用 Ingress 暴露服务也完全合理。这篇文章讲的是不带容器、一切以开发机为单位的场景,这个前提先说明白。
3. direnv:把代理环境变量锁进项目目录
3.1 环境变量污染的常见现场
先看你有没有遇到过这些情况:全局 export 了 HTTP_PROXY 之后,拉内网 npm 包确实快了,但访问某个公网 API 却时好时坏;或者在项目 A 里设置的 NO_PROXY 漏了内网域名,导致所有内网请求都绕了一大圈,延迟高到想砸电脑。
环境变量污染的本质是:你在全局设置了本应属于某个项目的配置。HTTP_PROXY、HTTPS_PROXY、NO_PROXY 这些变量是进程级别的全局状态,任何终端的 export 都会影响后续所有命令。更麻烦的是,它们不区分大小写的问题也存在——有的软件认小写 http_proxy,有的软件只认大写 HTTP_PROXY,你一次要 export 两遍才不会踩坑。
我见过最混乱的一个开发机,用户 .bashrc 里塞了十几次 export,后面的人根本不敢动,怕一改某个项目就挂。这种“公共空间私搭乱建”的配置方式,就是混乱的源头。
3.2 direnv 的工作机制和安装
direnv 做的事情非常简单:进入目录时自动加载该目录下的 .envrc 文件,离开目录时自动卸载其中设置的环境变量。听起来很朴素,但解决的是“全局副作用”这个核心问题——环境变量现在只在你需要它的目录里生效。
安装方式很简单:
# macOS brew install direnv # Debian/Ubuntu sudo apt install direnv装完之后,还要在 Shell 配置里加一行 hook,让 direnv 能感知目录切换:
# 加到 ~/.bashrc 或 ~/.zshrc eval "$(direnv hook bash)" # 如果用的是 zsh,则改为 eval "$(direnv hook zsh)"加完后重新打开终端,进入一个有 .envrc 的目录时,会看到类似direnv: loading ~/project/.envrc的提示。首次加载会安全拦截,需要手动执行direnv allow放行。
3.3 一份可以直接抄的 .envrc 配置
假设项目 A 需要把所有出网流量指向本机的 whistle 调试代理,并且内网域名直连,配置长这样:
# .envrc export HTTP_PROXY="http://127.0.0.1:8899" export HTTPS_PROXY="http://127.0.0.1:8899" export NO_PROXY="localhost,127.0.0.1,.local,.corp.example.com,10.0.0.0/8"保存后执行direnv allow,环境变量就生效了。离开这个目录后,direnv 会自动把这些变量清掉,回到干净的全局状态。
如果项目 B 压根不需要代理,甚至要确保代理被显式关闭,可以这样写:
# .envrc unset HTTP_PROXY unset HTTPS_PROXY unset NO_PROXY这样即使全局环境被污染过,只要进了项目 B 的目录,也会强制清空。比在每个终端手工unset可靠得多。
3.4 使用中容易踩的坑
direnv 有几个坑值得专门提醒。
第一个坑是direnv allow被忽略。如果你改了 .envrc 之后发现变量没生效,大概率是改完文件后 direnv 提示需要重新 allow,而你直接忽略了。正确做法是:每次修改 .envrc,都重新执行一次direnv allow。
第二个坑是子目录覆盖父目录。direnv 默认只加载离你最近的 .envrc,不会递归叠加父级配置。如果项目根目录有一个 .envrc,子目录里又有自己的 .envrc,那么根目录的代理配置在子目录里是不生效的,除非子目录里显式调用source_up来加载父级配置。这个行为初期很容易让人疑惑。
第三个坑是 IDE 内置终端可能不加载 direnv。VSCode 如果用的是内置终端,通常能读到 Shell 配置,但有些 IDE 或脚本环境不会。遇到这种情况,要么在 IDE 设置里同步 Shell 配置,要么干脆在 IDE 里用 direnv 的direnv export手动验证。
第四,团队协作时需要注意提交策略:.envrc 里通常是本机路径和本机代理地址,不应该直接入库,建议提交一个.envrc.example作为模板,新同学克隆项目后复制一份再改成本地值。
4. whistle:调试代理规则再多,也能分组管住
4.1 为什么用 whistle 而不是直接改代码
以前做前后端联调,最蠢的办法是在代码里写死后端地址:测试环境要在 config 文件里改,本机跑又要改回来,经常出现“代码没问题,就是忘记切环境”的尴尬。用 whistle 之后,代码里只保留相对路径,所有转发规则全部挪到 whistle 里,改规则不用动代码、不用重启服务,保存即生效。
相比之下,Charles 和 Fiddler 也能做类似的事,但 whistle 对“多项目多规则管理”这件事实在太顺手了——它用文本文件管理规则,天然适合入库、diff、review。这个特性在团队协作里特别吃香。
4.2 安装启动和基础代理
whistle 是 Node.js 写的,安装和启动都很简单:
npm install -g whistle w2 start启动后,默认监听 8899 端口,直接访问http://127.0.0.1:8899就能打开配置页面。要让浏览器或系统流量走 whistle,需要把系统代理或浏览器代理指向127.0.0.1:8899。
如果只是调试 HTTP,到这里就够了。但要抓 HTTPS 请求,还需要下载并信任 whistle 的根证书。启动后按页面提示下载根证书,安装到系统信任链;移动端调试时,还要在手机上安装证书并信任描述文件。证书没装好,最常见的现象是 HTTPS 站点全报错,排查半天才发现是代理证书不受信任。
4.3 多项目规则分组与多环境切换
whistle 的规则面板支持创建多个规则集,我通常一个项目一个组,比如project-a、project-b、project-c。需要联调哪个项目,就启用哪个规则集,一键切换,互不干扰。
规则集的内容长这样:
# 项目 A:API 全部转发到本地联调机 /api/ http://192.168.31.10:8080 # mock 用户接口,不依赖真实后端 /api/user file:///Users/me/mock/user.json # 登录域名直接指向预发环境 login.project-a.example.com http://pre.project-a.example.com这组规则表达了三件事:代码里没有改动,但 /api 路径的请求都被转到了 192.168.31.10 这台联调机;用户接口单独 mock 成本地文件;登录域名直接指向预发环境。当联调结束,停用这个规则集,一切恢复正常。
规则匹配也支持正则和通配符,比如把某个域名下所有图片请求代理到本地目录。多个规则同时命中时,whistle 按照从精确到模糊的顺序匹配,越具体的规则优先级越高。这个优先级逻辑和 Nginx 的 location 有些类似,理解之后规则配置会顺手很多。
4.4 联调结束忘关代理的血泪教训
这里必须分享一个我真实踩过的大坑。有一次联调结束,我急着去吃饭,whistle 的系统代理开关没关。下午回来直接打开线上页面,发现页面上所有接口全部请求到了本地空服务,白屏了很久,我还以为是线上环境挂了,拉了几个同事一起查,最后才发现是代理还开着。
从那以后,我给自己定了几条铁律:联调结束,第一件事关闭 whistle 的系统代理开关;规则文件命名带项目名,统一放在一个目录里并入库;切换环境前先看一眼代理状态。工具能帮你管规则,但它管不了你记不记得关开关,这种“随手关开关”的肌肉记忆只能靠习惯养成。现在我把这些约定也写进了团队的开发规范,新同学很少再犯类似的错。
5. Nginx 反向代理:多个后端服务一个入口
5.1 服务一多,端口就乱
后端只要一拆微服务,端口就开始失控。用户服务 8081,订单服务 8082,支付服务 8083,这还算少的;如果每人本地还跑着不同分支、不同端口,前端开发的时候就要在代码里维护一张“服务地址对照表”,切环境全靠人肉改。
反向代理解决的就是这个问题。把一个域名和一个端口作为统一入口,根据 URL 路径把请求分发到不同的上游服务。前端只需要面向http://dev.local:8080开发,根本不需要知道用户服务当前跑在哪个端口。
5.2 一套可复用的统一入口配置
下面是一个 Nginx 反向代理的最小配置,把三个服务聚合到一个入口:
upstream user_service { server 127.0.0.1:8081; } upstream order_service { server 127.0.0.1:8082; } upstream pay_service { server 127.0.0.1:8083; } server { listen 8080; server_name dev.local; location /api/user/ { proxy_pass http://user_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; } location /api/order/ { proxy_pass http://order_service/; } location /api/pay/ { proxy_pass http://pay_service/; } }保存到/etc/nginx/conf.d/dev.local.conf,然后执行:
nginx -t nginx -s reload访问http://dev.local:8080/api/user/profile,请求会被转发到http://127.0.0.1:8081/profile。这里注意,proxy_pass后面带不带斜杠,行为完全不一样,下面细说。
5.3 用 include 管理多项目配置
如果你和我一样同时维护好几个项目,不要把所有配置塞进同一个文件。我的习惯是每个项目一个文件,放在/etc/nginx/conf.d/下,文件名带项目标识:
/etc/nginx/conf.d/ ├── project-a.conf ├── project-b.conf └── project-c.confNginx 的主配置文件里默认有include /etc/nginx/conf.d/*.conf;,不需要额外处理。这样一来,每个项目的 Nginx 配置可以独立更新、独立 校验、独立 reload,互不影响。任何时候想临时停掉某个项目的入口,只要把对应的 .conf 文件改名或者移走,再 reload 即可,非常灵活。
5.4 这类配置最容易翻车的三个细节
第一个是proxy_pass的斜杠问题。location /api/user/ { proxy_pass http://user_service/; }表示把 /api/user/ 这截路径换掉再转发给上游,最终变成/profile;如果proxy_pass http://user_service;不带斜杠,则是把完整路径/api/user/profile原样转发给上游。很多接口 404,就是这里少写了一个斜杠。
第二个是 location 的优先级。Nginx 的 location 匹配顺序是:精确匹配=优先,然后是前缀匹配^~,再是正则~,最后才是普通前缀匹配。如果你同时写了多个 location,实际命中的可能和你直觉上的不一样,排查时一定要先想到这一层。
第三个是 WebSocket 转发。如果某个接口走的是 WebSocket,光写proxy_pass是不够的,还要额外加两行:
proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade";否则前端一直报连接断开。另外,长连接场景下proxy_read_timeout默认只有 60 秒,如果业务需要长时间保持,记得调大,比如proxy_read_timeout 3600s。这些细节不踩一次坑很难记得住,写在这里就当给大家省点时间。
6. 三件套组合实践:一次新需求从零到通的完整流程
6.1 推荐的组织结构
三个工具各自管一层,配置也要分开存放,我的开发机上大概长这样:
~/workspace/dev-proxy/ ├── envrc-examples/ │ ├── project-a.envrc.example │ └── project-b.envrc.example ├── whistle-rules/ │ ├── project-a.txt │ └── project-b.txt └── nginx-conf/ ├── project-a.conf └── project-b.conf这个目录不一定要在多台机器之间同步,但它是一个“代理配置的样板间”,新项目照着往里加即可,不用每次都从头想一遍。
6.2 新增一个代理需求的六步操作
假设现在有一个新项目 project-x,后端同学说联调机地址是 192.168.1.50:8080,前端开发要代理 /api,同时本机出网要走 whistle。我的操作流程基本是六步:
- 在项目根目录创建 .envrc,把 HTTP_PROXY 指向 127.0.0.1:8899,NO_PROXY 加上内网域名,然后
direnv allow。 - 在 whistle 里新建一个
project-x规则集,写入/api/ http://192.168.1.50:8080,启用它。 - 在
/etc/nginx/conf.d/下新建project-x.conf,把统一入口加进去,执行nginx -t校验,然后nginx -s reload。 - 打开系统代理开关,确认浏览器能正常访问 whistle 管理页、能打开本地页面。
- 在终端里
curl http://dev.local:8080/api/health,看返回是否符合预期,如果不对,依次检查 direnv 环境变量、whistle 规则、Nginx 配置这三层。 - 联调结束,关闭系统代理,在团队群里同步“project-x 联调完成,代理已关闭”。
这套流程走顺之后,新增一个代理需求基本控制在十分钟以内,而且因为每一层都只有一个入口,出了问题很快能定位。
6.3 团队划清边界后的变化
把三件套引入团队之后,最大的变化是“代理相关的求助消息变少了”。以前上午刚帮同事改完环境变量,下午又有人因为切错配置文件找过来,现在每个工具都有自己的配置文件,各自的职责边界清清楚楚。direnv 管不了转发规则,whistle 管不了路由入口,Nginx 也管不了环境变量,不需要再猜是谁污染了谁。
我个人在实际使用中最受益的地方,是排查问题的层次感。遇到接口不通,我会按顺序快速验证:先看 direnv 在当前目录下有没有正确加载,再到 whistle 管理页看匹配的规则,最后看 Nginx 的 error.log。大多数问题都能在五分钟内解决,不用再像以前那样翻遍所有配置也找不到线索。
最后再分享一个小技巧:把三个工具的检查命令写成一个proxy-status脚本,一条命令打印当前 direnv 环境变量、whistle 规则启用状态、Nginx 配置校验结果。日常切环境的时候跑一下,心里有底得多。工具管的不是玄学,是确定性和边界感,这才是代理不乱的关键。