首页 / 资讯中心 / 文章详情

React脚手架代理配置详解:从跨域到生产环境一次说清

React脚手架代理配置详解:从跨域到生产环境一次说清 ★ FEATURED ARTICLE
开发项目时本地前端服务跑在 3000 端口后端接口跑在 8080浏览器一请求就报跨域。很多人第一反应是让后端加 CORS 头或者用浏览器插件临时绕过但这些都治标不治本。React 脚手架自带的代理配置就是针对这个场景设计的标准解法也是我每次新起前端项目时最先落地的基础设施之一。这篇总结不是抄官方文档而是把我从“只会配 proxy 字段”到“能处理多个后端、路径重写、Cookie 丢失、生产环境失效”这一路踩过的坑和验证过的方案完整梳理一遍。无论你是刚接触脚手架的新手还是已经被代理问题折磨过的老手这篇文章都值得花十分钟读完。1. 本地开发为什么要一张“中转的车票”跨域问题的本质浏览器安全策略里的同源限制是前端开发中绕不开的一道墙。所谓同源指的是协议、域名、端口三者完全一致。开发时前端跑在http://localhost:3000后端跑在http://localhost:8080端口不同已经不是同源了浏览器自然拦截跨域响应。1.1 为什么不能直接让后端放开 CORS 一劳永逸后端加 CORS 头确实能解决一部分问题但在开发阶段往往不现实。比如后端是某个老项目代码里没有统一的请求入口Header 配置分散在各处或者后端环境由别人维护你提出加跨域配置对方要排期、要测试、要走发布流程一折腾就是半天。就算后端真的加了Access-Control-Allow-Origin: *麻烦也还在。某些时候浏览器会先发预检请求OPTIONS后端如果没有处理 OPTIONS 请求的逻辑预检直接失败正式请求根本发不出去。还有携带 Cookie 的场景Access-Control-Allow-Credentials和前端的withCredentials必须同时配置任何一边漏掉Cookie 就带不上。1.2 代理方案的实际原理同源请求“借道中转”代理的本质是把原本跨域的请求“伪装”成同源请求。前端页面请求http://localhost:3000/api/user脚手架启动的开发服务器拦截到/api开头的请求转发给http://localhost:8080/api/user服务端没有跨域概念正常返回数据开发服务器拿到响应后再原样返回给浏览器。此时浏览器看到的是来自localhost:3000的响应同源不拦截。这个过程中请求头和响应头在转发时通常都会被保留适合绝大多数 API 场景。我一直把代理理解成“中转车票”前端买的票是去 3000 站的代理帮你把行李请求转送到 8080 站取回行李响应再交给你全程没有让浏览器直接接触 8080所以“跨域”这张墙纸根本不会被触发。提示代理只存在于开发服务器层面上线部署后脚手架开发服务器就不再参与代理规则也就不生效了。生产环境的跨域问题要靠 Nginx 反向代理或后端 CORS 解决这是完全不同的另一套逻辑后面专门说。2. 一条字段走天下的阶段package.json 里加 proxy用 React 脚手架创建项目之后最省事的代理配置方式就是直接在package.json里配一个proxy字段。这个字段是开发服务器底层是 webpack-dev-server预留的快捷入口专门给没有复杂代理需求的场景用。2.1 配置的最小写法和它能匹配的路径在package.json中加上{ name: my-app, proxy: http://localhost:8080 }保存后重启开发服务器一般需要重启才能生效然后前端代码里把请求地址写成相对路径fetch(/api/user/list) .then(res res.json()) .then(data console.log(data));请求发到http://localhost:3000/api/user/list开发服务器发现proxy配置了目标地址http://localhost:8080就会把请求完整转发过去。后端收到的仍然是/api/user/list前端拿到的响应体来自后端一切正常。2.2 用 proxy 字段时最容易踩到的三个隐藏规则第一proxy字段的值只能是一个字符串也就是说只能指定一个后端地址。如果你同时要对接 A 服务的登录接口、B 服务的列表接口、C 服务的文件上传接口这个方案就撑不住了。第二前端请求必须是以相对路径形式发出。如果代码里写的是fetch(http://localhost:8080/api/user/list)直接指向了后端地址那么请求根本不会经过开发服务器的代理转发跨域问题依然存在。第三代理转发是“无脑透传”的不会帮你做路径改写。什么意思fetch(/api/user)转发后后端看到的是/api/user不是/user。如果后端接口设计没有/api前缀而前端为了区分代理请求统一加了/api后端就会找不到路由返回 404。这种场景要用路径重写在第二种方案里展开说。2.3 为什么很多人点开 Network 面板后一脸懵请求并没有变成后端地址这是一个非常容易误解的地方。配置代理后你在浏览器开发者工具里看到的请求地址依然是http://localhost:3000/api/user/list而不是http://localhost:8080/api/user/list。因为浏览器只跟 3000 端口交互代理转发是在“网络层之下”完成的浏览器感知不到真实后端地址。所以判断代理是否生效不要只看 Network 面板的 URL要看 Response Headers 里的内容是不是来自后端或者直接观察后端日志是否有请求打进来。# 后端控制台如果打印了请求日志说明代理转发成功 [INFO] 2024-05-20 14:32:11 - GET /api/user/list 200如果后端没有任何日志说明请求根本没有到达后端要么代理配置没生效要么请求路径没匹配上。3. 多后端并存时的升级打法http-proxy-middleware 配合 setupProxy.js项目一旦需要对接多个后端地址或者需要对不同前缀做不同转发规则package.json里的proxy字段就必须要让位给更灵活的http-proxy-middleware。这个中间件是 React 脚手架官方文档推荐的替代方案配置写在src/setupProxy.js里。3.1 安装和基本配置结构首先安装依赖npm install http-proxy-middleware --save-dev # 或者用 yarn yarn add http-proxy-middleware --dev然后在src目录下新建setupProxy.jsconst { createProxyMiddleware } require(http-proxy-middleware); module.exports function(app) { app.use( /api, createProxyMiddleware({ target: http://localhost:8080, changeOrigin: true, }) ); };这个文件的加载是脚手架自动完成的开发服务器启动时会读取src/setupProxy.js并应用里面的 Express 中间件逻辑不需要手动引入到组件里也不需要修改webpack配置。3.2 多路由规则的最常用排列当后端不止一个时可以写多个中间件规则const { createProxyMiddleware } require(http-proxy-middleware); module.exports function(app) { // 用户相关接口转发到 A 服务 app.use( /api/user, createProxyMiddleware({ target: http://localhost:8081, changeOrigin: true, }) ); // 商品相关接口转发到 B 服务 app.use( /api/product, createProxyMiddleware({ target: http://localhost:8082, changeOrigin: true, }) ); // 文件上传单独走 C 服务 app.use( /upload, createProxyMiddleware({ target: http://localhost:8083, changeOrigin: true, }) ); };配置changeOrigin: true的目的是把请求头里的Host字段改成目标域名。有些后端会根据Host做不同处理不设置的话后端看到 Host 是localhost:3000可能会报错或返回异常内容。3.3 pathRewrite 的用处去掉前缀、改写路径刚才说过proxy字段没法改写路径。而http-proxy-middleware提供了pathRewrite让你可以随心所欲调整转发到后端的路径。比如前端统一用/api开头后端路由没有这个前缀app.use( /api, createProxyMiddleware({ target: http://localhost:8080, changeOrigin: true, pathRewrite: { ^/api: , // 把 /api 前缀剥掉 }, }) );这样前端请求/api/user/list后端实际收到的是/user/list。正则写法稍微有点绕但核心就是一个^/api: 表示把所有以/api开头的路径中的/api替换为空字符串。还可以做更复杂的改写比如把版本号从路径里剥离pathRewrite: { ^/api/v1: /v1, ^/api/v2: /v2, }如果你的后端同一个环境里既有 v1 又有 v2 接口这种写法可以一个代理入口统一转发后端根据路径区分版本。3.4 带 Cookie 和 WebSocket 的代理细节开发时涉及登录态需要把 Cookie 带到后端代理配置里要增加createProxyMiddleware({ target: http://localhost:8080, changeOrigin: true, cookieDomainRewrite: localhost, // 把 Set-Cookie 里的域改写成 localhost onProxyReq: function(proxyReq, req, res) { // 必要时手动设置 Cookie const cookie req.headers.cookie; if (cookie) { proxyReq.setHeader(cookie, cookie); } }, });cookieDomainRewrite解决的是后端种 Cookie 时指定了域导致浏览器无法保存的问题。如果不做改写后端返回Set-Cookie: Domain.example.com; ...浏览器发现当前域名是localhost不匹配Cookie 就被拒绝了登录态自然丢失。WebSocket 代理需要额外传ws: truecreateProxyMiddleware({ target: http://localhost:8080, changeOrigin: true, ws: true, });如果你的项目用到 WebSocket比如推送消息不开启这个选项连接会被开发服务器默默丢弃前端一直收不到消息且网络面板里看不出明显报错非常隐蔽。4. 两种方案到底怎么选场景判断与决策清单proxy字段和setupProxy.js不是二选一的严格排他关系但在实际项目中我的习惯是单一后端、路径不需要改写、临时联调用用proxy字段只要涉及多个后端、需要改写路径、开启 WebSocket 或 Cookie 改写直接上setupProxy.js。下面这个表格是我在项目评审时常拿出来的对比对比维度package.json 的 proxysetupProxy.js http-proxy-middleware配置位置单一字符串独立 JS 文件可编程多个后端支持不支持支持路径改写不支持支持pathRewriteCookie 域改写不支持支持cookieDomainRewriteWebSocket 支持默认支持简单场景可显式控制ws: true按环境动态配置很难可根据环境变量灵活配置底层原理脚手架封装Express 中间件直管4.1 一个反直觉的事实url 前缀不匹配时proxy 字段也能转发proxy字段有个冷门行为请求路径不管有没有特殊前缀只要当前域名端口是开发服务器自己它都会尝试转发到目标地址。也就是说fetch(/random/path)也会被转发到后端只是后端可能返回 404 而已。而http-proxy-middleware的规则则必须指定匹配路径前缀比如/api不在前缀范围内的请求会正常走开发服务器返回前端资源不会转发。这个差异会带来一个开发时的“看似正常实则奇怪”的现象用proxy字段时前端写一个/api/xxx的错误路径可能后端返回 404 页面而前端却拿到了 200 的 HTML 内容用http-proxy-middleware时同样的请求可能直接被开发服务器当成静态资源请求处理返回index.html数据却是 0 字节。排查这类问题时我的经验是先看后端有没有请求日志。有日志说明代理规则生效了再看返回内容。没有日志则优先检查规则前缀是否匹配。4.2 实际项目中的三个典型选型场景场景一你在做一个内部管理后台后端只有一个 Java 服务接口都带/api前缀前后端联调文件不多。这种直接配proxy字段十分钟搞定少即是多。场景二公司整体微服务化不同模块对应不同后端服务前端需要同时访问用户服务、订单服务、支付服务。这种就只能用setupProxy.js每个服务一条规则清晰可维护。场景三项目同时接后端 API 和第三方地图服务地图服务需要特殊 HeaderAPI 服务需要 Cookie。这种更复杂的场景setupProxy.js里可以在onProxyReq拦截器里按目标地址动态添加请求头。app.use( /map, createProxyMiddleware({ target: https://map.example.com, changeOrigin: true, onProxyReq(proxyReq, req, res) { proxyReq.setHeader(X-Custom-Key, process.env.MAP_KEY); }, }) );前端写fetch(/map/place/search?kwxxx)代理会自动加上密钥 Header第三方服务无需暴露密钥给浏览器。5. 那些让排查成本飙升的报错现象、根因和药方代理配置本身并不难难的是报错时一脸茫然。我把自己在项目中遇到的高频问题整理成了一份排查手册按现象分类可以直接照着操作。5.1 现象一Network 面板显示 404但浏览器页面正常最常见的情况大概占了代理问题的 40%。请求发到了http://localhost:3000/api/user开发服务器找不到静态资源返回了index.html内容类型是text/html所以接口虽然不是 200但页面没坏。排查步骤确认后端是否启动端口是否正确确认代理规则的路径前缀是否匹配确认target是否写错了协议http还是https确认后端路由是否真的存在/api/user。5.2 现象二报错Error occurred while trying to proxy: localhost:8080/api/user这个错误是代理转发到目标地址时失败常见于后端服务没启动后端服务启动在其他端口比如改成了 8081后端需要 HTTPS但代理配置写的是 HTTP后端有 IP 白名单changeOrigin: true后 Host 头变成了 localhost白名单校验失败。处理方法# 先确认后端端口是否真的在监听 curl http://localhost:8080/health如果curl有响应说明代理配置或路径有问题如果curl都连不上先去启动后端。5.3 现象三请求发出去了但响应一直 pending 不回来这种情况多数是因为代理配置成了 WebSocket但后端没有对 WebSocket 请求做响应后端接口本身死循环或请求阻塞代理网络层因为keepAlive保持了一个不健康的连接。我的处理方式是先断开所有代理规则直接用curl调后端接口看后端自身是否健康。后端没问题再逐步加代理规则测试。5.4 现象四代码里请求地址带了完整域名代理完全不生效fetch(http://localhost:8080/api/user/list)这是很多从其他框架转来的人最容易犯的错误。代理只拦截“相对路径”请求浏览器发出的绝对 URL 如果指向了非开发服务器地址就不会经过代理转发跨域问题再次出现。规范做法是项目内维护一个统一的请求基础路径// src/utils/request.js const BASE_URL /api; export function get(url) { return fetch(${BASE_URL}${url}); }这样请求全部走相对路径代理规则才能兜住。5.5 现象五代理在 Windows 上启动时报 EADDRINUSEEADDRINUSE表示端口被占用。开发服务器的默认端口 3000 被其他进程占用了需要换端口启动# 脚手架支持直接指定端口 npm start -- --port 3001然后用http://localhost:3001访问代理目标不变。6. 接口请求封装与代理联动的细节axios 里的 baseURL 和 withCredentials很多人配好了代理却发现 axios 发起的请求仍然有问题。核心原因是 axios 的默认配置和代理规则没有对齐。6.1 baseURL 到底该写什么在开发环境baseURL 不应该写成后端地址应该写成相对路径/apiimport axios from axios; const service axios.create({ baseURL: /api, timeout: 10000, });请求示例service.get(/user/list);最终发出的 URL 是/api/user/list代理规则匹配/api前缀转发到后端。有些人在 axios 里写了baseURL: http://localhost:8080/api代理基本等于白配跨域问题原样出现。axios 的 baseURL 和浏览器fetch一样走的是浏览器网络层请求会直接发给 8080跳过开发服务器的转发。6.2 withCredentials 和代理的联手如果项目里使用 Cookie 保持登录态axios 需要开启const service axios.create({ baseURL: /api, timeout: 10000, withCredentials: true, });此时代理配置里也要确保cookieDomainRewrite已配置否则后端Set-Cookie可能会因为域名不匹配被浏览器丢弃。两端都正常登录态才能完整穿透开发服务器。6.3 响应拦截器里的常见误判我在项目里常写一个响应拦截器当 HTTP 状态码为 401 时自动跳转登录页。但代理配好后如果后端接口有鉴权校验返回 401 是很正常的。拦截器逻辑要确保只在真实业务场景触发不要因为代理在 Network 里显示 401 就以为是代理故障。service.interceptors.response.use( response response, error { if (error.response error.response.status 401) { window.location.href /login; } return Promise.reject(error); } );如果后端接口本来就不需要鉴权却出现 401则要检查代理是否把请求头丢掉了常见的是AuthorizationHeader 在转发过程中被剥离。6.4 代理对请求方法敏感吗http-proxy-middleware默认对所有方法都生效包括 POST、PUT、DELETE 和 OPTIONS。但如果你在中间件里写了自己的逻辑或者后端对 OPTIONS 预检有要求可以增加方法过滤app.use( /api, createProxyMiddleware({ target: http://localhost:8080, changeOrigin: true, methods: [GET, POST, PUT, DELETE], }) );不写methods时默认全部方法都代理一般不用刻意设置除非你有特殊需求。7. 配置完代理后还要做的事验证清单和常见遗漏代理配置完成后很多人只在前端页面点两下看到数据出来了就以为结束了其实很多隐患没暴露。我个人的习惯是跑一套简单的验证清单。7.1 验证清单详情检查 Network 面板请求 URL 依然以localhost:3000开头检查 Response Headers字段来自后端比如Server头、自定义的X-Request-Id等打开后端控制台确认请求日志有输出测试 GET、POST、PUT 各至少一次测试带 Cookie 的请求确认 Set-Cookie 被浏览器接受如果有 WebSocket测试连接能建立、能收发消息切换接口路径比如从/api/user切到/api/product确认规则匹配正确。7.2 容易被忽略的一个文件.env.development代理规则里如果写死了后端地址不同开发人员本地后端端口不一致时会互相踩坑。我建议把目标地址抽到环境变量里const { createProxyMiddleware } require(http-proxy-middleware); const target process.env.REACT_APP_PROXY_TARGET || http://localhost:8080; module.exports function(app) { app.use( /api, createProxyMiddleware({ target, changeOrigin: true, }) ); };然后在项目根目录建.env.developmentREACT_APP_PROXY_TARGEThttp://localhost:8080其他人拉代码后修改.env.development里对应后端地址即可不需要改动任何 JS 文件。7.3 关于 https 目标地址的一个提示如果后端地址是https://api.example.com而本地开发服务器的证书是自签名的代理可能会因为证书验证失败而报错。这种情况通过在配置里加入secure: false解决createProxyMiddleware({ target: https://api.example.com, changeOrigin: true, secure: false, // 忽略自签名证书错误 })secure: false只影响代理到后端的连接不会强制浏览器信任证书所以生产环境不受影响。8. 生产环境还配不配部署阶段的跨域策略开发环境的代理解决的是本地联调问题一旦打包部署setupProxy.js和proxy字段都不再生效。生产环境的跨域处理是另一套逻辑需要提前规划。8.1 Nginx 反向代理的写法参考最常见的方式是 Nginx 将/api前缀的请求转发到后端服务server { listen 80; server_name example.com; location /api/ { proxy_pass http://backend-server:8080/api/; 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 / { root /usr/share/nginx/html; index index.html; } }注意proxy_pass结尾的/api/和没有/api/的区别这决定了是透传完整路径还是剥离前缀和开发环境pathRewrite的思维完全一致。8.2 后端 CORS 的兜底方案如果你无法控制 Nginx 配置或者后端部署在云函数等平台后端 CORS 是兜底手段。前端请求必须指向完整的后端地址然后后端在响应里加Access-Control-Allow-Origin: https://example.com Access-Control-Allow-Credentials: true Access-Control-Allow-Headers: Content-Type, Authorization这种方式只适合后端接口不多、域名固定的情况不然通配符失控安全性和可维护性都不够。8.3 上线前必须检查的跨域配置项上线前的自测清单我通常包含浏览器直接请求后端接口查看响应头是否包含允许跨域的字段模拟 POST 带 JSON 请求确认预检请求能被后端正确响应登录后刷新页面确认 Cookie 或 Token 存储方式在跨域情况下依然有效检查是否有任何前端代码残留了localhost:8080等开发地址。这条很关键开发时为了快速调试经常有人把baseURL临时改成后端地址上线前忘记改回来导致线上请求直接打到 localhost所有用户调用失败。建议在项目里加一个环境判断工具函数const isProduction process.env.NODE_ENV production; const BASE_URL isProduction ? https://api.example.com : /api;只要所有请求都通过这一个入口发出去就不会出现开发地址泄漏到生产环境的问题。9. 从代理配置延伸出去的思考前端工程化里的边界意识代理配置看起来只是一个不起眼的开发辅助功能但把它放到整个前端工程体系里看背后反映的是边界意识。前端的代码运行在浏览器里天然受同源策略约束所有跨域方案本质都是在“绕”这个约束只是绕的位置不同开发环境在 Node 层绕生产环境在网关层绕。我见过不少项目代理配好了但团队里每个人都在自己的代码里写死http://localhost:8080或者把后端地址塞进请求工具函数里导致代理规则形同虚设。这不是配置的问题而是工程规范的问题——代理不是写给某一个人用的它应该成为项目基础设施的一部分统一封装、统一维护每个人开箱即用。如果你的团队项目里还没有统一的请求封装我建议在配代理的同时把请求工具函数也一起规范化baseURL 统一用环境变量驱动超时时间、拦截器、错误提示全部集中管理。代理解决的是“请求怎么出去”请求封装解决的是“代码怎么写清晰”两者配合才是完整的解决方案。另外一个容易忽略的点是代理规则也会影响前端静态资源的访问。比如你配了/static前缀的代理但前端本身有静态资源目录就会和真实资源路径冲突。所以代理前缀的选择要避开脚手架默认的静态资源路径比如public/目录下的资源通常不走代理中间件。我在新项目里通常用/api作为所有后端请求的统一前缀因为public目录和大多数业务的路径都不冲突且一眼就能识别哪些请求是需要代理的后端接口。10. 一些值得保留的实操习惯和最终建议最后分享几条我在这类配置问题上沉淀的个人习惯不保证绝对正确但长期用下来省了很多排查时间。第一开发服务器重启频率要高。修改package.json的proxy字段后必须重启开发服务器修改setupProxy.js后部分情况下脚手架会自动重启但如果不放心手动重启一次最稳妥。我在团队里遇到过有人改了配置但没重启白白排查了半小时。第二代理的报错不一定显示在浏览器控制台。很多代理错误是静默的浏览器只看到 404 或 pending真实原因在后端日志里。排查时一定要两端同时打开日志前端控制台和后端控制台对照着看。第三不要把代理配置写在组件里或者工具函数里。它应该独立存在路径清晰、注释完善属于项目根级别的基础设置任何开发人员拉下代码后不需要额外沟通就能理解。第四提交代码时注意不要把个人的后端地址带上。我见过团队成员本地用localhost:9090提交代码时把setupProxy.js里的 target 也一起提交了其他人拉下来代理全部指向错误端口。解决方式是用环境变量隔离每人本地.env.development文件不同而setupProxy.js提交的是统一模板。如果你现在正被本地跨域问题困扰先不要急着给后端提工单按照这篇文章的顺序先确认后端端口健康再确认请求是相对路径最后用setupProxy.js完成规则匹配。大概率二十分钟内就能跑通。代理配置本身不难难的是理解它到底在哪一层解决问题以及遇到问题时的排查路径是否能覆盖所有可能性。把这些搞清楚了React 脚手架的代理对你就不再是一个黑盒。
阅读完成 · 觉得有帮助?
咨询建站