Vite proxy解决本地跨域:核心配置、字段详解与生产替代方案
遇到过吧本地Vue项目跑在5173端口后端服务跑在8080端口前端一调接口浏览器的Network里明明有请求控制台却一直报CORS错误。页面加载不出来后端日志里也没看到请求进来。很多人第一反应是让后端加CrossOrigin但加了之后又牵出Cookie、预检请求一堆破事。我后来在vite.config.js里配置server.proxy代理几分钟就把本地跨域问题解决干净了。这篇文章把Vite proxy从原理到实战拆开讲透包括核心字段解读、多环境管理、排查思路以及上线后的替代方案适合正在被本地跨域折腾的Vue Vite开发者。1. 跨域问题的根源请求发出去了响应回不来先说一个反直觉的事实当浏览器拦截跨域请求时请求大概率已经发出去了后端也收到了只是浏览器在拿到响应之后发现响应头里没有允许跨域的标识于是直接把响应挡在门外并把错误抛给前端开发者。1.1 同源策略浏览器充当了严格的快递员同源策略是浏览器最基础的安全机制之一。所谓“同源”要求协议、域名、端口三者完全一致。以http://localhost:5173为例只要有一项不同比如端口换成8080浏览器就认为这是跨域。这个策略本身是保护用户的没有它任意网站都能随便读取你在其他平台的状态和接口数据。但开发阶段前端和后端通常会分两个进程跑在本地不同端口这天然就跨域了。前端在5173后端在8080浏览器直接访问后端接口时就会触发同源策略的拦截。很多人会问既然请求已经发出去了后端也收到了为什么控制台报的是跨域错误而不是业务错误原因很简单响应虽然到达了浏览器但浏览器拒绝把响应内容交给JS代码。从JS的角度看这个请求就是“失败”了。1.2 后端开CORS为什么只是“能用”而没根治问题解决跨域最直接的做法是后端配置CORS中间件比如Spring Boot里加CrossOrigin或者在Nginx里追加Access-Control-Allow-Origin头。这在功能上确实能通但本地开发时我不太推荐把这个当成唯一方案。原因有几点。第一CORS配置会写进业务代码或部署配置里开发、测试、生产环境往往需要不同的允许域名一旦写死环境切换时容易漏改。第二一旦接口需要带CookieAccess-Control-Allow-Origin不能设置成*必须指定具体域名还要额外处理Access-Control-Allow-Credentials这个联动关系很容易踩坑。第三如果前端本地起了多个端口或者后端服务有多个CORS配置就要维护成列表随着团队扩张会越来越乱。更麻烦的是每次跨域请求还可能会触发OPTIONS预检。预检请求处理不好后端会返回405或者缺少必要的响应头调试起来又绕一道弯。所以我的建议是本地开发阶段用Vite的proxy代理来绕开跨域后端不需要关心谁来访问生产环境再用网关或Nginx统一处理。1.3 Vite Dev Server代理的工作原理Vite的proxy本质上是利用了Node.js服务器的转发能力。本地开发时前端代码跑在Vite Dev Server上当浏览器请求/api/xxx时Vite并不直接把这个请求当作静态资源处理而是根据vite.config.js里的server.proxy配置把请求转发到目标后端地址。关键点在于这个转发过程发生在服务器端不是浏览器端。浏览器始终只和同源的Vite Dev Server通信所以不会触发浏览器的同源策略。后端收到的请求是由Vite服务器发起的也不存在浏览器跨域拦截问题。响应原路返回浏览器看起来就像在同源环境下请求一样控制了跨域错误。这个过程很像外卖骑手你下单给平台Vite Dev Server平台把订单转给餐厅后端餐厅做好饭交给平台平台再送到你手上。你从头到尾只跟平台打交道不会因为餐厅和你不在一个地址而投诉。2. proxy核心配置vite.config.js逐行拆解Vite的代理配置并不复杂核心就是server.proxy对象。但很多项目里配置往往是从网上复制粘贴的能用但不知道字段含义出了问题也不知道从哪改起。这里我把最常用的写法拆开讲。2.1 一个可以直接跑的最小配置在项目根目录找到vite.config.js没有就自己建一个。下面是一份可以直接使用的最小配置import { defineConfig, loadEnv } from vite import vue from vitejs/plugin-vue export default defineConfig({ plugins: [vue()], server: { host: 0.0.0.0, port: 5173, proxy: { /api: { target: http://localhost:8080, changeOrigin: true, rewrite: (path) path.replace(/^\/api/, ), }, }, }, })这段配置的含义是当本地开发服务器收到以/api开头的请求时把请求转发到http://localhost:8080并把路径前缀/api去掉。比如前端的请求地址是/api/user/list实际转给后端的地址就是http://localhost:8080/user/list。如果后端接口本身就带有/api前缀那rewrite这行就不用写直接透传。2.2 键名匹配规则路径前缀不是随便写的proxy对象里的键名比如/api是用来匹配请求路径的。这里的匹配规则不是简单的“路径相等”而是“路径前缀匹配”。也就是说只要请求路径以/api开头就会被这个代理规则捕获。这里要特别注意如果同时配置了/api和/api/v1Vite会选择更长的匹配规则。所以多个服务或前缀共存时把更具体的路径放在前面或者依赖Vite的精确匹配规则不要自己堆叠容易混淆的前缀。键名最好和前端请求的统一前缀保持一致。实际项目中我习惯让axios的baseURL统一设置成/api这样所有接口请求都会走代理后端也能通过路径区分业务模块。如果键名和请求前缀对不上代理就不会生效具体排查方法后面专门讲。2.3 rewrite和target路径怎么改、地址往哪发target是代理的目标地址也就是后端服务的实际地址。可以是IP、域名也可以是http://localhost:8080甚至还支持https://只是https场景要注意证书问题。rewrite接收一个函数参数是当前请求路径返回值是改写后的新路径。这个函数里最常用的是正则替换rewrite: (path) path.replace(/^\/api/, )含义很简单如果路径以/api开头就把它删掉。为什么经常要删因为前端习惯给所有请求加/api前缀但后端Controller里定义的路径可能并没有这个前缀这时代理时去掉就刚好匹配。如果后端接口路径同样以/api开头那就不写rewrite。还有一种常见场景前端路径是/api/v1/user后端只接受/user可以这样写rewrite: (path) path.replace(/^\/api\/v1/, )2.4 changeOrigin / secure / ws易被忽略的选项changeOrigin是我建议一定要开的一个选项。它决定转发请求时是否把请求头里的Host字段改成目标地址的域名。后端如果做了域名白名单校验或者根据Host头生成回调地址不开changeOrigin就可能拿到localhost:5173导致校验失败。更严重的是在一些鉴权场景里后端生成的临时链接或者Cookie域名会跟着错。所以本地调试时我一般都会写成true避免这类隐秘问题。secure选项适用于目标是https地址的场景。如果目标服务器的SSL证书是自签名或没有正确配置Vite转发时会因为证书校验失败而报错。这时候把secure设置成false可以跳过证书校验。注意这仅用于本地开发生产环境不能这么干。ws选项用于WebSocket代理。如果接口里要建立ws://或wss://连接比如实时通知就必须在对应的代理规则中加上ws: true。否则WebSocket握手请求会被当成普通HTTP请求连接建立失败。下面是一个考虑到这些细节的完整示例proxy: { /api: { target: http://localhost:8080, changeOrigin: true, rewrite: (path) path.replace(/^\/api/, ), }, /socket: { target: ws://localhost:8082, ws: true, changeOrigin: true, }, }3. 多环境多后端的代理扩展实践项目一多或者环境一多代理配置就不仅仅是写死一个target的事。开发、测试、联调、本地mock每种场景都要对应不同的后端地址。如果每次切换环境都要手动改vite.config.js既麻烦又容易忘了改回来。3.1 用loadEnv区分开发和生产环境Vite提供了loadEnv方法可以在vite.config.js里读取当前模式对应的环境变量。Vite启动时默认会加载.env、.env.development等文件。我的做法是在项目根目录创建.env.development和.env.test等文件分别写入VITE_PROXY_TARGEThttp://localhost:8080VITE_PROXY_TARGEThttp://test-api.example.com然后改造vite.config.jsimport { defineConfig, loadEnv } from vite import vue from vitejs/plugin-vue export default defineConfig(({ mode }) { const env loadEnv(mode, process.cwd(), ) return { plugins: [vue()], server: { host: 0.0.0.0, port: 5173, proxy: { /api: { target: env.VITE_PROXY_TARGET || http://localhost:8080, changeOrigin: true, rewrite: (path) path.replace(/^\/api/, ), }, }, }, } })这里loadEnv的第三个参数传了空字符串表示把环境变量文件里的变量全部读取出来而不是只读VITE_前缀开头的。这样可以避免一部分配置变量在客户端代码里暴露又能让vite.config.js拿到。启动开发服务器时如果不指定mode默认是development会对应读取.env.development。当你执行vite build --mode test时Vite会切换成test模式读取.env.test。这个机制不仅可以用在代理配置上还可以同步控制打包时是否启用mock、是否压缩日志等。3.2 多个服务前缀的配置拆分大型项目往往不是单一后端可能是用户服务、订单服务、文件服务各自独立部署。这时可以在proxy对象里挂多个键名分别转发到不同地址proxy: { /api/user: { target: http://localhost:8081, changeOrigin: true, rewrite: (path) path.replace(/^\/api\/user/, /user), }, /api/order: { target: http://localhost:8082, changeOrigin: true, rewrite: (path) path.replace(/^\/api\/order/, /order), }, /upload: { target: http://localhost:8083, changeOrigin: true, }, }上面这种写法让请求路径自带服务标识每个标识对应独立的目标服务。后端可以根据改写后的路径直接路由到具体Controller不需要额外网关配置。不过要注意键名匹配是前缀匹配。如果配了/api/user同时也没有更长的冲突路径那么/api/user/login和/api/user/info都会走到同一个target。如果想再细分可以继续增加更长的键名比如/api/user/vipVite会优先匹配更长的路径。3.3 从webpack devServer迁移的速查对照很多老项目还在用webpackvue.config.js里通过devServer.proxy配置代理。迁到Vite后字段几乎一一对应只是入口和文件格式变了。配置项webpack devServerVite server.proxy入口配置devServer.proxyserver.proxy目标地址targettarget路径改写pathRewriterewrite修改Host头changeOrigin: truechangeOrigin: trueWebSocketws: truews: true跳过证书校验secure: falsesecure: false最大差别在于pathRewrite和rewrite的写法。webpack里pathRewrite是一个对象比如pathRewrite: { ^/api: }而Vite里的rewrite是一个函数rewrite: (path) path.replace(/^\/api/, )迁移时把那行配置换成函数写法即可其他选项基本可以直接沿用。还有一点Vite配置文件默认是ESM格式用export default而webpack配置是CommonJS的module.exports。从旧项目复制配置过来时这一行一定要改。4. 代理不生效的排查链路按顺序做一遍代理配置看起来简单但实际开发中“不生效”的情况非常常见。我见过不少同事卡在代理问题上半小时起步其实大部分原因就那么几类。与其打开控制台乱猜不如按下面这个顺序排查一遍。4.1 改完配置没重启等于白改vite.config.js是启动时加载的不像项目源码那样支持热更新。新增或修改了server.proxy配置后必须手动重启Vite Dev Server配置才会重新生效。很多人改完配置后发现代理还是失败第一反应是去检查代码折腾半天最后发现只是没重启。所以我的习惯是只要动过vite.config.js第一件事就是把终端里的Vite进程停掉重新跑一遍。如果是Vite 3以上版本有些配置支持server.watch但proxy相关改动依然要重启才算数。重启后注意看终端日志Vite会输出“Local”“Network”地址。如果有代理配置错误有些版本还会在控制台直接把错误打印出来。4.2 请求路径与proxy键名对不上这是第二常见的问题。比如前端代码里axios请求路径写的是/app/user/list但proxy对象的键名是/api那么请求根本不会匹配到代理规则Vite会把它当成静态资源在本地找结果当然是404。要判断是哪一种情况打开浏览器的Network面板看请求地址。如果请求URL是http://localhost:5173/app/user/list且状态码404那大概率是没有匹配到代理键名。如果请求URL是http://localhost:5173/api/user/list但状态码200且响应结构不对那可能是代理生效了只是rewrite改写后路径和后端路由对不上。统一约定很重要。我一般会让所有前端请求都使用同一个前缀比如/api。这样只需要在proxy里配置一个键名后端也能通过这一个前缀识别前端流量。4.3 用curl和终端日志验证代理是否生效当我们在浏览器里访问/api接口时如果代理已经生效Vite会打印一条请求日志包含转发到的目标地址。不同版本格式可能不一样但一般能看到类似http://localhost:8080/user/list的信息。如果浏览器控制台不方便看可以直接在终端里用curl验证curl http://localhost:5173/api/user/list如果返回的是后端的JSON响应说明代理链路通了。如果返回的是Vite的404页面或者HTML内容说明请求没有进入代理规则需要检查键名匹配。还有一种情况请求能打通但状态码报500或后端业务错误。这通常不是跨域问题而是后端接口本身报错了。用curl直接请求后端地址对比一下就能快速判断责任在谁。4.4 配置文件报错的处理Vite在启动时会加载vite.config.js如果配置文件本身有语法错误会直接启动失败。常见的报错包括Failed to load config from ... vite.config.js后面跟着行列号。我遇到过的原因主要有下面几种配置文件里写了中文标点或多余逗号。把CommonJS的module.exports和ESM的export default混用了。对象结构少了一个括号。代码里用了某个Node环境下没有的变量比如直接读取了浏览器的window对象。如果是ESM模块识别问题可以把配置文件从vite.config.js改成vite.config.mjs或者确认package.json里没有设置type: commonjs。Vite官方推荐直接在配置里使用ESM语法这也是为什么所有示例里都有export default。如果报错行列号指向某个具体位置可以先看那一段的括号是否闭合。很多配置对象嵌套深了少一个括号确实不好找我通常会把这段代码单独格式化一下再重启。5. 生产环境没有Vite Dev Server代理怎么办在本地开发时Vite的proxy帮助我们绕过了跨域。但把项目执行vite build之后产出的是纯静态文件没有Vite Dev Server在背后转发了。这时候如果前端代码还在请求相对路径/api部署到服务器上就会直接请求部署域名的/api大概率也是404。5.1 为什么build之后的项目不能再依赖vite.config.jsvite build构建出来的静态资源通常在Nginx、CDN或云服务器上托管整体是一个静态服务器环境并不运行Node服务。server.proxy配置只属于开发服务器构建过程中根本不会把这段代理逻辑打包进去。所以生产环境的跨域和接口转发要靠部署层的反向代理来解决。最常见的就是Nginx。前端部署到/usr/share/nginx/htmlNginx监听80或443当收到/api请求时用proxy_pass把请求转发给后端服务。浏览器视角里请求还是发给当前域名不存在跨域。5.2 Nginx的proxy_pass和Vite的rewrite如何对应来看一个典型的Nginx配置location /api/ { proxy_pass http://localhost:8080/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; }这里proxy_pass http://localhost:8080/;末尾带了一个/意味着会把/api/前缀去掉。比如请求/api/user/list转发到后端就是http://localhost:8080/user/list。这个效果和Vite里rewrite: (path) path.replace(/^\/api/, )是一模一样的。如果后端接口本身带/api前缀那Nginx的proxy_pass末尾就不要加/直接写成location /api/ { proxy_pass http://localhost:8080; }这样请求路径会原样透传/api/user/list对应http://localhost:8080/api/user/list和Vite里不写rewrite的行为一致。还有WebSocket场景Nginx需要额外配置升级请求头才能支持ws协议location /socket/ { proxy_pass http://localhost:8082/; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade; }Nginx版本的等价配置和Vite的ws: true实现的目标是同一个让WebSocket握手成功。5.3 前端统一管理接口前缀的约定为了让开发代理和生产的Nginx代理无缝衔接前端代码里最好把接口前缀固定成一个约定值不要一个请求写/api另一个请求又写/app。我习惯在axios实例的baseURL里统一配置const request axios.create({ baseURL: /api, timeout: 10000, })这样本地开发时Vite会按/api前缀代理到后端生产部署时Nginx也按/api前缀转发前后端不需要改任何一行业务代码只需要调整部署层的转发规则即可。这里还要注意跨环境变量的问题。如果你在.env.production里配了VITE_API_BASE_URL让axios使用绝对地址那么生产环境的接口请求就会直接指向某个具体域名此时就不需要Nginx再转发/api了。两种方案各有优劣使用相对路径/api配合反向代理的方式能最大化保持部署灵活性我目前更推荐这种相对路径方案。从开发期的Vite proxy到生产期的Nginx反向代理本质上都是“反向代理”思路只是承载者不一样。理解了Vite proxy的字段和作用原理再看Nginx配置就不会觉得陌生了。最后再分享一个我自己的使用习惯vite.config.js里的target地址尽量都通过环境变量读取不要把具体IP、域名写死在配置文件里。这样团队成员从本地联调切到测试环境时只需要改对应的.env文件不需要动代码和配置逻辑。项目多了以后这套约定会帮你省下大量踩坑时间。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →