ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

React开发中代理配置详解:从跨域原理到工程实践

React开发中代理配置详解:从跨域原理到工程实践

你是否经历过这样的场景:前后端联调时,接口请求明明发出去,浏览器 Network 面板里却躺着一排红色报错,提示 CORS 跨域。项目里有人告诉你“加个代理就好了”,但具体怎么配、为什么这么配、哪些参数一改就崩,没人说清楚。我前后在多个 React 项目里踩过代理配置的坑,今天把这类配置从原理到实操完整梳理一遍,希望对正在被跨域问题折磨的人有点帮助。

先说清楚一件事:本文讨论的“代理”,是前端开发服务器(devServer)层面的接口转发代理,目的是让浏览器发出的请求“看起来”是同源的,从而绕过跨域限制。这和系统级网络代理、任何网络访问工具都没有关系,纯粹是本地开发联调的标准手段。它解决的是这样一个高频问题:前端跑在 3000 端口,后端接口跑在 8080 端口,两者端口不同,直接请求必然被浏览器拦截。你当然可以让后端去开 CORS,但更多时候,前端手里那套开发服务器配置更灵活、更不需要求人。

适合读这篇文章的人,主要是三类:一是刚接触 React 脚手架项目的开发者,二是长期做前后端分离、经常需要切换联调环境的前端工程师,三是对代理报错束手无策、只能复制粘贴配置但不知道原理的人。读完之后,你至少能回答三个问题:代理配置的每个字段到底在干什么?多环境、多接口场景怎么组织配置?配完之后出问题怎么定位?

1. 为什么React开发中要配代理?——先把跨域这件事聊透

1.1 跨域问题的本质

浏览器在安全策略上有一个核心规则叫“同源策略”。所谓同源,指的是协议、域名、端口三者完全一致。只要其中一个不同,浏览器就会认为这是两个不同源的环境,对脚本发起的跨源 HTTP 请求默认进行拦截。注意,这里拦截的是“响应”,请求实际上可能已经发到了服务器,但浏览器把返回数据拦下来不交给前端脚本,所以你在控制台看到的是网络请求报错。

拿本地开发举例:脚手架创建的项目启动后默认地址是http://localhost:3000,后端服务跑在http://localhost:8080。前端代码里写axios.get('http://localhost:8080/api/user'),端口不同,属于跨域。浏览器弹出 CORS 错误,接口数据拿不到,这就是绝大多数前端新手遇到的第一个“玄学问题”。

实际上这个限制是浏览器主动做的一个保护机制,它防止恶意网站通过脚本访问你登录过的其他网站的敏感接口。但开发阶段,前后端分离让这个限制变得非常碍事——前端要频繁请求后端接口,总不能每次都让后端临时改 CORS 配置。

1.2 为什么代理是最省事的方案

解决跨域,业内常见的有三种思路:后端开 CORS、前端用 JSONP、开发服务器配代理。三者的选择逻辑值得说道说道。

后端开 CORS 是最直接的,在响应头里加上Access-Control-Allow-Origin等字段。但缺点很明显:你得能改后端代码,或者让后端同事配合你加规则。有时候你对接的是别人已经部署好的测试环境,根本没权限改服务端配置。另外,如果前端需要带 Cookie 或自定义请求头,CORS 还要处理预检请求,规则更多一层,一不小心就漏配。

JSONP 只支持 GET 请求,而且得后端接口专门返回一段可执行的回调代码,现在主流项目基本都是 JSON 接口,这个方案基本被淘汰了。

代理方案的本质是:让前端请求不再直接发给后端,而是发给自己的开发服务器(比如 3000 端口的服务),开发服务器接收到请求后再转发给真正的后端服务器(8080 端口)。因为服务器之间的请求没有浏览器同源策略限制,所以转发畅通无阻。响应回来时,开发服务器把它伪装成自己返回的数据,浏览器也就高高兴兴接收了。

