说说 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:服务器间通信,无同源策略限制 │
│ │
└─────────────────────────────────────────────────────────────────┘
详细解释
- 浏览器视角:请求的是
http://localhost:8080/api/data,同源,不触发跨域限制 - 代理服务器:收到请求后,作为客户端向
http://localhost:3000发起新请求 - 服务器间通信:服务器与服务器之间的 HTTP 请求不受同源策略限制
- 响应返回:代理服务器将目标服务器的响应原样返回给浏览器
深入理解
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 标志
);
}
}
}
}
面试要点
回答思路
- 什么是跨域:浏览器的同源策略限制,协议/域名/端口任一不同即跨域
- 代理配置:devServer.proxy 配置,使用 http-proxy-middleware
- 工作原理:
- 浏览器请求同源的开发服务器
- 开发服务器转发请求到目标 API
- 服务器间通信无跨域限制
- 响应原样返回给浏览器
- 为什么能解决:浏览器只限制浏览器与服务器的通信,服务器之间不受限制
常见追问
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 将浏览器请求转发到目标服务器,由于浏览器只限制浏览器与服务器的跨域通信,而服务器之间不受同源策略限制,因此可以解决开发环境的跨域问题。