返回首页

说说 webpack proxy 工作原理?为什么能解决跨域?

问题解析

这道题考察两个核心知识点:webpack-dev-server 的代理功能以及跨域问题的本质和解决方案。面试官希望看到你能解释清楚代理是如何工作的,以及为什么服务器之间的通信不存在跨域问题。

核心概念

什么是跨域(CORS)

跨域(Cross-Origin Resource Sharing)是浏览器的**同源策略(Same-Origin Policy)**限制。当请求的协议、域名、端口任一不同,即构成跨域:

同源要求:协议 + 域名 + 端口 完全相同

http://localhost:8080 请求 http://localhost:3000/api  -> 跨域(端口不同)
http://a.com 请求 https://a.com/api                    -> 跨域(协议不同)
http://a.com 请求 http://b.com/api                     -> 跨域(域名不同)

浏览器 vs 服务器

场景 是否存在跨域 原因
浏览器 -> 不同源服务器 同源策略限制
服务器 -> 任意服务器 无同源策略限制

详细解答

1. Webpack DevServer Proxy 配置

// webpack.config.js
module.exports = {
  devServer: {
    port: 8080,
    proxy: {
      // 简单配置
      '/api': 'http://localhost:3000',

      // 完整配置
      '/api': {
        target: 'http://localhost:3000',  // 目标服务器
        changeOrigin: true,               // 改变源(虚拟主机)
        pathRewrite: {
          '^/api': ''                     // 重写路径:/api/user -> /user
        },
        secure: false,                    // 接受 HTTPS 无效证书
        ws: true,                         // 代理 WebSocket
        logLevel: 'debug'                 // 日志级别
      },

      // 多个代理配置
      '/auth': {
        target: 'http://auth-server.com',
        changeOrigin: true
      }
    }
  }
};

2. 代理工作原理

请求流程

浏览器请求流程(使用代理前 - 跨域):
┌─────────────┐         ┌─────────────────────┐
│   浏览器     │  ---->  │  http://api.server  │
│ :8080       │  XHR    │ :3000/api/data      │
└─────────────┘         └─────────────────────┘
      │                         │
      │    跨域!被浏览器阻止      │
      └─────────────────────────┘

浏览器请求流程(使用代理后 - 不跨域):
┌─────────────┐         ┌─────────────────┐         ┌─────────────────────┐
│   浏览器     │  ---->  │ webpack-dev-    │  ---->  │  http://api.server  │
│ :8080       │  XHR    │ server :8080    │  HTTP   │ :3000/api/data      │
│             │         │ (代理服务器)     │         │                     │
└─────────────┘         └─────────────────┘         └─────────────────────┘
                              │
                              │  同源!请求成功
                              ▼
                        返回数据给浏览器

详细流程

// 1. 浏览器发起请求(同源,无跨域)
fetch('/api/users');  // 请求 http://localhost:8080/api/users

// 2. webpack-dev-server 接收到请求
// 检查是否匹配 proxy 配置

// 3. 匹配成功,转发请求到目标服务器
const httpProxyMiddleware = require('http-proxy-middleware');

// 伪代码
app.use('/api', httpProxyMiddleware({
  target: 'http://localhost:3000',
  changeOrigin: true,
  pathRewrite: { '^/api': '' }
}));

// 4. 目标服务器处理请求
// http://localhost:3000/users

// 5. 目标服务器返回响应

// 6. webpack-dev-server 将响应返回给浏览器

3. http-proxy-middleware 原理

Webpack DevServer 使用 http-proxy-middleware 实现代理功能:

// http-proxy-middleware 核心原理
const httpProxy = require('http-proxy');

function createProxyMiddleware(options) {
  const proxy = httpProxy.createProxyServer({
    target: options.target,
    changeOrigin: options.changeOrigin,
    // ... 其他配置
  });

  return function proxyMiddleware(req, res, next) {
    // 1. 检查是否匹配代理路径
    if (!shouldProxy(req)) {
      return next();
    }

    // 2. 重写路径
    if (options.pathRewrite) {
      Object.keys(options.pathRewrite).forEach(pattern => {
        const regex = new RegExp(pattern);
        req.url = req.url.replace(regex, options.pathRewrite[pattern]);
      });
    }

    // 3. 修改请求头
    if (options.changeOrigin) {
      req.headers.host = new URL(options.target).host;
    }

    // 4. 转发请求
    proxy.web(req, res, (err) => {
      if (err) {
        console.error('代理错误:', err);
        res.status(500).send('代理错误');
      }
    });
  };
}

4. 为什么能解决跨域

核心原因:同源策略只限制浏览器

┌─────────────────────────────────────────────────────────────────┐
│                        跨域解决原理                              │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│   浏览器                          代理服务器          目标API    │
│   ┌─────────┐                   ┌─────────┐        ┌─────────┐  │
│   │  :8080  │  ──XHR请求──>    │  :8080  │  ──>   │  :3000  │  │
│   │         │  (同源,OK)      │ (devServer)      │ (API)   │  │
│   │         │  <─────────────  │         │  <───  │         │  │
│   └─────────┘                   └─────────┘        └─────────┘  │
│                                                                  │
│   关键点:                                                       │
│   1. 浏览器 -> 代理服务器:同源(都是 localhost:8080)            │
│   2. 代理服务器 -> 目标API:服务器间通信,无同源策略限制           │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘

详细解释

  1. 浏览器视角:请求的是 http://localhost:8080/api/data,同源,不触发跨域限制
  2. 代理服务器:收到请求后,作为客户端向 http://localhost:3000 发起新请求
  3. 服务器间通信:服务器与服务器之间的 HTTP 请求不受同源策略限制
  4. 响应返回:代理服务器将目标服务器的响应原样返回给浏览器

