前后端分离的项目里,Vue应用通常跑在本地开发服务器的某个端口上,例如 http://localhost:5173,而接口服务可能部署在 http://api.ipipp.com。浏览器同源策略会拦截不同源之间的响应读取,这就是控制台里频繁出现 CORS 报错的根本原因。很多团队处理跨域时习惯在后端临时返回通配符 Access-Control-Allow-Origin: *,开发阶段确实省事,但到了测试、预发、生产等多个环境,接口域名各不相同,前端如果把地址写死,切换环境就要改代码,容易出错也不利于自动化发布。合理的做法是让接口域名根据环境动态注入,开发环境同时配合代理绕过浏览器限制,生产环境再结合 Nginx 或后端 CORS 白名单完成闭环。

一、跨域的本质与开发环境代理思路
浏览器同源策略要求页面脚本只能读取同源响应。这里的同源指的是协议、域名、端口三者完全一致,例如 http://localhost:5173 和 http://localhost:3000 端口不同,就属于跨域。很多开发者误以为跨域是请求没有到达服务器,实际上大多数情况下后端已经正常处理了请求,只是浏览器在拿到响应后发现响应头里没有允许当前源读取的字段,于是把结果丢弃,并在控制台打印 CORS 错误。对于简单 GET、POST 请求,浏览器不会发送预检请求;但对于携带自定义请求头、使用 PUT 或 DELETE 方法、或 Content-Type 为 application/json 的请求,浏览器会先发送一个 OPTIONS 预检请求,确认服务器明确允许这些方法后才继续真实请求。
Vue 官方脚手架内置的开发服务器底层都是 Node HTTP 服务,可以非常方便地开启 proxy 代理。代理的核心思路是:前端代码只请求当前开发服务器的相对路径,例如 /api/user,这个请求对浏览器而言是同源的,因此不会触发跨域限制。开发服务器收到请求后,在 Node 层把请求转发到真正的后端地址,例如 http://localhost:3000/api/user,拿到响应后再返回给浏览器。这样浏览器从头到尾只跟同一个开发服务器通信,跨域问题自然就不存在了。
先创建环境变量文件,把开发环境的接口前缀和目标地址分开管理:
# .env.development VITE_API_BASE_URL=/api VITE_TARGET_ORIGIN=http://localhost:3000
接着在 Vite 的配置文件中根据变量启用代理:
import { defineConfig, loadEnv } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd(), '')
return {
plugins: [vue()],
server: {
proxy: {
'/api': {
target: env.VITE_TARGET_ORIGIN,
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
}
}
}
}
})
这里有两个配置非常关键。changeOrigin 会把转发请求的 Host 头改成目标服务器的域名,避免某些后端根据 Host 做校验时拒绝请求。rewrite 则负责路径重写,如果后端接口本身不带 /api 前缀,就需要把请求路径中的 /api 去掉,否则会出现 404。开发代理只对本地开发环境有效,打包后的静态文件不会经过 Vite 开发服务器,因此生产环境还需要单独处理。
二、多环境动态域名配置的核心实现
不同环境接口域名不同,如果让开发、测试、预发和生产共用一份构建配置,就需要把接口地址抽离成环境变量。Vite 项目默认会读取项目根目录下的 .env、.env.development、.env.production 等文件,只有以 VITE_ 开头的变量才会暴露给客户端代码。读取方式统一为 import.meta.env.VITE_XXX。如果项目使用的是 Vue CLI,则变量前缀是 VUE_APP_,代码中通过 process.env.VUE_APP_XXX 读取,两者思路完全一致,只是前缀不同。
以 Vite 项目为例,分别给开发、测试和生产定义不同的接口地址:
# .env.development VITE_API_BASE_URL=/api # .env.test VITE_API_BASE_URL=https://test-api.ipipp.com # .env.production VITE_API_BASE_URL=https://api.ipipp.com
然后在 axios 实例中读取该变量,不要在任何组件里直接写接口地址,这样就能实现按构建模式自动切换:
import axios from 'axios'
const service = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL,
timeout: 10000
})
service.interceptors.request.use(config => {
const token = localStorage.getItem('token')
if (token) {
config.headers.Authorization = 'Bearer ' + token
}
return config
})
service.interceptors.response.use(
response => response.data,
error => Promise.reject(error)
)
export default service
环境变量方案适合每个环境单独构建的场景,但如果你想用同一份构建产物部署到多个环境,就需要引入运行时配置。可以把配置文件放到 public 目录,这样构建时不会被 Vite 打包处理,部署时由运维或脚本替换其中的配置。文件内容可以形如:
// public/config.js
window.__APP_CONFIG__ = {
API_BASE_URL: '/api'
}
之后在 index.html 中通过 <script src="/config.js"></script> 引入,axios 实例读取时优先使用运行时配置,找不到再退回环境变量:
const runtimeBaseURL = window.__APP_CONFIG__?.API_BASE_URL
const fallbackBaseURL = import.meta.env.VITE_API_BASE_URL
const service = axios.create({
baseURL: runtimeBaseURL || fallbackBaseURL,
timeout: 10000
})
这种做法的好处是同一份包可以在不同环境直接部署,不需要反复打包,也方便测试同学在部署机器上临时调整接口地址。但它也有一个前提:配置文件必须在 Vue 应用加载前完成加载,否则 window.__APP_CONFIG__ 是 undefined。因此引入脚本时不要使用异步加载,也不要把配置文件放在会被 Hash 路由影响的路径里。两种方案可以按团队发布流程选择,业务复杂时也可以同时保留,环境变量作为构建时默认值,运行时配置作为兜底覆盖。
三、生产环境配合 Nginx 与常见注意事项
开发环境可以靠代理绕过跨域,但生产环境需要更彻底的处理方式。如果前端静态资源和后端接口可以部署在同一个域名下,最推荐的做法是用 Nginx 托管前端文件,并把 /api 路径反向代理到后端服务。浏览器仍然只跟同一个源通信,从架构上直接消除跨域问题。一个基础的配置如下:
server {
listen 80;
server_name www.ipipp.com;
location / {
root /usr/share/nginx/html;
try_files $uri $uri/ /index.html;
}
location /api/ {
proxy_pass http://backend-server:8080/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
如果前后端确实必须使用不同域名,比如前端在 www.ipipp.com,接口在 api.ipipp.com,那就需要后端配置 CORS 响应头。以 Node.js 后端为例,可以统一在中间件里设置允许的前端源:
app.use((req, res, next) => {
res.setHeader('Access-Control-Allow-Origin', 'https://www.ipipp.com')
res.setHeader('Access-Control-Allow-Credentials', 'true')
res.setHeader('Access-Control-Allow-Methods', 'GET,POST,PUT,DELETE,OPTIONS')
res.setHeader('Access-Control-Allow-Headers', 'Content-Type,Authorization')
if (req.method === 'OPTIONS') {
return res.sendStatus(204)
}
next()
})
实际落地时还有几个高频坑要留意。第一,修改 .env 文件后必须重启开发服务器,否则 Vite 不会重新加载环境变量。第二,如果 axios 的 baseURL 配置成了完整的绝对地址,例如 http://localhost:3000,请求就不会匹配 Vite 的代理规则,跨域依然存在。第三,前端如果开启了 withCredentials 携带 Cookie,后端不能使用 Access-Control-Allow-Origin: *,必须明确指定来源域名,并且同时返回 Access-Control-Allow-Credentials: true。第四,预检请求会被浏览器缓存,修改 CORS 配置后如果仍然报错,可以先尝试无痕窗口或强制刷新。第五,Nginx 中 location /api/ 和 proxy_pass 末尾的斜杠会直接影响最终拼接路径,出现 404 或 403 时优先检查路径是否重复或缺失。第六,线上页面使用 HTTPS 时,接口地址绝对不能使用 HTTP,否则浏览器会直接拦截混合内容请求,这已经不是跨域问题,而是安全策略问题。
动态配置域名的最终目的不是单纯解决某一个环境的跨域报错,而是让 Vue 项目在不同环境中都能使用一套稳定的请求层代码。环境变量负责构建时切换,运行时配置负责部署时调整,开发代理负责本地调试,生产环境则通过 Nginx 同源转发或后端 CORS 白名单兜底。把这些环节组合起来,才能让多环境开发、发布和排错都变得可控。