我用一个生活中常见的类比帮助理解:这就好比你去公司前台取快递,你不想让快递员直接进办公区,就让所有快递统一送到前台,再由前台转交给你。前台就是代理,快递员就是后端服务器,你就是浏览器。浏览器只认自己这一栋楼里的“前台”,前台去帮你跑腿,问题就解决了。

1.3 代理与直接请求的差异对比

我把两种方式放在一起做个对比,方便你直观感受代理的价值。

对比维度前端直接请求后端通过 devServer 代理转发
请求地址http://localhost:8080/api/user/api/user(相对路径)
浏览器是否跨域是,被拦截否,请求发往同源的开发服务器
后端是否需要特殊配置需要开启 CORS不需要额外配置
Cookie 携带跨域受限同源场景,自然携带
环境切换需要改前端代码里的完整地址只需改代理配置的 target

这条对比表基本就是我选择代理而不是让后端配 CORS 的原因:不依赖他人、环境切换成本低、前后端本地开发完全解耦。接下来就进入正题,看看在 React 项目里具体怎么配。

2. 核心配置:React脚手架里的代理到底怎么配

2.1 最基础的代理写法

在 React 项目里,webpack 的 devServer 提供了 proxy 配置项,这是改造的基础。如果你用的是官方脚手架创建的项目,可以直接在项目根目录的配置文件中加一个proxy字段。不同版本的工具支持的形式略有差别,但核心思路一致:一个对象,键是匹配的路径前缀,值是要转发的目标地址。

最基础的写法是这样的:

// 项目根目录配置文件中添加 proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } }

这段配置的含义是:只要请求路径以/api开头,开发服务器就把请求转发到http://localhost:8080上。比如前端请求/api/user,实际转发给后端的是http://localhost:8080/api/user。此时请求地址是相对路径,浏览器认为请求发给了自己的开发服务器,不存在跨域问题。

这里有两个细节必须注意:第一,proxy字段必须写在顶层配置对象中,不是写在devServer里面再套一层,具体看脚手架版本要求;第二,配置改动之后必须重启开发服务器,我之前犯过改完配置不重启,盯着浏览器看了五分钟,以为配置写错了。

2.2 必须搞懂的三个关键参数

很多人的代理改完不生效,或者接口 404,问题往往出在参数理解不到位。我挑三个最常用的参数讲透。

第一个是target,含义很直白,就是转发的目标服务器地址。但有个细节:它只是“目标地址”,不会自动帮你拼路径,你请求的完整路径会原样接上去。所以你要确保后端接口实际路径和你前端请求路径匹配。

第二个是changeOrigin。这个参数的作用是修改请求头里的Host字段。默认情况下,开发服务器转发请求时,Host字段保留的是原始域名,也就是localhost:3000。有些后端服务做了域名白名单校验,发现Host不是自己认识的域名就返回 403。设置changeOrigin: true之后,开发服务器会用target的域名作为Host,模拟请求是发给目标域名的,从而绕过这类校验。我的建议是,开发场景一律设为true,省得后端域名一校验就出各种幺蛾子。

第三个是pathRewrite,这是最容易忽略也最容易写错的一个。它的作用是重写转发路径。比如后端接口实际地址是http://localhost:8080/user,前端代码里统一用/api/user来发请求,转发过去之后后端不认识api这一层。这时你就需要把/api前缀干掉:

proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true, pathRewrite: { '^/api': '' } } }

这段配置的意思很明确:请求/api/user时,先去掉开头的/api,再把剩余部分/user拼到target后面,于是实际转发地址变成http://localhost:8080/user。^/api这种写法是正则语法,^表示匹配以/api开头的字符串。如果不用^,写'/api',它会把所有出现的/api都替换掉,很容易误伤路径中其他的相同片段。

2.3 参数选型背后的逻辑

为什么有 split 式的配置方式,也有函数式的配置方式?我简单解释一下各自适合的场景。