深入理解

changeOrigin 详解

proxy: {
  '/api': {
    target: 'http://api.example.com',
    changeOrigin: true  // 改变请求头中的 origin
  }
}

// changeOrigin: false(默认)
// 请求头:Host: localhost:8080
// 某些服务器会根据 Host 做路由,可能导致 404

// changeOrigin: true
// 请求头:Host: api.example.com
// 虚拟主机场景下必须开启

pathRewrite 使用场景

// 场景:API 路径需要去掉 /api 前缀

// 前端代码
fetch('/api/users');

// 代理配置
proxy: {
  '/api': {
    target: 'http://localhost:3000',
    pathRewrite: { '^/api': '' }
  }
}

// 实际转发到:http://localhost:3000/users
// 而不是:http://localhost:3000/api/users

WebSocket 代理

devServer: {
  proxy: {
    '/socket': {
      target: 'ws://localhost:3000',
      ws: true,  // 代理 WebSocket
      changeOrigin: true
    }
  }
}

// 前端
const socket = new WebSocket('ws://localhost:8080/socket');

多环境代理配置

// 根据环境变量配置不同代理
const PROXY_CONFIG = {
  development: {
    '/api': {
      target: 'http://localhost:3000',
      changeOrigin: true
    }
  },
  testing: {
    '/api': {
      target: 'http://test-api.example.com',
      changeOrigin: true
    }
  },
  staging: {
    '/api': {
      target: 'https://staging-api.example.com',
      changeOrigin: true,
      secure: false  // 接受自签名证书
    }
  }
};

module.exports = {
  devServer: {
    proxy: PROXY_CONFIG[process.env.NODE_ENV] || PROXY_CONFIG.development
  }
};

最佳实践

1. 封装请求基础配置

// src/api/request.js
const BASE_URL = process.env.NODE_ENV === 'development'
  ? ''  // 开发环境使用相对路径,走代理
  : 'https://api.production.com';  // 生产环境完整地址

async function request(url, options = {}) {
  const response = await fetch(`${BASE_URL}${url}`, {
    headers: {
      'Content-Type': 'application/json'
    },
    ...options
  });
  return response.json();
}

// 使用
request('/api/users');  // 开发环境自动代理到 localhost:3000

2. 配合环境变量

// .env.development
REACT_APP_API_URL=/api

// .env.production
REACT_APP_API_URL=https://api.production.com

// 代码中使用
const API_URL = process.env.REACT_APP_API_URL;

3. 代理配置抽离

// config/proxy.js
module.exports = {
  '/api': {
    target: process.env.API_TARGET || 'http://localhost:3000',
    changeOrigin: true,
    pathRewrite: { '^/api': '' },
    onProxyReq: (proxyReq, req) => {
      console.log('代理请求:', req.method, req.url, '->', proxyReq.path);
    },
    onError: (err, req, res) => {
      console.error('代理错误:', err);
      res.status(500).json({ error: '代理服务器错误' });
    }
  }
};

// webpack.config.js
const proxyConfig = require('./config/proxy');

module.exports = {
  devServer: {
    proxy: proxyConfig
  }
};

4. 处理 Cookie 和 Session

proxy: {
  '/api': {
    target: 'http://localhost:3000',
    changeOrigin: true,
    cookieDomainRewrite: 'localhost',  // 重写 cookie domain
    onProxyRes: (proxyRes, req, res) => {
      // 处理响应 cookie
      const cookies = proxyRes.headers['set-cookie'];
      if (cookies) {
        proxyRes.headers['set-cookie'] = cookies.map(cookie =>
          cookie.replace(/; secure/gi, '')  // 开发环境移除 secure 标志
        );
      }
    }
  }
}

面试要点

回答思路

  1. 什么是跨域:浏览器的同源策略限制,协议/域名/端口任一不同即跨域
  2. 代理配置:devServer.proxy 配置,使用 http-proxy-middleware
  3. 工作原理
    • 浏览器请求同源的开发服务器
    • 开发服务器转发请求到目标 API
    • 服务器间通信无跨域限制
    • 响应原样返回给浏览器
  4. 为什么能解决:浏览器只限制浏览器与服务器的通信,服务器之间不受限制

常见追问

Q: 除了代理,还有哪些解决跨域的方法?

A:

  • CORS(服务端设置 Access-Control-Allow-Origin)
  • JSONP(只支持 GET)
  • Nginx 反向代理(生产环境常用)
  • postMessage(iframe 通信)
  • WebSocket(不受同源策略限制)

Q: changeOrigin 的作用是什么?

A: changeOrigin 会改变 HTTP 请求头中的 Host 字段为目标服务器的 host。这在目标服务器使用虚拟主机(根据 Host 路由)时必须开启。

Q: 生产环境怎么用代理?

A: 生产环境通常使用 Nginx 反向代理,配置类似:

location /api {
    proxy_pass http://backend-server;
    proxy_set_header Host $host;
}

Q: pathRewrite 什么时候用?

A: 当后端 API 路径没有 /api 前缀,但前端为了区分需要加前缀时使用。例如前端请求 /api/users,实际转发到后端 /users。

Q: 代理和 CORS 有什么区别?

A: 代理是在开发服务器层面解决跨域,对后端无感知;CORS 是后端通过设置响应头告诉浏览器允许跨域。代理适合开发环境,CORS 适合生产环境或需要开放 API 的场景。

一句话总结

Webpack DevServer 的 proxy 功能通过 http-proxy-middleware 将浏览器请求转发到目标服务器,由于浏览器只限制浏览器与服务器的跨域通信,而服务器之间不受同源策略限制,因此可以解决开发环境的跨域问题。