Vite运行时动态切换代理:告别改配置重启开发服务器

发布时间:2026/10/9 13:06:48
Vite运行时动态切换代理:告别改配置重启开发服务器
做前端开发最烦的事情之一就是改代理。一会儿要连本地后端一会儿要连测试环境一会儿又要连同事电脑上临时开的服务。以前的标准操作是改vite.config.ts里的target保存重启 dev server再刷新页面。一天下来光重启开发服务器就好几次项目大一点的时候每次等冷启动的几十秒都像坐牢。后来我试了给 Vite 项目做运行时切换代理把目标后端的切换放到请求处理阶段不再依赖配置文件重启整套联调流程顺畅了非常多。这篇文章就把我在实际项目中用过的几种方案、切换原理、踩过的坑和最终沉淀下来的配置一并分享出来。如果你也在为多环境联调头疼这篇文章值得看完。1. 为什么需要在运行时切换代理1.1 前端开发切换环境之痛很多团队的前后端是并行开发的后端代码在不同人手上环境也有好几套本地启动的 Java 服务、测试服务器、Mock 服务、还有同事电脑上临时运行的 Node 服务。前端代码为了保证调试效率一般把这些接口统一走/api前缀再由 Vite 开发服务器转发到真正的后端地址。问题就出在这个真正后端地址上。如果只有一套写死的代理配置每次切换环境都要打开vite.config.ts找到server.proxy.target改地址保存然后重启 Vite最后浏览器刷新。这个过程不仅慢还非常容易出事。比如改完测试环境忘了改回来提交代码时把配置一起推上去了队友拉下来跑起一个指向内网测试服务的项目半天排查不出来。更烦的是大项目重启后依赖预构建也会重新执行冷启动从 20 秒到 1 分钟不等开发体验极差。所以我一直想要的是一个不重启、不修改配置文件、随时可以切换代理目标的能力这就引出了运行时动态代理的概念。1.2 静态代理 vs 动态代理Vite 默认提供的server.proxy配置本质上是一种静态代理。启动 dev server 时Vite 把server.proxy转成http-proxy-middleware实例然后挂到 Connect 中间件栈上。此时代理目标target已经被读取并固化在代理中间件的内部配置里。请求进来后代理中间件只会拿着这个固定目标去转发不会重新读取配置文件或者环境变量。动态代理则相反代理目标不是在中间件创建时固定而是在每个请求到来时重新决定。比如根据请求头、Cookie、查询参数、内存变量甚至远程配置中心来决定这次请求到底转发到哪个后端。http-proxy-middleware提供的router选项就是做这种动态路由的开关键。router可以是一个对象也可以是一个函数函数会在每个请求处理时被调用返回值作为本次请求的真实目标。听起来很玄乎其实就一句话把改配置重启变成请求时现算。这也是运行时切换代理能实现的基础。1.3 动态代理适合哪些场景下面这几种情况建议直接上动态代理前后端并行开发后端端口或 IP 经常变动前端不想为每个变动都重启一次。需要根据不同分支在多个远程环境之间来回切换比如 dev、test、staging 三个环境联调。需要把某个请求临时转发到队友电脑上帮队友 debug 接口不想改全局配置。做多服务聚合不同请求头携带不同的环境标识路由到不同后端的灰度或分流场景。如果你是单人开发只有一套本地后端那动态代理可能没必要。但只要你的开发环境超过两套或者你的同事经常需要联调动态代理就能省下大量时间。下面从原理到实现一步步说清楚。2. Vite 代理机制与实现原理2.1 server.proxy 到底怎么工作的要理解运行时切换得先知道 Vite 代理的内部机制。Vite 开发服务器是基于 Connect 的中间件架构。当你在vite.config.ts里写了这样的配置export default defineConfig({ server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } })Vite 会在启动阶段读取这个对象遍历每个 key比如/api创建一个http-proxy-middleware实例然后通过server.middlewares.use()把这层代理中间件挂到中间件栈的对应位置。此后浏览器里发向http://localhost:5173/api/xxx的请求会被这个代理中间件拦截转发到http://localhost:8080/api/xxx。关键点在于这个target是在中间件创建时传进去的属于 初始化配置。如果运行期间没有特殊机制中间件不会自己重新读取源配置所以改vite.config.ts里的target必须重启才能生效。http-proxy-middleware内部还支持上下文匹配、路径重写、WebSocket 转发、错误处理等一系列逻辑但这些都是在中间件对象创建时确定好行为边界。2.2 反向代理的几个核心参数在配置动态代理之前先把 Vite proxy 里几个常用参数的真实含义理清楚。这些参数同时也是http-proxy-middleware的参数Vite 只是原样透传。参数作用典型值target代理目标服务器地址http://localhost:8080changeOrigin将请求头中的Host与Origin改为目标地址的来源truepathRewrite对请求路径进行重写常用于去掉/api前缀{ ^/api: }secure是否校验 HTTPS 证书目标地址为自签名证书时必须设为falsefalsews是否转发 WebSocket 升级请求truerouter动态决定 target 的回调函数或配置表函数changeOrigin很容易被忽略但它在后端有跨域校验或防盗链时特别重要。浏览器发出的请求Host是localhost:5173代理转发时如果不改Host目标服务器的安全策略可能会拒绝请求。changeOrigin: true可以把这个头改成目标域名避免很多诡异的联调问题。pathRewrite则解决前后端路径不一致的问题。比如前端统一请求/api/user但后端路由是/user那就要在转发前把/api去掉。如果不做路径重写代理会把/api/user原样转发给后端后端大概率返回 404。2.3 为什么不能直接写运行时读取的变量有同学会想那我能不能在vite.config.ts里写target: process.env.MY_BACKEND_URL然后通过修改环境变量来切换答案是能读到环境变量但读不到运行时变化的环境变量。因为 Vite 配置文件是在启动时执行的process.env.MY_BACKEND_URL只在启动那一刻被求值一次之后哪怕你改环境变量、重新exportVite 进程内的变量也不会自动更新。如果项目用了 dotenv 这类工具同样是在启动时读取.env文件之后的修改也不会自动生效。有些项目会做保存配置后自动重启 dev server这种方案本质上不是动态代理而是自动重启。它能解决的只是手动重启的体力劳动但重启带来的页面刷新、状态丢失、依赖预构建等待时间都还在。真正的运行时切换应该做到请求发出前的那一刻才去决定转发目标。这就是下一章要讲的router函数。3. 实现运行时切换代理的三种方案3.1 方案一使用 http-proxy-middleware 的 router 函数先看最轻量、最优雅的一种直接在 Vite 的server.proxy配置里加一个router属性。这个属性是http-proxy-middleware提供的Vite 会透传给内部代理中间件。router可以是一个对象比如{ /api: http://localhost:8080 }也可以是一个函数。函数签名是(req) targetString。这里的关键是router函数会在每个请求进入代理时被调用它的返回值会覆盖之前设置的target。所以我们可以把目标环境编码到请求头里或者从 Cookie、查询参数里读出来。一个最基础的配置模板// vite.config.ts import { defineConfig } from vite; const proxyTargets { dev: http://localhost:8080, test: http://10.0.0.5:8080, mock: http://localhost:3000, }; export default defineConfig({ server: { proxy: { /api: { target: http://placeholder:8080, // 占位地址router 会覆盖 changeOrigin: true, router(req) { const env req.headers[x-target-env] || dev; return proxyTargets[env] || proxyTargets.dev; }, }, }, }, });这个配置的意思是所有/api请求进来先看请求头里的x-target-env。如果值是test本次请求就转发到http://10.0.0.5:8080如果是mock就转发到http://localhost:3000默认走dev。因为router是每次请求都执行所以切环境只需要改变请求头dev server 完全不需要重启。实际操作中如果请求头携带自定义值可能会触发浏览器预检更稳妥的方式是用 Cookie 或查询参数。后面 4.2 会详细对比。3.2 方案二自定义 Vite 插件用全局接口切换router函数虽然灵活但它需要每次请求都带着环境标识。如果团队里有的人用 Postman有的人用浏览器还有的人啥都不想设置希望有一个全局的开关——点一下整个项目的代理目标就变了那就是方案二。实现思路写一个 Vite 插件在configureServer里维护一个当前代理目标的内存变量然后暴露一个内部接口比如POST /__proxy外部触发后修改这个变量。代理中间件同样靠router函数读取这个变量从而实现所有请求统一切换。完整代码示例如下// vite-plugin-dynamic-proxy.ts import { createProxyMiddleware } from http-proxy-middleware; import type { Plugin } from vite; const targets: Recordstring, string { dev: http://localhost:8080, test: http://10.0.0.5:8080, mock: http://localhost:3000, }; export function dynamicProxy(): Plugin { let currentTarget targets.dev; const proxyMiddleware createProxyMiddleware({ target: currentTarget, changeOrigin: true, router() { return currentTarget; }, }); return { name: dynamic-proxy, configureServer(server) { // 所有 /api 请求走动态代理 server.middlewares.use(/api, proxyMiddleware); // 内部控制接口GET 查看当前目标POST 切换目标 server.middlewares.use(/__proxy, (req, res) { if (req.method GET) { res.setHeader(Content-Type, application/json); res.end(JSON.stringify({ current: currentTarget, targets })); return; } if (req.method POST) { let body ; req.on(data, (chunk) (body chunk)); req.on(end, () { try { const { target } JSON.parse(body); if (!Object.values(targets).includes(target)) { res.statusCode 400; res.end(invalid target); return; } currentTarget target; res.statusCode 200; res.end(ok); } catch (err) { res.statusCode 400; res.end(bad request); } }); return; } res.statusCode 405; res.end(Method Not Allowed); }); }, }; }然后在vite.config.ts里引入这个插件import { dynamicProxy } from ./vite-plugin-dynamic-proxy; export default defineConfig({ plugins: [dynamicProxy()], });使用方式很简单在浏览器控制台执行fetch(/__proxy, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ target: http://10.0.0.5:8080 }) });之后再发/api请求就会自动转发到新目标页面无需刷新。整个项目的人都可以用这一个接口把开发环境切换做成一个团队内部小工具。这个方案适合需要团队协作、统一管理的项目。3.3 方案三监听配置文件变化自动重启不推荐有一些团队用chokidar监听一个proxy.config.json文件一变就调server.restart()重启 Vite。这个方案严格来说不是运行时切换它还停留在重启生效的模式里。相比手动重启它只是省去了按键操作。但每次重启带来的副作用一样都没少浏览器页面需要刷新、前端状态丢失、Vite 重新预构建依赖、大项目等待时间长。而且如果频繁修改配置文件dev server 可能会反复重启开发体验更糟。我个人的态度是这个方案只适合做兜底比如项目里所有组件都不允许加自定义逻辑、只能改配置文件时用它。否则优先考虑方案一和方案二。3.4 方案对比与选型建议方案是否重启切换粒度实现成本适用场景router 函数 请求头/Cookie否请求级低单人联调、多环境快速切换自定义插件 内部控制接口否全局中团队协作、统一环境管理监听文件自动重启是全局低无自定义逻辑限制的兜底方案选型建议如果你只是自己开发时切换环境用方案一就够了改动几行配置不需要额外依赖。如果你需要把切换能力分享给团队里的测试、后端或者要做一个开发工具面板那就上方案二。方案三我基本不推荐除非项目对配置文件有强约束。4. 实操基于 router 函数实现运行时切换4.1 从占位 target 开始方案一不需要额外安装依赖Vite 内部已经带了http-proxy-middleware。只需要改vite.config.ts。先整理一个相对完整的配置把路径重写也加上避免很多项目常见的 404// vite.config.ts export default defineConfig({ server: { proxy: { /api: { target: http://placeholder:8080, // 占位 changeOrigin: true, secure: false, pathRewrite: { ^/api: }, router(req) { // 默认走 dev const env req.headers[x-target-env] ?? dev; const targets { dev: http://localhost:8080, test: http://10.0.0.5:8080, mock: http://localhost:3000, }; return targets[env] ?? targets.dev; }, }, }, }, });注意target写的是http://placeholder:8080这个地址一般不会真实存在也不影响使用因为router会在每个请求时返回真实值。这样配置的好处是即使某次router函数因为异常没有执行请求会失败但不会被错误转发到某个真实服务便于发现问题。pathRewrite把/api前缀去掉然后转发给后端。如果你的后端接口本身就需要/api前缀就不要加这个选项。很多人踩过 404 的坑都是因为前后端路径没对齐。4.2 通过请求头、Cookie、查询参数切换在上面配置里router读的是req.headers[x-target-env]。这种方式在 curl 里测试最方便curl -H x-target-env: test http://localhost:5173/api/user后端日志里会看到来自10.0.0.5:8080的请求。但如果在前端浏览器里用fetch带自定义头会触发跨域预检OPTIONS 请求后端如果没有对应的OPTIONS处理反而可能失败。所以浏览器场景我更推荐用 Cookie 或者查询参数。用 Cookie 的实现router(req) { const match (req.headers.cookie || ).match(/proxy_env(dev|test|mock)/); const env match ? match[1] : dev; return targets[env] ?? targets.dev; }这样只要在浏览器控制台执行document.cookie proxy_envtest下一次请求就会切到 test 环境不需要自定义请求头也不触发预检。用查询参数的实现router(req) { const url new URL(req.url, http://localhost); const env url.searchParams.get(targetEnv) || dev; return targets[env] ?? targets.dev; }请求/api/user?targetEnvmock就会走 mock 服务。不过查询参数会被代理原样转发到后端如果不想让后端看到需要在pathRewrite里配合处理稍微麻烦一些。综合下来Cookie 方案在浏览器里最省事。4.3 通过自定义接口实现一键切换如果你选择了方案二插件里的POST /__proxy接口已经可以实现全局切换。把它包装成一段页面脚本就能做成一个迷你控制台。比如在 React 项目里可以在调试环境放一个小组件import { useEffect, useState } from react; export function ProxySwitcher() { const [target, setTarget] useState(); useEffect(() { fetch(/__proxy) .then((res) res.json()) .then((data) setTarget(data.current)); }, []); const switchProxy async () { const next prompt(输入代理目标地址, target); if (!next) return; await fetch(/__proxy, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ target: next }), }); setTarget(next); }; return button onClick{switchProxy}切换代理/button; }这种交互在团队内部联调时很好用测试同学也可以自己点按钮切换不需要碰任何配置。要注意的是这个接口只能在 dev server 环境中使用部署到生产环境时必须移除否则暴露了内部代理控制口安全性会出问题。4.4 页面侧自动读取环境并切换还有一种进阶玩法从 URL 参数或者本地存储里读取目标环境在应用启动时调用一次切换接口。比如访问http://localhost:5173/?backendtest页面里的一段初始化脚本解析出backendtest然后调用POST /__proxy设置全局目标。这个思路可以当成团队的环境路由约定前端攻城狮、后端、测试都能通过改 URL 参数来快速指定环境不需要任何额外工具。实现时要注意时序切换接口要在业务接口发出之前调用。放在应用入口文件的最前面用fetch同步等待完成后再渲染应用或者用阻塞式XMLHttpRequest。虽然体验一般但能保证首次数据请求就落在正确的代理目标上。4.5 容易被忽略的细节运行时切换不是只改target就完了还有几个配置细节会影响结果。changeOrigin一定要开否则目标后端可能因为Host不一致拒绝请求或者无法识别环境。secure如果代理目标是https://且使用自签名证书一定要设置secure: false否则会报证书错误。ws如果后端有 WebSocket 接口且走的是/api路径代理需要设置ws: true否则 WebSocket 握手会失败。Vite 的 HMR 有自己的 ws 路径一般不会冲突。cookieDomainRewrite如果代理目标设置了基于域名的 Cookie跨域转发后浏览器可能不认。配合changeOrigin和目标域设置必要时使用cookieDomainRewrite: 将 Cookie 域重写为当前域。这些细节是动态代理能真正跑通的关键比切换目标本身更容易踩坑。5. 常见问题与排查实录5.1 配置了 router 但请求还是走旧地址这个是最常见的问题。排查看三处第一确认router函数有没有被触发。最简单的方式是在router里加一行console.log如果 dev server 终端没有任何输出说明请求根本没走到这个中间件。第二检查代理路径匹配。比如你配置的是/api但请求实际发起的是/api2或/apis那就不会命中。这是 Vite 前缀匹配的正常行为不是 bug。第三检查是否有两个代理中间件互相干扰。比如自定义插件里手动挂了一个 proxy同时又在server.proxy里配置了同样的路径后挂的中间件可能把请求拦走了导致 router 无法执行。解决原则同一个路径只保留一条代理通道要么用server.proxy配置要么用插件自己挂载不要两边都配。5.2 请求返回 404 或 502404 大概率是路径重写问题。前端请求/api/user后端实际路由是/user你需要在代理里配置pathRewrite: { ^/api: }。如果后端路由本身需要/api那就不重写。还有一个盲区有的代理目标服务有自己的全局前缀比如实际地址是http://10.0.0.5:8080/api-service/user那target应该写成完整的http://10.0.0.5:8080/api-service同时pathRewrite要把/api去掉。这一点需要和后端确认。502 则说明代理目标服务不可达。可能是地址写错、端口没监听、服务未启动也可能是目标服务器的防火墙拦截。用curl直接访问目标地址验证一下基本能定位。5.3 WebSocket 连接总是断如果你的业务使用 WebSocket并且走的是代理路径需要在代理配置里显式加上ws: true。很多人的 WebSocket 握手是在/api这个路径下进行的如果只代理普通 HTTP握手请求没有被转发连接自然建立不起来。另外如果代理目标是 HTTPS自签名证书会导致 WebSocket 握手失败也要配合secure: false使用。可以先在浏览器 Network 面板里看 WS 请求的状态码如果是 502 或者 200 但握手失败基本就是代理没设置ws或证书校验问题。5.4 HTTPS 目标报证书错误代理目标如果是https://地址且后端用的是自签名证书浏览器控制台经常出现net::ERR_CERT_COMMON_NAME_INVALID错误。这不是代理代码的问题而是 Node 在转发时进行了证书校验。解决办法是给代理配置加一行secure: false告诉 Node 跳过对目标端证书的校验。注意secure和浏览器里的继续访问是两回事它控制的是 Node 与目标后端之间的 TLS 校验不会影响浏览器到 Vite dev server 的那一段。只在开发环境下使用生产环境切到 HTTPS 外部代理时不要盲目关闭。5.5 代理目标切换后登录态丢失动态切换环境后Cookie 经常对不上号。比如你在 dev 环境登录过Cookie 的 Domain 是localhost切到 test 环境后后端返回的 Cookie Domain 是10.0.0.5浏览器不会把它和localhost关联最终表现为登录失效。这种情况可以通过cookieDomainRewrite: 把目标端返回的 Cookie Domain 重写成当前请求域让浏览器能正确保存。另外如果登录态是放在Authorization头里的也要确认切换目标后 token 是否仍然有效。多环境切换时建议把本地存储的 token 也一并切换避免出现代理切过去了但请求头还是旧 token的问题。5.6 代理切换接口被外部访问用方案二暴露的/__proxy接口如果部署到生产环境等于把一个可以随意修改代理目标的开关暴露给所有人。这是一个安全隐患。所以要在插件里加一个环境判断只在server模式下注册这个接口生产构建时不要执行。比如configureServer(server) { if (process.env.NODE_ENV ! development) return; // 注册控制接口 }如果项目里用了统一的权限校验也可以在控制接口上做一层鉴权。安全永远是高优先级尤其是团队内网工具也不能掉以轻心。6. 扩展思路与个人心得6.1 生产环境能动态代理吗生产环境通常部署在 Nginx 后面一般不建议做运行时动态切换因为生产环境的稳定性、安全性和可追踪性远比开发方便更重要。但 Nginx 本身也有类似的动态路由能力比如用map指令根据请求头映射不同上游服务器或者用proxy_pass配合变量。如果你的生产环境真的有灰度分流或者多环境切换需求可以在 Nginx 层做。但要注意流量治理的严肃性动态切换手段越灵活越要配合日志和监控体系。开发环境用 Vite 做动态代理是因为 dev server 只服务开发者自己权限边界清楚即使出问题也不会影响线上用户。6.2 把切换机制做成团队开发工具我现在负责的项目已经不再让开发者手动改vite.config.ts了。我们把方案二做成了一个小工具插件内置在项目的 devDependencies 里团队所有人跑起来后可以在一个内部调试面板中选择环境dev、test、mock、某位同事的机器。选择完成后所有/api请求自动切到对应环境前端不会刷新后端 log 也能实时看到来源。这个工具的收益是后端同事不用再为前端连不上我的服务反复发地址前端同事不用再为某个环境接口异常而全局排查测试可以直接在浏览器里切换后端版本做快速对比验证。它把团队的协作摩擦降到了很低。6.3 我踩过的一些坑写下来提醒你第一个坑不要在router函数里做耗时操作。比如每次请求去读取磁盘上的 JSON 文件、去请求远程配置中心虽然技术上可行但会拖慢每个接口的响应速度联调时根本没法用。建议把配置缓存到内存里切换时才更新。第二个坑router函数执行时req.url是原始请求路径不是重写后的路径。如果要根据路径分流务必先搞清楚前缀匹配和pathRewrite的执行顺序。大多数场景下router先执行拿到目标后才做路径重写。我在这个地方调试了半个下午。第三个坑切换接口要设计成可逆操作。切到某个环境后如果发现接口异常应该能一键切回默认环境。方案二的GET /__proxy可以展示当前目标但最好再支持一个POST /__proxy/reset一键恢复默认值效率会高很多。第四个坑多人共用一个 dev server 时全局切换会误伤别人。比如后端同事对外演示时把代理切到了他自己机器上其他正在联调的前端同事全部断掉。解决方法是使用请求头或 Cookie 方案替代全局变量或者给控制接口增加会话概念只影响当前会话。不过这样复杂度就上去了一般团队不需要。6.4 最后一点实用建议如果你想让切换代理的目标地址支持内网 IP 直连千万不要写死端口最好把每个环境的目标地址拆成protocol host port三个字段方便以后端口调整时只改一处。同时把常用环境做成预设而不是让使用者手填完整 URL可以避免不少低级错误。另外强烈建议在 README 里留下一段速查命令把 curl、Cookie 和插件控制接口的用法写清楚。团队工具最怕只有一个人会玩一旦那个人休假整个团队的联调节奏就乱了。文档里用最直白的话写下默认环境是哪个、怎么切、怎么改回默认足够了。动态代理这个事核心不是技术多难而是把开发环境的切换成本从分钟级降到秒级。做好了整个团队的联调效率都能上一个台阶。上面这些配置和思路是我在实际项目里反复试出来的你完全可以直接抄。如果你们项目里有更变态的代理需求比如根据请求体内容动态分流欢迎按照类似思路扩展原理都是一样的。