对象形式的配置适合路径前缀固定、规则简单的项目。函数形式则适合需要根据不同请求动态决定转发目标的场景,比如多后端服务按请求头切分。以我在某公司的实际经历为例,一个后台管理系统需要同时请求用户服务和订单服务两个后端,分别跑在 8081 和 8082 端口,对象形式就得写两段:

proxy: { '/api/user': { target: 'http://localhost:8081', changeOrigin: true }, '/api/order': { target: 'http://localhost:8082', changeOrigin: true } }

如果两个服务还有共同的/api/auth路径,请求会先匹配更长的前缀,所以/api/user和/api/order要放在/api之前或者单独写,否则会被更短的/api规则先拦截。实际开发中,这种多服务共存的场景挺常见的,配置顺序需要格外留意,规则匹配是从上往下按优先级来的,不是按对象书写顺序来,这是好多人踩过的坑。

3. 不同环境下的代理配置方案

3.1 区分开发与联调环境的配置写法

本地开发时,后端在自己电脑上跑,代理目标通常是http://localhost:8080。但联调阶段,测环境的后端可能在某个远程地址,比如http://192.168.1.100:8080。如果每次切换环境都手动改配置,既容易忘,又容易在提交代码时误把本地配置带到仓库里。

这里我的做法是把代理目标抽出来,配合环境变量按需加载。React 脚手架本身支持通过.env.development、.env.production等文件注入环境变量。我在.env.development里加一个自定义变量:

REACT_APP_API_TARGET=http://192.168.1.100:8080

然后在代理配置里读取这个变量:

proxy: { '/api': { target: process.env.REACT_APP_API_TARGET || 'http://localhost:8080', changeOrigin: true } }

这样,代码仓库里保留的是一套使用环境变量的“通用配置”,每个人本地开发时只需在自己的环境文件里指定目标地址,切换后端环境不会污染公共代码。注意,自定义环境变量必须带REACT_APP_前缀,否则脚手架不会把它注入到前端代码里,这是工具本身的硬性约定。

3.2 HTTPS 与 WebSocket 代理的特殊处理

本地开发如果用了 HTTPS,证书通常是自签名的,开发服务器转发请求到 HTTPS 后端时,Node 环境会因证书不受信任而报错。此时可在代理配置里加上:

secure: false

意思是关闭对目标服务器 SSL 证书的校验。仅限本地开发使用,上线前务必检查确认不要带到生产环境,否则削弱了基本的传输安全校验。另外,如果后端是wss://的 WebSocket 服务,代理还需要单独开启 WebSocket 支持,配置如下:

proxy: { '/ws': { target: 'ws://localhost:8080', ws: true, changeOrigin: true } }

加上ws: true之后,开发服务器会把 WebSocket 的连接请求也一并代理转发。我在做某个实时看板项目时,就是因为漏了这个参数,WebSocket 一直握手失败,控制台报错信息又模棱两可,排查了很久才发现是代理只转了 HTTP 请求,没转 WebSocket 升级请求。

3.3 生产环境还需要代理吗

这个问题很多新人问。严格来说,生产环境部署后,前端静态资源和后端接口通常在同一个域名或网关之下,不存在前端开发阶段的跨域问题。即使存在跨域,也应该由部署层的网关或反向代理解决,而不是靠前端开发服务器。

我在生产环境的常见部署方式是:前端打包出来的静态资源放到 Nginx 或者云服务的静态站点里,后端 API 挂在同一个域名下的/api路径,由反向代理层转发到真正的后端服务。前端代码里直接写/api/user这种相对路径,部署的时候网络层自然就把请求转到正确的位置。这也是为什么我推荐前端代码统一使用相对路径fetch('/api/user')而不是fetch('http://localhost:8080/api/user'),这样代码从开发到上线,请求路径不用改,只改各环境的网络配置就行。

4. 代理配置的常见问题与排查技巧实录

4.1 改了配置但请求还是走原地址

