先回忆一个场景:vue 项目 npm run dev 跑起来了,页面正常渲染,一调后端接口,控制台立刻飘红——Access to XMLHttpRequest at 'http://localhost:8080/...' has been blocked by CORS policy。后端同事把接口地址发给你,你拿 Postman 调得好好的,可浏览器就是不让过。
这种本地开发跨域问题,在 vite 项目里最标准的解法就是 proxy:打开 vite.config.js,配置 server.proxy,让 vite 自带开发服务器帮你转发请求。前端不用后端开 CORS、不用装浏览器插件、不用改任何业务代码,纯配置就能把跨域按下去。这篇文章就以 vue + vite 为背景,把跨域原理、proxy 配置的细节、实际踩过的坑一次聊透,希望正在被跨域折磨的同学看完能少走弯路。
1. 先搞清楚为什么会跨域:同源策略和代理的本质
1.1 浏览器同源策略到底拦的是什么
同源策略是浏览器的一个安全机制。所谓同源,指的是协议、域名、端口三者完全一致。http://localhost:5173 和 http://localhost:8080,端口不一样,算不同源;http://127.0.0.1:5173 和 http://localhost:5173,虽然看起来差不多,但域名写法不一样,也算不同源。这个标准非常严格,稍微差一个字符都不行。
本地开发普遍就是这个局面:vue 项目跑在 5173 端口,后端接口跑在 8080 端口,二者不是同一个 origin。浏览器发请求的时候,发现目标地址跨域,于是把服务器返回的响应拦下来,不给前端 JS 读取。很多人容易有个误区,觉得浏览器是把请求拦住了。实际上请求已经发出去了,后端也正常处理了,数据也返回了,但浏览器在“接收”这一步拦截了,前端拿不到 response,所以表现成报错。
这个设计本来是为了保护用户,防止恶意网站读取你在其他网站的数据。但对联调阶段的开发者来说,它就是一个纯纯的阻碍。有个生活化的类比:同源策略就像小区门禁,快递员想进你家送快递,门禁系统发现这人不是本小区的,东西到门口就被拦下了。快递其实已经送到小区门口了,只是进不了门。
1.2 vite proxy 为什么能“绕过”跨域
跨域是浏览器的规矩,不是 HTTP 协议本身的规矩。vite dev server 跑在 Node 环境里,Node 发 HTTP 请求根本不看同源策略。于是 vite 就把这个特性利用起来:本地开发时,浏览器请求的地址写成 vite dev server 自己的地址,再由 dev server 转发到真正的后端接口。整个链路是这样的:
浏览器发出请求 http://localhost:5173/api/user/list → vite dev server 收到请求 → dev server 把请求转发到 http://localhost:8080/api/user/list → 后端返回数据 → dev server 再把数据返回给浏览器。
浏览器全程只和 localhost:5173 通信,这是同源请求,浏览器不拦;真正的跨域请求由 dev server 在服务端完成,服务端没有跨域概念,天然能通。这个思路和 Nginx 反向代理是一模一样的,vite proxy 本质就是一个内置在开发服务器里的轻量反向代理。
理解这一点很重要,它能帮你定位很多奇怪的问题。比如有同学问“为什么代理配好了还是跨域”,多半是核心逻辑出了偏差:前端代码里请求地址写死了后端的完整地址,比如 axios 的 baseURL 直接写 http://localhost:8080,这样浏览器就会绕过 vite dev server 直接请求后端,proxy 配得再完整也接管不到这个请求。
2. 手把手配置 vite.config.js:proxy 三个核心参数
2.1 最小可用配置长什么样
在 vite 项目的根目录找到 vite.config.js(也可能是 vite.config.ts),配置 server 对象下的 proxy。一个最小可用的配置是这样的:
// vite.config.js import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' export default defineConfig({ plugins: [vue()], server: { proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true, } } } })配置完之后,你在前端请求 /api/user/list,vite 就会把它转发到 http://localhost:8080/api/user/list。浏览器看到的请求地址始终是 http://localhost:5173/api/user/list,同源,不会跨域。前端请求代码基本不用动,唯一要注意的是尽量用相对路径:
// 推荐:相对路径,走 vite 代理 axios.get('/api/user/list') // 不推荐:写死绝对地址,代理直接失效 axios.get('http://localhost:8080/api/user/list')这里有一个很多人没注意的细节:键名 '/api' 是匹配规则,表示所有以 /api 开头的请求路径都走这个代理。target 是真正的后端地址,端口、协议都不能写错,http/https 写反了也会出问题。
2.2 target、changeOrigin、rewrite 分别有什么用
target 不用多讲,就是目标服务器,需要提醒的是端口别写错、协议别写错。我见过有人排查半天,最后发现是 target 把 8080 写成了 80,或者 https 写成了 http。
changeOrigin 这个参数是很多后端联调半天不通的根源。它控制的是请求转发时,要不要把请求头里的 Host 字段改成 target 的域名。HTTP 请求里有一个 Host 头,表示你请求的目标主机。默认情况下 vite 转发的请求会保留浏览器发来的 Host,也就是 localhost:5173。部分后端接口会校验 Host 或 Referer 头,发现来源不是它认识的域名,直接拒绝。
把 changeOrigin 设为 true 之后,转发的请求头里 Host 就被替换成 target 的域名和端口了,后端看起来就像是你直接请求它一样。用类比来说,changeOrigin 就像是给快递换了一身印着“小区内部人员”的工作服,门禁一看是自己人,放行。
rewrite 是路径重写函数,它决定转发时 URL 怎么变化。默认不写,就是原样转发——前端请求 /api/user/list,转发给 target 的还是 /api/user/list。这个默认行为让很多人困惑,所以下一节单独讲。
2.3 接口带不带前缀,配置差在哪
这是我在带新人时讲得最多的一点。rewrite 到底要不要写,取决于后端接口到底有没有 /api 这个前缀。先说结论:后端接口路径和前端请求路径能对上,就不用 rewrite;对不上,才需要 rewrite 调整。
场景 A:后端本身就是 /api 开头。后端接口是 http://localhost:8080/api/user/list,前端代码里直接请求 /api/user/list。这时候 target 指向 http://localhost:8080,转发时 /api/user/list 原样带过去,正好和后端匹配,不用写 rewrite。
场景 B:后端没有 /api 前缀。后端实际接口是 http://localhost:8080/user/list,但前端为了统一走代理,请求地址写了 /api/user/list。此时如果还按场景 A 配置,vite 转发过去的是 http://localhost:8080/api/user/list,后端根本没有这个路径,返回 404。必须加一条 rewrite,把开头的 /api 去掉:
proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true, rewrite: path => path.replace(/^\/api/, ''), } }很多同学看见 404 第一反应是“后端接口挂了”,实际上后端接口好端端的,是你多加了一个 /api。判断方法也很简单:把 target 和后端路径拼起来,看最终的 URL 是否等于后端真实接口地址。拼出来不对,就调整 rewrite。这里有个建议:前端请求路径和后端路径的对应关系,最好在配置注释里写清楚,避免后面接手的同事一脸懵。
提示:rewrite 里的正则 /^/api/ 只匹配路径开头的 /api,不会误伤路径中间出现的 api 字符串。有人图省事写成 path.replace('/api', ''),不带正则,如果路径里恰好有多个 api 相关的片段,结果会非常诡异,强烈建议用正则写法。
3. 进阶配置:HTTPS、多环境和多代理规则
3.1 后端是 https 或自签名证书时怎么办
本地开发时后端有时候是测试服务器,域名是 https 的,比如 https://test-api.example.com。如果证书正常,直接配 target 就行,浏览器和 vite 都会正常处理。但如果后端是 IP + https,或者用了自签名证书,vite 转发时通常会报证书校验错误,表现为请求一直失败或者直接看到 SSL 相关报错。
此时在代理配置里加 secure: false,让转发过程跳过 TLS 证书校验。注意,这个参数的含义是“不对目标服务器做证书校验”,不会影响浏览器本身的证书校验,所以不用担心安全问题——反正是开发阶段连接测试服务器用。配置写法:
proxy: { '/api': { target: 'https://10.0.0.18:8443', changeOrigin: true, secure: false, } }这里有个容易忽略的点:如果 target 是 https 协议,但忘了写 secure: false,报的错可能不是“证书无效”,而是代理直接连不上、连接被重置。因为有的自签名证书在握手阶段就被拒绝了,表现非常像网络不通。遇到 https 目标地址请求异常,先想到 secure 参数。
3.2 用环境变量切换 target,一套配置跑不同环境
项目开发到中后期,往往会区分本地、测试、预发环境,后端地址各不相同。如果每换一个环境都手动改 vite.config.js,容易改错还要重启 dev server。我习惯把 target 做成环境变量,在 .env.development 里维护。
# .env.development VITE_API_TARGET=http://localhost:8080然后在 vite.config.js 里用 loadEnv 读取:
import { defineConfig, loadEnv } from 'vite' import vue from '@vitejs/plugin-vue' export default defineConfig(({ mode }) => { const env = loadEnv(mode, process.cwd(), '') return { plugins: [vue()], server: { proxy: { '/api': { target: env.VITE_API_TARGET, changeOrigin: true, } } } } })这样想切环境,只需要改 .env 文件里的 VITE_API_TARGET,不用再碰代理配置。团队协作时,每个人本地的后端地址可能也不同,可以各自维护一份 .env.local,不影响别人。有个小经验:变量命名规范很重要。Vite 会默认加载 VITE_ 开头的变量到前端代码,代理配置里读取的时候同样可以用这个前缀,统一管理起来更清晰。
3.3 正则匹配、多代理实例和 WebSocket
除了字符串前缀匹配,proxy 的键还支持正则。比如只想代理 /api 和 /auth 两个前缀,可以直接写正则:
proxy: { '^/(api|auth)': { target: 'http://localhost:8080', changeOrigin: true, } }如果前端要代理多个不同的后端服务,配置多个键即可。比如 /api 代理到 Java 服务,/upload 代理到文件服务,互不干扰。匹配规则是按顺序尝试,命中哪个就交给哪个 target,所以具体键的顺序一般不影响结果,但建议把更具体的路径放前面,避免歧义。
WebSocket 场景也需要单独提一下。如果项目里有在线聊天、实时推送这类 ws 连接,且后端是 ws:// 协议,代理配置里要加 ws: true,否则 WebSocket 握手可能失败。示例:
proxy: { '/socket': { target: 'ws://localhost:9000', ws: true, changeOrigin: true, } }最后还有一个 bypass 函数,它是 proxy 配置里的高级选项,可以针对单个请求动态跳过代理。比如某个路径不想走代理、直接返回一段响应,可以这么写:
proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true, bypass(req) { if (req.url.includes('/api/mock')) { return '/mock-data.json' } } } }这个功能不常用,但遇到特殊需求时非常省事。不过它属于 http-proxy 的进阶能力,新手阶段用不到可以跳过,等真有场景了再回来查。
4. 常见问题排查:改了配置没效果、404、502 怎么定位
4.1 判断代理有没有生效,先看网络面板
遇到代理相关的问题,我第一个动作永远是打开浏览器开发者工具的 Network 面板,重新触发一次请求,看请求的 URL 和状态。这个方法能解决绝大多数排查困惑。
如果网络面板里显示的 URL 是 http://localhost:5173/api/user/list,说明请求确实被 vite 接管了,代理配置是生效的,问题很可能出在 target 或 rewrite。如果显示的是 http://localhost:8080/user/list,说明请求压根没走代理,多半是前端代码里写了绝对地址,或者用了某种方式绕过了相对路径。
判断代理是否生效还有一个更“暴力”的办法:把 target 故意配成一个不存在的端口,比如 9999,再刷新页面。如果报 ECONNREFUSED 或者类似的连接失败错误,说明代理确实在帮你转发,只是目标不对。如果请求行为完全没有变化,说明请求根本没进代理,问题在前端请求地址上。
4.2 报错速查:404、502、503、ECONNREFUSED 分别是什么意思
整理一个速查表,方便对照排查:
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 请求走了代理,返回 404 | rewrite 把路径写错了,或多加了前缀 | 把 target 和转发路径拼起来,对比后端真实接口地址 |
| 返回 502 Bad Gateway | 代理和目标服务器之间连接失败 | 确认 target 地址端口是否正确,服务是否启动 |
| 返回 503 | 目标服务不可用或拒绝了请求 | 直接请求 target 地址,看能否访问 |
| ECONNREFUSED | 目标端口没有服务在监听 | 检查后端服务是否启动、端口是否正确 |
| SSL / 证书相关报错 | 目标是 https 且证书不被信任 | 加 secure: false |
| 后端说 Host 不对 | changeOrigin 没设 true | 显式配置 changeOrigin: true |
| 接口始终报跨域 | 前端写了绝对地址,代理没接管 | 改成相对路径,带 /api 前缀 |
想特别强调一个排查思路:代理转发这件事可以分为“浏览器到 vite”和“vite 到后端”两段。第一段出问题,表现是网络面板里根本没有走到代理的请求;第二段出问题,表现是 vite 正常收到了请求,但 target 那边的响应报错。先分清是前一段还是后一段,再针对性查,效率会高很多。
4.3 rewrite 写错、changeOrigin 漏配等高频坑
rewrite 写错是我见过最多的坑。除了前面说的不用正则的问题,还有人会把 rewrite 写成 path => path.replace(/^/api//, ''),多了一个结尾斜杠,结果会导致路径里的分隔符被吃掉了。比如 /api/user/list 变成了 userlist,后端自然 404。正确的做法是明确自己要去掉的是什么:如果前端请求是 /api/user/list,想去掉 /api,就用 /^/api/;如果想去掉 /api/,就用 /^/api//。
还有一个高频坑:前端请求路径里少了斜杠。比如 /api/user/list 和 /apiuser/list,看起来只是笔误,但后者根本匹配不到 '/api' 前缀。匹配规则是前缀匹配,不是模糊匹配,所以路径要严格控制,不能指望代理帮你自动修正。
changeOrigin 漏配也经常遇到。如果后端不校验 Host,漏配也能跑通,所以一直没人注意。但一旦换了个校验严格的后端,就会出现“接口偶尔通、换了环境就不通”的诡异问题。我现在的习惯是每套代理规则都无脑加 changeOrigin: true,即使暂时用不到也写上,后面改动时有据可查。
修改 vite.config.js 不生效的问题是另一个高频困扰。vite.config.js 属于配置文件,很多情况下 dev server 不会完全热更新配置内容。如果改了配置发现没反应,别纠结,直接 Ctrl+C 重启 npm run dev,重启后一定生效。这个小动作能避免很多为“玄学 bug”浪费的时间。
4.4 代理配好了接口还是报跨域,怎么回事
这是我在论坛里看到频率很高的问题。代理已经配好,target 也正确,网络面板显示请求走的是 localhost:5173,但控制台还是报跨域错误。这种情况我遇到过几次,原因基本就两类。
一类是前端代码里用到了自定义请求头,比如在 Authorization 之外又加了 X-Token 之类的头。请求带自定义头,浏览器在正式请求前会发一个 OPTIONS 预检。走代理后这个预检请求同样由 vite 转发,正常来说 vite 会把响应返回,但如果后端对 OPTIONS 请求处理不友好,或者代理把 OPTIONS 请求拦截了,浏览器就会认为预检失败,表现依然是跨域。排查时可以专门看 Network 面板里有没有 OPTIONS 请求、状态码是什么。
另一类是后端配置了 CORS 相关的响应头,但和代理转发后的响应冲突了。比如后端把 Access-Control-Allow-Origin 写死成了某个域名,而浏览器请求的 origin 是 localhost:5173,哪怕走了代理,浏览器同样会拦。这种情况严格来说不是代理的问题,而是后端 CORS 配置问题,协调后端处理即可。
5. 上线之后还会跨域吗:开发代理和生产的边界
5.1 vite proxy 只在开发环境生效
vite proxy 是 dev server 提供的功能,npm run build 打包出来的是纯静态文件,本身没有 server,proxy 自然不存在。所以上线之后,前端部署在 Nginx 或者对象存储上,浏览器请求依然会有跨域问题,而且比开发环境更麻烦,因为这时候没有 vite 帮你挡一挡。
如果项目是前后端分离并且部署在不同域名下,生产环境的跨域必须从架构层面解决。最推荐的方案是用 Nginx 做反向代理,把 /api 开头的请求转发到后端服务,让浏览器始终只访问前端域名。这个思路和 vite proxy 完全一致,相当于把开发代理搬到了服务器上。一个简单的 Nginx 配置片段:
location /api/ { proxy_pass http://localhost:8080/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; }这里要注意 proxy_pass 结尾的斜杠:带斜杠表示会把 /api/ 前缀去掉再转发,不带斜杠则保留 /api。这个细节和 vite 里 rewrite 的作用本质相同,搞反了同样会 404。
5.2 生产环境的跨域方案怎么选
还有一条路是后端开 CORS,在响应头里加 Access-Control-Allow-Origin。这条路开发省事,但要注意:如果涉及 Cookie 跨域,后端需要配套 Access-Control-Allow-Credentials 等响应头;如果 Access-Control-Allow-Origin 用了通配符 *,又没法带 Cookie。不少团队一开始为了方便全加星号,后面要加会话态时才发现处处是坑,整改起来很麻烦。
所以我的建议是这样的优先级:能走同源就同源,用 Nginx 反代把前后端收敛到同一个域名;实在不能同源,就规范地做 CORS 白名单;vite proxy 始终定位为开发工具,它的价值是让开发阶段不被跨域绊住,而不是替代生产方案。我之前带的一个项目就是这样,开发环境统一走 /api 前缀,上线之后换成 Nginx 反代,前端代码几乎零改动,整个迁移非常顺滑。
另外有个小实践可以分享:前端代码里把请求 baseURL 做成可配置的,开发环境指向 /api,生产环境指向自己的域名,不要写死。这样开发用 vite proxy,上线切 Nginx,代码几乎不用动,联调和部署都省心。
配置 vite proxy 这件事,门槛其实不高,但因为它介于“前端配置”和“服务端代理”之间,很多人出了问题容易一头扎进代码里翻。我个人的实操体会是:先按“浏览器到 vite、vite 到后端”两段来拆,再用网络面板确认请求实际走到了哪,绝大多数问题都能快速定位。最后再分享一个小习惯:凡是新增代理规则,我都在配置里写一行注释,标明这个规则对应的后端服务和负责人。等到接口迁移、后端换地址的时候,你会感谢这行注释帮你省下的沟通时间。