Vite 代理跨域全解析:从 server.proxy 到请求转发的实现原理

在前后端分离的开发模式中,跨域问题几乎是每个前端开发者都会遇到的"拦路虎"。当浏览器中运行的前端应用(例如 http://localhost:5173)向后端 API(例如 http://localhost:3000/api/users)发起请求时,由于协议、域名或端口不同,浏览器的同源策略会直接阻止请求,并抛出 CORS 错误。虽然可以通过后端配置 CORS 头解决,但在开发阶段,更便捷的方案是在前端开发服务器上设置代理。Vite 的 server.proxy 配置正是为此而生。本文将深入浅出地讲解 Vite 代理是如何解决跨域问题的。

同源策略与跨域的本质

首先,回顾一下跨域产生的根本原因:浏览器的同源策略。该策略规定,一个源(协议+域名+端口)的脚本只能访问同源的资源。当前端页面和 API 服务器源不一致时,浏览器就会拦截响应。

传统的解决方式包括:

  • JSONP(只支持 GET)。
  • CORS (后端配置 Access-Control-Allow-Origin)。
  • 代理(前端开发服务器转发请求)。

Vite 代理的核心原理:中间件转发

Vite 开发服务器本质上是一个 Node.js HTTP 服务器。server.proxy 配置利用 http-proxy-3 库创建了一个代理中间件。该中间件会拦截特定规则的请求,不将请求交给 Vite 的静态文件服务或模块转换管道,而是直接转发到目标后端服务器,然后将后端的响应原样返回给浏览器。

为什么这能绕过同源策略?

关键点在于:代理发生在服务器端,而不是浏览器端

  1. 浏览器向 http://localhost:5173/api/users 发起请求(同源请求,因为页面也是从 localhost:5173 加载的)。
  2. Vite 服务器接收到请求后,根据代理配置,在服务器内部http://localhost:3000/api/users 发起新的 HTTP 请求。
  3. 后端返回响应给 Vite 服务器,Vite 服务器再将响应返回给浏览器。

由于浏览器始终与同源的 Vite 服务器通信,不存在跨域问题。整个过程对浏览器透明,它认为自己请求的是同源资源。

Vite 代理的工作流程

  1. 启动开发服务器 :Vite 读取 server.proxy 配置,为每个规则创建一个 http-proxy 实例。
  2. 请求到达 :浏览器发送请求到 Vite 服务器(例如 http://localhost:5173/api/users)。
  3. 中间件匹配 :Vite 的 proxyMiddleware 根据请求路径匹配代理规则。如果匹配,则进入代理流程;否则交给下一个中间件(如静态文件服务)。
  4. 转发请求http-proxytarget 发起新的 HTTP 请求,复制原请求的 headers、body 等。
  5. 接收响应 :目标服务器返回响应,http-proxy 将响应头和数据写回给浏览器。
  6. 完成:浏览器收到响应,由于是同源请求,没有跨域限制。

Vite 中的反向代理(最常用)

Vite 开发服务器(运行在 localhost:5173)通过 server.proxy 将特定前缀的请求转发到后端 API 服务器(如 http://localhost:3000),从而绕过浏览器的同源策略。

工作流程

  • 浏览器请求 http://localhost:5173/api/users
  • Vite 服务器识别到 /api 前缀,作为代理客户端 。向 http://localhost:3000/api/users 发起请求。
  • 后端返回数据,Vite 原样转发给浏览器。
  • 整个过程浏览器只与 Vite 服务器通信,不知后端存在。
js 复制代码
// vite.config.js
export default {
  server: {
    proxy: {
      '/api': {
        target: 'http://localhost:3000',   // 后端服务器地址
        changeOrigin: true,   // 修改 Host 头为 target
        rewrite: (path) => path.replace(/^\/api/, '')
      }
    }
  }
}

Vite 中的正向代理

工作流程

  • Vite 服务器收到 /api 请求后,不直接连接 target ,而是先连接到 forward 指定的正向代理服务器。
  • 正向代理再转发请求到真正的 target
  • 响应原路返回。
js 复制代码
// vite.config.js
export default {
  server: {
    proxy: {
      '/api': {
        target: 'http://target-server.com',   // 最终目标
        forward: 'http://proxy.company.com:8080', // 正向代理地址
        changeOrigin: true
      }
    }
  }
}

当浏览器发起一个匹配 /api 的请求时,Vite 开发服务器会执行以下步骤:

  1. 浏览器请求GET http://localhost:5173/api/users
  2. Vite 代理中间件匹配:根据配置,该请求应被代理。
  3. 构造请求选项 :Vite 使用 common.setupOutgoing 构造出站请求参数,包括目标主机、路径、头部等。
  4. 检测到 forward 配置 :由于配置了 forward,Vite 不会直接连接 target,而是:
    • 创建一个到 forward 地址(正向代理服务器)的 HTTP 或 HTTPS 请求。
    • 将原始请求的 URL、头部、请求体等封装后发给正向代理。
  5. 正向代理转发 :正向代理服务器收到请求后,根据请求中的目标地址(即 target 中的主机和路径)向真正的目标服务器发起请求。
  6. 目标服务器响应:目标服务器返回数据给正向代理。
  7. 正向代理回传:正向代理将响应返回给 Vite 开发服务器。
  8. Vite 转发给浏览器:Vite 将响应原样返回给浏览器。

整个流程对浏览器完全透明,浏览器只知道自己请求了 localhost:5173,并不知道请求经过了正向代理和目标服务器。

server.proxy 配置

js 复制代码
proxy?: Record<string, string | ProxyOptions>
typescript 复制代码
interface ProxyOptions extends httpProxy.ServerOptions {
  /**
   * rewrite path 重写请求路径
   * 接收原始路径,返回新路径。常用于去掉代理前缀。
   */
  rewrite?: (path: string) => string
  /**
   * configure the proxy server (e.g. listen to events)
   * 提供一个钩子,允许直接访问底层 http-proxy 实例,用于监听事件或自定义行为。
   */
  configure?: (proxy: httpProxy.ProxyServer, options: ProxyOptions) => void
  /**
   * webpack-dev-server style bypass function
   * 绕过代理,直接由 Vite 开发服务器处理请求
   */
  bypass?: (
    req: http.IncomingMessage,
    /** undefined for WebSocket upgrade requests */
    res: http.ServerResponse | undefined,
    options: ProxyOptions,
  ) =>
    | void
    | null
    | undefined
    | false
    | string
    | Promise<void | null | undefined | boolean | string>
  /**
   * rewrite the Origin header of a WebSocket request to match the target
   * 重写 WebSocket 请求的 Origin 头,使其与代理目标匹配。
   *
   * **Exercise caution as rewriting the Origin can leave the proxying open to [CSRF attacks](https://owasp.org/www-community/attacks/csrf).**
   * 安全警告:官方文档明确警告,重写 Origin 可能导致 CSRF 攻击,应谨慎使用。
   */
  rewriteWsOrigin?: boolean | undefined
}

http-proxy-3/lib/http-proxy/index.ts

typescript 复制代码
interface ServerOptions {
  // NOTE: `options.target and `options.forward` cannot be both missing when the
  // actually proxying is called.  However, they can be missing when creating the
  // proxy server in the first place!  E.g., you could make a proxy server P with
  // no options, then use P.web(req,res, {target:...}).
  /** URL string to be parsed with the url module. */
  // 最终目标服务器 URL,代理请求将被转发到此地址
  target?: ProxyTarget;
  /** URL string to be parsed with the url module or a URL object. */
  // 上游正向代理服务器 URL。若指定,请求会先发给 forward,再由其转发到 target
  forward?: ProxyTargetUrl;
  /** Object to be passed to http(s).request. */
  // 自定义 HTTP/HTTPS 代理的 Agent 实例,用于控制连接池、代理认证等
  agent?: any;
  /** Object to be passed to https.createServer(). */
  // 当创建 HTTPS 服务器时,传入的 TLS 选项
  ssl?: any;
  /** If you want to proxy websockets. */
  // 是否代理 WebSocket 连接
  ws?: boolean;
  /** Adds x- forward headers. */
  // 是否添加 X-Forwarded-For、X-Forwarded-Port、X-Forwarded-Proto 等头部,用于向后端传递原始客户端信息
  xfwd?: boolean;
  /** Verify SSL certificate. */
  // 是否验证 SSL 证书
  secure?: boolean;
  /** Explicitly specify if we are proxying to another proxy. */
  // 是否将当前代理视为另一个代理的下游。用于链式代理场景,会影响请求路径的处理
  toProxy?: boolean;
  /** Specify whether you want to prepend the target's path to the proxy path. */
  // 是否将 target 的路径前缀添加到代理请求路径前
  prependPath?: boolean;
  /** Specify whether you want to ignore the proxy path of the incoming request. */
  // 是否忽略代理路径
  ignorePath?: boolean;
  /** Local interface string to bind for outgoing connections. */
  // 本地网络接口的 IP 地址,用于绑定出站连接的源地址
  localAddress?: string;
  /** Changes the origin of the host header to the target URL. */
  // 是否将请求头中的 Host 改为 target 的主机名。解决后端根据 Host 做虚拟主机路由时的跨域问题
  changeOrigin?: boolean;
  /** specify whether you want to keep letter case of response header key */
  preserveHeaderKeyCase?: boolean;
  /** Basic authentication i.e. 'user:password' to compute an Authorization header. */
  // 基本认证凭证('user:password'),会自动生成 Authorization 头
  auth?: string;
  /** Rewrites the location hostname on (301 / 302 / 307 / 308) redirects, Default: null. */
  // 重写重定向响应中的 Location 头的主机名部分
  hostRewrite?: string;
  /** Rewrites the location host/ port on (301 / 302 / 307 / 308) redirects based on requested host/ port.Default: false. */
  // 是否根据原始请求的主机和端口自动重写重定向的 Location 头
  autoRewrite?: boolean;
  /** Rewrites the location protocol on (301 / 302 / 307 / 308) redirects to 'http' or 'https'.Default: null. */
  // 强制将重定向中的协议重写为 'http' 或 'https'
  protocolRewrite?: string;
  /** rewrites domain of set-cookie headers. */
  // 重写 Set-Cookie 头中的 Domain 属性。可设为固定字符串或映射对象
  cookieDomainRewrite?: false | string | { [oldDomain: string]: string };
  /** rewrites path of set-cookie headers. Default: false */
  // 重写 Set-Cookie 头中的 Path 属性
  cookiePathRewrite?: false | string | { [oldPath: string]: string };
  /** object with extra headers to be added to target requests. */
  headers?: { [header: string]: string | string[] | undefined };
  /** Timeout (in milliseconds) when proxy receives no response from target. Default: 120000 (2 minutes) */
  // 代理请求超时(毫秒),超过此时间未收到目标服务器响应则断开连接
  proxyTimeout?: number;
  /** Timeout (in milliseconds) for incoming requests */
  // 客户端请求超时(毫秒),影响 req 的 timeout 事件
  timeout?: number;
  /** Specify whether you want to follow redirects. Default: false */
  // 是否自动跟随目标服务器返回的 3xx 重定向
  followRedirects?: boolean;
  /** If set to true, none of the webOutgoing passes are called and it's your responsibility to appropriately return the response by listening and acting on the proxyRes event */
  // 若为 true,代理不会自动将响应体返回给客户端,需用户通过监听 proxyRes 事件自行处理
  selfHandleResponse?: boolean;
  /** Buffer */
  // 预读的请求体流,用于在代理开始前已经读取了部分数据的情况
  buffer?: Stream;
  /** Explicitly set the method type of the ProxyReq */
  // 强制指定代理请求的 HTTP 方法
  method?: string;
  /**
   * Optionally override the trusted CA certificates.
   * This is passed to https.request.
   * 覆盖受信任的 CA 证书,用于自定义证书校验
   */
  ca?: string;
  /** Optional fetch implementation to use instead of global fetch, use this to activate fetch-based proxying,
   * for example to proxy HTTP/2 requests
   * 可选的自定义 fetch 实现,用于启用基于 Fetch API 的代理(支持 HTTP/2 等)
  */
 fetch?: typeof fetch;
  /** Optional configuration object for fetch-based proxy requests. 
   * Use this to customize fetch request and response handling. 
   * For custom fetch implementations, use the `fetch` property.*/
  // 配合 fetch 使用的额外选项,如请求重试、超时等
 fetchOptions?: FetchOptions;
}

一、字符串简写

js 复制代码
// vite.config.js
export default {
  server: {
    proxy: {
      '/api': 'http://localhost:3000',
    },
  },
};

二、对象完整配置

js 复制代码
export default {
  server: {
    proxy: {
      '/api': {
        target: 'http://localhost:3000',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api/, '')
      }
    }
  }
}

三、正则表达式匹配

js 复制代码
export default {
  server: {
    proxy: {
      '^/api/.*': 'http://localhost:3000',
      '^/auth/.*': {
        target: 'http://localhost:4000',
        changeOrigin: true
      }
    }
  }
}

源码分析

js 复制代码
  // proxy
  const { proxy } = serverConfig
  // 配置代理中间件
  // 用于将请求转发到其他服务器,如 API 服务器
  if (proxy) {
    const middlewareServer =
      (isObject(middlewareMode) ? middlewareMode.server : null) || httpServer

    middlewares.use(proxyMiddleware(middlewareServer, proxy, config))
  }

中间件会为每个规则创建代理

http-proxy-3 创建代理

配置了 forward

没有配置 forward

相关推荐
早點睡3902 小时前
ReactNative项目OpenHarmony三方库集成实战:react-native-vector-icons
javascript·react native·react.js
梵得儿SHI2 小时前
Vue 3 工程化实战:Axios 高阶封装与样式解决方案深度指南
前端·javascript·vue3·axios·样式解决方案·api请求管理·统一请求处理
暗不需求2 小时前
深入 JavaScript 核心:用原生 JavaScript 打造就地编辑组件
前端·javascript
江湖行骗老中医2 小时前
Vue 3 的父子组件传值主要遵循单向数据流的原则:父传子 和 子传父。
前端·javascript·vue.js
RPGMZ2 小时前
RPGMakerMZ 游戏引擎 野外采集点制作
javascript·游戏·游戏引擎·rpgmz·野外采集点
时寒的笔记2 小时前
js基础05_js类、原型对象、原型链&案例(解决无限debugger)
开发语言·javascript·原型模式
CyrusCJA2 小时前
Nodejs自定义脚手架
javascript·node.js·js
一只小阿乐2 小时前
react中的zustand 模块化
前端·javascript·react.js·react状态管理·zustand
用户84298142418102 小时前
十二个JS混淆加密工具
javascript