现象是:请求 URL 依然显示http://localhost:3000/api/user,没有转发到http://localhost:8080。大概率是代理配置没生效,或者配置位置不对。首先确认你改的是否是最终生效的 webpack 配置文件,有些脚手架把 webpack 配置拆分成多个文件,你改了一个被覆盖的副本,自然没反应。其次确认是否重启了开发服务器,代理配置不属于热更新的范围,必须手动重启。

还有一个隐蔽问题:如果你在代码里用axios.get('http://localhost:8080/api/user')写死了完整地址,那么请求根本不会经过开发服务器,代理自然也就不起作用。代理只对“相对路径”或者“与开发服务器同源的路径”生效,写死完整地址等于绕过了代理层。碰到这种情况,把请求地址改成/api/user就完事了。

4.2 代理生效了但接口返回 404

请求确实转发到了后端,但响应是 404。这种问题十有八九出在路径匹配上。最常见的是后端接口没有/api前缀,而前端路径带了,且你没有配置pathRewrite。比如后端真实接口是/user,你访问的是/api/user,后端找不到这个路径,就会返回 404。

排查方法很笨但很有效:先在浏览器直接访问后端真实地址,确认接口存在;再用代理访问前端地址,观察转发后的实际路径。你可以临时在pathRewrite里加一段日志输出,或者在后端接口日志里查看收到的请求路径。我一般直接去后端控制台确认访问日志,看 URL 长什么样,对比一下就知道重写规则对不对了。

另一个容易忽略的坑是接口带有路径参数,比如/api/user/123,如果你的pathRewrite写的是'/api'而不是'^/api',那么/api/user/123里的/api会被替换,但后续路径保持不变,正常情况下也能工作。但如果接口路径里在非开头位置也出现了/api字样,就会被误替换,所以一律用^/api更保险。

4.3 代理之后 Cookie 丢失

如果你在代理场景下使用了跨域 Cookie 登录态,可能会发现请求能通,但 Cookie 始终带不上,或者登录状态一直丢失。这是因为浏览器在同源策略下,只会给同源请求自动携带 Cookie。开发代理下的前端请求虽然“看起来是同源的”,但部分场景下浏览器对 Cookie 的写入和携带仍有些额外限制。

解决方法通常是确保请求携带凭证:使用 axios 时设置withCredentials: true,使用 fetch 时设置credentials: 'include'。同时要注意代理配置本身不影响响应头里的Set-Cookie,如果后端响应里通过Set-Cookie写入 Cookie,浏览器是否接受还跟 Cookie 的 Domain、SameSite 属性相关。开发阶段遇到这种问题,最简单的验证方式是先去掉代理,直接请求后端看 Cookie 是否正常;确认后端没问题后,再排查代理层面的参数配置。

4.4 常见问题速查表

我把开发中遇到的高频问题整理成一个表格,排查时按图索骥很快。

症状可能原因排查思路
请求未转发,CORS 报错代理配置未生效检查配置文件位置、是否重启、请求是否为相对路径
404 找不到接口后端路径与前端路径不匹配查后端日志,确认转发后的实际路径,检查 pathRewrite
403 或域名校验失败Host 头未修改给代理配置加changeOrigin: true
WebSocket 握手失败缺少 WebSocket 代理支持加上ws: true参数
HTTPS 证书报错目标服务器证书不受信任开发环境临时设置secure: false
代理路径被更短的前缀拦截规则匹配顺序不对调整代理规则顺序,长前缀规则放在前面

这张表不是我随手拍的,每一项背后都是真实的排障经历。比如那一次我排查了很久的“代理不生效”,最后发现是同事在 package.json 里的字段写法跟脚手架版本不兼容,代理配置被工具整个忽略掉了,所以第一步检查配置是否被有效加载,永远排在第一位。

5. 一套顺手的代理配置完整方案(可直接抄作业)

5.1 一个覆盖多场景的模板

下面这份配置模板,我目前基本是复制到新项目里改改目标地址就用的。它兼顾了多前缀、路径重写、WebSocket、HTTPS 关闭证书校验等常见情况:

