无需后端配合,前端如何优雅解决跨域并实现多环境切换?
上周三下午,团队遇到一个棘手问题:需要在 5 个不同环境(开发、测试、预发布、UAT)之间来回切换验证功能,每次都要改配置文件、重启服务,折腾半小时才能开始开发调试。更麻烦的是,直接调用跨域 API 时被 CORS 挡在门外,等后端配置又要排期。
这种情况你肯定也遇到过。今天分享一个我们实际在用的方案:动态代理 + 请求头路由,专门针对开发和调试阶段,不用改后端代码,不用重启服务,在页面上点一下就能切换环境,多人同时开发不同环境也互不干扰。
1. 问题到底卡在哪里
先说说我们当时的处境,看看有没有同款痛点。
1.1 环境切换太麻烦
开发和调试时,经常要在多个环境之间切换验证。传统做法是改 .env 文件或者在代码里写死 API 地址:
// 改之前
const API_BASE = 'https://dev.example.com'
// 改之后
const API_BASE = 'https://uat.example.com'
每次切换都要:
- 修改配置文件
- 重启开发服务器
- 重新登录系统
最坑的是,调试到一半忘记改回来,把其他环境的配置提交上去了...
1.2 CORS 错误绕不过
开发和调试时,经常会直接访问不同环境的 API,浏览器直接拦截:
Access to XMLHttpRequest at 'https://test-api.example.com'
from origin 'http://localhost:3000' has been blocked by CORS policy
等后端配置 CORS?沟通协调至少半天,开发进度直接卡住。而且不同环境的 CORS 配置策略不同,临时请求调整也很麻烦。
1.3 多人协作互相干扰
团队 5 个人同时开发,如果共用一套环境配置,张三切到测试环境,李四的请求也跟着过去了,排查问题都找不到北。特别是多人并行开发不同环境时,这种干扰会严重影响效率。
2. 解决思路
核心想法其实不复杂:让每个请求自己携带环境信息,代理服务器根据这个信息动态转发。
浏览器前端 → 携带 X-Env-Target 请求头 → 代理服务器 → 动态转发到目标开发/测试环境
具体分三步:
- 前端在界面选择目标环境,存到 localStorage,每次请求自动带上环境信息
- 代理服务器读取请求头,动态转发到对应环境
- 代理层统一处理 CORS,绕过目标环境的跨域限制
3. 具体实现
3.1 前端部分
3.1.1 环境选择 UI
我们在登录页面加了一个环境选择器,开发者可以直接选择目标环境(开发、测试、UAT 等):
// src/pages/Login/index.tsx
interface EnvConfig {
name: string
url: string
}
const DEFAULT_ENVS: EnvConfig[] = [
{ name: '开发', url: 'https://dev-api.example.com' },
{ name: '测试', url: 'https://test-api.example.com' },
{ name: 'UAT', url: 'https://uat-api.example.com' }
]
const STORAGE_KEY_CURRENT_ENV = 'current_env_url'
const handleEnvChange = (value: string) => {
setCurrentEnvUrl(value)
localStorage.setItem(STORAGE_KEY_CURRENT_ENV, value)
message.success('Switching environment...')
setTimeout(() => {
window.location.reload()
}, 200)
}
这里有个细节:切换环境后需要刷新页面,因为有些全局配置是在初始化时读取的。
3.1.2 请求拦截器注入环境信息
axios 拦截器里自动添加环境请求头,业务代码完全无感知:
// src/utils/request.ts
const HttpService = axios.create({
baseURL: import.meta.env.DEV ? 'http://localhost:3001' : import.meta.env.VITE_API_BASE_URL,
timeout: 60000,
headers: { 'Content-Type': 'application/json' }
})
HttpService.interceptors.request.use(config => {
if (import.meta.env.DEV) {
const envTarget = localStorage.getItem('current_env_url')
if (envTarget) {
config.headers['X-Env-Target'] = envTarget
}
}
return config
})
为什么用请求头而不是 URL 参数?
一开始我们试过把环境信息放在 URL 里,比如 /api/users?env=test,但很快发现问题:
- 每个 API 调用都要处理这个参数
- 容易遗漏,导致请求发到错误环境
- URL 变长,日志里也不好看
用请求头就干净多了,拦截器统一处理,业务代码完全不用管。
3.2 代理服务器
3.2.1 初始化
npm install express http-proxy-middleware
3.2.2 完整实现
// proxy-server.js
import express from 'express'
import { createProxyMiddleware } from 'http-proxy-middleware'
const app = express()
// 处理 OPTIONS 预检请求
app.options('*', (req, res) => {
res.header('Access-Control-Allow-Origin', '*')
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, PATCH, OPTIONS')
res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization, x-env-target')
res.header('Access-Control-Max-Age', '86400')
res.sendStatus(200)
})
const proxy = createProxyMiddleware({
target: 'http://localhost:3000',
changeOrigin: true,
secure: false,
logLevel: 'warn',
// 转发前检查请求头
onProxyReq: (proxyReq, req, res) => {
const envTarget = req.headers['x-env-target']
if (!envTarget || typeof envTarget !== 'string') {
return res.status(400).json({
error: 'Bad Request',
message: 'Missing X-Env-Target header'
})
}
console.log(`[Proxy] ${req.method} ${req.url} -> ${envTarget}${req.url}`)
},
// 响应返回前添加 CORS 头
onProxyRes: (proxyRes, req, res) => {
res.header('Access-Control-Allow-Origin', '*')
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, PATCH, OPTIONS')
res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization, x-env-target')
},
// 动态路由:根据请求头决定转发目标
router: req => {
const envTarget = req.headers['x-env-target']
if (envTarget && typeof envTarget === 'string') {
return envTarget
}
throw new Error('Missing X-Env-Target header')
}
})
app.use('/api', proxy)
app.get('/health', (req, res) => {
res.json({ status: 'ok', mode: 'dynamic-routing' })
})
const PORT = process.env.PORT || 3001
app.listen(PORT, () => {
console.log(`Dynamic Proxy Server running on port ${PORT}`)
})
3.2.3 踩坑记录
第一次调试时,我们发现 OPTIONS 预检请求也被转发到了目标服务器,导致 CORS 错误。后来才明白,浏览器在发送正式请求前会先发 OPTIONS 请求确认跨域权限,这个请求需要在代理层直接返回,不能转发。
另外,Access-Control-Max-Age 设为 86400(24 小时)很重要,这样浏览器会缓存预检结果,避免每个请求都发 OPTIONS,能明显提升性能。
3.3 多用户并发支持
这个方案最爽的一点是天然支持多用户并发开发不同环境。
用户 A (Chrome) → localStorage A → X-Env-Target: 开发环境
用户 B (Safari) → localStorage B → X-Env-Target: 测试环境
用户 C (Firefox) → localStorage C → X-Env-Target: UAT 环境
隔离原理很简单:
- localStorage 在每个浏览器里独立存储
- 每个请求携带自己的环境信息
- 代理服务器无状态,根据每个请求独立转发
我们团队 5 个人同时开发,有人测开发环境,有人测测试环境,有人测 UAT,互不影响。
4. 使用流程
-
启动代理服务器
npm run proxy -
启动开发服务器
npm run dev -
登录页面选择环境(开发/测试/UAT 等)
-
打开 DevTools 查看请求
Request URL: http://localhost:3001/api/xxx Request Headers: X-Env-Target: https://test-api.example.com -
查看代理日志
[Proxy] GET /api/closingReport/getAllList -> https://test-api.example.com/api/closingReport/getAllList [Proxy] Response status: 200
5. 这个方案适合什么场景
我们用了半年,感觉这些场景特别合适:
- 开发阶段:需要频繁切换多个开发环境或分支
- 联调阶段:前端需要连接不同环境的服务
- UAT 验证:需要临时切换到 UAT 环境验证功能
不太适合的场景:
- 性能要求极高的场景(代理会增加一点延迟)
- 需要严格权限控制的环境(请求头可以被伪造)
6. 还能怎么扩展
如果你有更多需求,可以考虑这些方向:
环境配置管理:把环境信息抽离成配置文件,支持变量模板,比如 {{BASE_URL}}/api/users
请求记录回放:拦截并保存历史请求,方便复现和调试问题
环境健康检查:定期检查各开发环境可用性,异常时自动切换或告警
7. 最后
这个方案在我们团队落地后,开发和调试阶段的环境切换从"改配置 - 重启 - 登录"的 5 分钟流程,变成了"点一下 - 刷新"的 10 秒操作。
当然,代理服务器本身也需要维护,偶尔会遇到一些边界情况。但总体来说,投入产出比很高,特别适合开发和调试阶段使用。