proxy: { '/api': { target: process.env.REACT_APP_API_TARGET || 'http://localhost:8080', changeOrigin: true, secure: false, pathRewrite: { '^/api': '' } }, '/ws': { target: process.env.REACT_APP_WS_TARGET || 'ws://localhost:8080', ws: true, changeOrigin: true } }

配合.env.development文件:

REACT_APP_API_TARGET=http://192.168.1.100:8080 REACT_APP_WS_TARGET=ws://192.168.1.100:8080

这套组合解决了我日常 90% 的开发联调需求。组织逻辑先说清楚:/api走 HTTP 接口转发,去掉前缀交给后端;/ws走 WebSocket 转发,保留前缀或者按后端协议调整重写规则。两个规则的匹配顺序谁前谁后无所谓,因为路径前缀不会互相重叠。

5.2 路径规范的建议

关于请求路径的组织,我强烈建议前端代码里统一使用/api作为业务接口前缀,具体后端服务类型通过次级前缀区分,比如/api/user、/api/order。这样做有两点好处:一是在代理配置里只用维护一个/api前缀,路径重写规则集中;二是部署到生产环境时,网络层只要把/api转发给后端容器就行,规则简单不容易出错。

我见过某些项目把接口前缀写得五花八门,有的带版本号/v1/user,有的用模块名/user-service/user。代理配置相应拆成好几块,每次联调都要反复确认转发路径,非常痛苦。如果后端接口命名在团队里没法统一,前端可以自行在代理层做一层兼容映射,比如:

proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true, pathRewrite: { '^/api/user': '/user', '^/api/order': '/order' } } }

这样前端看到的路径是一致的/api/...,后端看到的路径是符合自己规范的接口路径,代理层做了一次字段映射。这种思路尤其适合对接多个历史遗留后端服务的场景。

5.3 上线前记得处理这些尾巴

代理配置有一个很容易被忽略的问题:它只存在于开发阶段,打包后的生产代码里不会包含这套代理逻辑。所以上线前要检查的是:前端生产环境请求的 URL 是否用的是相对路径,以及生产网络层是否正确配置了反向代理。很多项目在开发时用/api/user,上线时也把/api/user写进代码,却没有在部署层配置转发,结果线上接口全部 404。这不是代理的问题,而是部署规划的问题,需要前后端和运维同学提前对好路径规范。

我在之前的某个项目中做过一次“线上回归清单”:第一步,检查前端代码中是否还有localhost之类的硬编码开发地址;第二步,检查打包产物中静态资源的引用路径是否正常;第三步,访问线上页面,通过浏览器开发者工具确认接口请求 URL 与网关转发规则一致。这三步走完,生产环境代理类问题基本能提前拦住。

6. 写在最后的排障心得

配置代理本身并不复杂,配置项也就那么几个,但这类问题往往让人气馁的地方在于:报错信息不直接,现象千奇百怪。我个人的体会是,遇到代理问题先别急着改配置,按“请求是否经过代理”和“转发后的路径是否正确”这两个维度去拆。第一个维度看请求地址和开发服务器的网络日志,第二个维度看后端日志。只要确认请求确实到达了后端,剩下的就是路径匹配和参数的问题,范围一下子就缩小了。

还有一个小工具推荐:浏览器开发者工具里的 Network 面板,配合“Preserve log”开关,可以清晰地看到请求是发到开发服务器还是直接发到了后端,这一步基本能筛掉一半问题。另外,如果你在某一步反复确认配置没错但还是不生效,可以新建一个最简单的测试项目,只配置一个代理规则试试,往往能帮你分辨是工具配置问题还是业务代码问题。

最后再分享一个实际操作中的小技巧:代理目标地址尽量不用 IP 加端口的形式写死在配置里,而是用环境变量驱动。这样同事换了一个后端环境,不用动公共代码,只需修改自己本地的环境文件。这个习惯我保持了很长时间,它也成了我在团队里推荐的标准做法。配置代理是小事,但处理好这些细节,能让协作顺畅不少。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进