资讯动态

前后端本地联调实战:跨域、代理配置与调试技巧

发布时间:2026/10/1 1:22:28 来源:尧图企业网站定制
前后端本地联调一套开发环境里的连通术藏着团队90%的调试时间开发一个月的前后端分离项目前后端各自跑得飞快一联调就是一下午。这种场景我见过太多次了——后端说“我接口明明好的你用Postman试试”前端说“我页面明明好的你自己看控制台全是报错”。最后排查下来问题往往不是代码逻辑而是本地开发环境没有打通。前后端分离架构下前端跑在5173端口Vite默认或8080端口Webpack默认后端跑在8080或者8000端口两边不在同一个源浏览器直接发请求就会被同源策略拦在门外。这就是“本地联调”要解决的核心问题让你在本机开发时前端页面发出的请求能畅通无阻地到达后端服务并且正确处理返回的数据。这篇文章不聊概念直接给你一套能落地的联调方法论。我会告诉你本地联调到底在调什么、环境怎么准备、跨域报错怎么区分、代理配置怎么写才不会踩坑以及Cookie、文件上传、WebSocket这些高频场景要注意什么。适合刚接触前后端分离的入门者也适合正在做SpringBootVue、FastAPIVue这类项目但是一直被联调问题卡住的人。1. 本地联调的本质前端端口与后端端口之间的连通问题先搞清楚一个概念本地联调调的到底是什么很多人以为联调是“把前端代码和后端代码放在同一个项目里跑起来”这是错的。前后端分离项目里前端代码和后端代码在物理上是分开的各自有独立的服务进程。前端开发服务器负责托管静态资源、提供热更新后端开发服务器负责处理业务逻辑、读写数据库。联调要做的就是让浏览器里跑着的前端页面能够通过HTTP请求访问到后端服务。但这里有一个绕不开的坎——浏览器的同源策略。同源策略规定协议、域名、端口三者完全一致才叫同源否则就视为跨域。本地开发时前端Dev Server跑在http://localhost:5173后端服务跑在http://localhost:8080端口不同天然就是跨域。浏览器会拦截前端发出的跨域请求这就是你打开控制台看到Access to XMLHttpRequest ... has been blocked by CORS policy报错的根本原因。下表是常见的本地联调端口组合你可以对照自己的项目看看前端框架前端默认端口常见后端后端默认端口Vite5173Spring Boot8080Vue CLI8080FastAPI8000React CRA3000Django8000Next.js3000Node.js Express3001Nuxt3000Go Gin8080本地联调的经典场景有两种。一种是你只有一个纯前端页面后端接口部署在远端服务器上前端需要直接请求远端的API地址另一种是前后端都在本地开发后端刚写好一个接口你需要在本地快速验证前端能不能正确拿到数据并渲染到页面上。无论是哪种场景核心诉求都一样让前端页面发出去的请求“看起来”是同源的或者让后端主动“允许”跨域请求。知道了这一点后面所有的方案就都围绕这两条路展开。2. 联调前的环境准备先确认两端各自能跑起来再谈连通联调第一步不是改代理而是确认前后端各自的开发环境是健康的。这个顺序特别重要我在实际项目中见过太多次“联调不通过”结果排查半天发现是前端依赖装错了版本、后端根本没连上数据库。2.1 前端侧确认依赖完整、Dev Server能正常启动前端项目拿到手先执行依赖安装再启动开发服务器。# npm 项目 npm install npm run dev # pnpm 项目 pnpm install pnpm dev # yarn 项目 yarn install yarn dev这里有一个特别容易踩的坑不要在一个项目里混用不同的包管理器。今天用npm装依赖明天用pnpm装后天又用yarn最后node_modules里的依赖结构和lock文件对不上项目启动报各种奇怪的错。确定一个包管理器之后统一用它这是经验之谈。启动之后还要注意Node版本。Vite 4以上要求Node 14.18Vite 5要求Node 18Spring Boot前后端分离项目里前端如果用的是老版本Vue CLINode版本太高反而会报digital envelope routines::unsupported错误。要么用nvm切换Node版本要么在启动脚本里加NODE_OPTIONS--openssl-legacy-provider。Dev Server启动后浏览器直接访问前端地址页面能正常渲染出来前端侧就算通过了。2.2 后端侧确认服务启动无异常、接口本地可访问后端的启动方式取决于技术栈。Spring Boot用mvn spring-boot:run或者IDE直接启动FastAPI用uvicorn main:app --reload --port 8000Express直接node app.js。不管哪种启动成功后都有一个标志控制台出现类似Tomcat started on port(s): 8080或者Uvicorn running on http://0.0.0.0:8000的字样。启动无报错不等于接口一定可用。先用curl验证一下后端接口curl http://localhost:8080/api/v1/health如果返回了JSON数据说明后端服务正常。如果连不上优先检查三件事后端依赖的数据库、Redis等中间件是否已启动端口是否被占用Windows上尤其常见执行netstat -ano | findstr 8080查看后端配置文件里的数据库连接地址、账号密码是否正确2.3 最容易忽略的一点确认后端监听地址这里我要重点说一个极其隐蔽的问题很多后端框架默认只监听localhost当你用浏览器直接访问http://localhost:5173去请求http://localhost:8080时因为是同一个本机回环地址没问题。但如果你要联调的是局域网内另一台机器的后端服务比如后端跑在同事的电脑上地址是http://192.168.1.100:8080而后端只监听了localhost你的请求就会被拒绝。解决办法是在启动后端时显式指定监听0.0.0.0。Spring Boot在application.yml里配置server: address: 0.0.0.0 port: 8080FastAPI启动时指定--host 0.0.0.0。这样后端才能接收来自本机之外的其他机器请求局域网联调才走得通。3. 跨域报错与CORS先搞清楚报错到底是谁拦截的环境都正常了接下来就是联调核心环节——发请求。这时候大概率你会遇到跨域报错。很多人一看到CORS policy就冲去找后端加跨域配置这方向不一定错但你要先搞清楚这个报错到底是浏览器拦的还是后端主动拒绝的两者的排查路径完全不同。3.1 浏览器的同源策略与CORS机制同源策略是浏览器的安全机制。你从http://localhost:5173这个页面发出一个AJAX请求到http://localhost:8080浏览器发现端口不同就会检查后端返回的响应头里有没有Access-Control-Allow-Origin: http://localhost:5173。如果没有浏览器直接不把响应交给前端代码控制台报错。关键点在于浏览器并没有阻止请求发出后端也可能正常处理了请求并返回了数据只是浏览器在返回阶段把响应拦截了。这也是为什么你在Network面板里能看到这个请求状态码甚至是200但前端代码拿不到响应数据。CORS跨域资源共享机制本质上是后端在响应头里“授权”——告诉浏览器这个来源的页面可以读取我的响应。Spring Boot的典型写法是加一个配置类Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOrigins(http://localhost:5173) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true); } }FastAPI的写法是用CORSMiddlewarefrom fastapi.middleware.cors import CORSMiddleware app.add_middleware( CORSMiddleware, allow_origins[http://localhost:5173], allow_methods[*], allow_headers[*], allow_credentialsTrue, )3.2 两类报错的区分方法看Network面板的Preflight区分报错来源有一个非常有效的方法打开浏览器Network面板看有没有一个OPTIONS请求。CORS机制在跨域非简单请求比如带Content-Type: application/json的请求之前会先发一个OPTIONS预检请求。如果这个OPTIONS请求返回了403或者404大概率是后端没做跨域配置或者后端根本没处理OPTIONS请求如果OPTIONS返回了200但后续的真实请求响应头里没有Access-Control-Allow-Origin说明后端的CORS配置有问题。如果是浏览器拦截你在Network面板里能看到请求已经发出去了响应头里也能看到后端的数据但面板顶部会有红色警告。3.3 什么时候该用CORS、什么时候该用代理明白了CORS只是“授权”机制你就可以做出正确选型了。CORS方案适合后端服务已经部署到测试环境或生产环境前端本地开发直接请求远端接口。这时候你改不了Nginx配置只能在远端后端加CORS配置。代理方案适合前后端都在本地开发或者后端部署在你自己可控的服务器上。代理方案是前端Dev Server收到你的请求后由Dev Server转发给后端转发过程发生在服务器端浏览器感知不到天然不受同源策略限制。我个人的经验是本地联调优先用代理方案。原因有三个第一不用入侵后端代码后端不用为开发环境单独加CORS配置第二生产环境部署时通常也用Nginx做同源代理本地开发行为和线上行为一致不会出现“本地好好的上线就跨域”的问题第三代理方案可以灵活控制路径前缀对接多个后端服务时尤其方便。4. 代理配置的完整实战Vite与Webpack两套方案对照代理配置是本地联调里技术含量最高、也最容易出错的一步。写错一个正则调接口就是404漏掉一个changeOrigin后端验证Host头时直接拒绝。下面把Vite和Webpack两套主流前端构建工具的代理配置完整讲一遍。4.1 理解代理转发的核心路径重写先看一个最典型的场景后端接口路径是/v1/users前端开发时想用/api/v1/users来请求。为什么前端要加/api前缀因为生产环境下Nginx会拦截/api开头的请求并转发到后端服务让前端代码统一带/api前缀就能保证本地和生产环境的前端代码不需要做任何路径上的修改。代理配置要做的事情是前端请求http://localhost:5173/api/v1/usersDev Server收到后把请求转发给后端http://localhost:8080/v1/users。注意转发时要去掉/api前缀因为后端接口路径里没有这个前缀。4.2 Vite代理配置详细拆解Vite的代理配置在vite.config.js里代码如下// vite.config.js import { defineConfig } from vite import vue from vitejs/plugin-vue export default defineConfig({ plugins: [vue()], server: { host: 0.0.0.0, port: 5173, proxy: { /api: { target: http://localhost:8080, // 后端服务地址 changeOrigin: true, // 修改请求头中的Host为target rewrite: (path) path.replace(/^\/api/, ), // 移除路径前缀 } } } })每个参数的含义你必须吃透因为联调报错90%都出在这几个参数上target后端服务的完整地址。注意要带协议http://localhost:8080和http://127.0.0.1:8080在很多后端框架眼里是不同的Host优先保持一致。changeOrigin这个参数的作用是把发往后端的请求头里的Host字段改成target的域名。后端如果做了域名校验比如Spring Security里配置了允许的HostchangeOrigin不设成true就会被拒。rewrite路径重写函数。^/api是一个正则匹配路径开头的/api把它替换成空字符串。这里是最容易写错正则的地方后面我详细说。配置完成后前端代码里的请求路径就统一写成带/api前缀的相对路径// 前端请求封装的示例 import axios from axios const request axios.create({ baseURL: /api, // 统一使用相对路径 timeout: 15000, }) request.get(/v1/users).then(res { console.log(res.data) })4.3 Webpack / Vue CLI代理配置对照老项目还在用Vue CLI的话代理配置在vue.config.js里// vue.config.js module.exports { devServer: { port: 8080, proxy: { /api: { target: http://localhost:8081, // 注意前端8080后端8081 changeOrigin: true, pathRewrite: { ^/api: } } } } }Vite的rewrite函数和Webpack的pathRewrite对象实现的功能一样只是语法不同。Vite用函数更灵活Webpack用键值对更直白。4.4 最容易踩的坑proxy只匹配路径开头我就直接说最常见的翻车现场前端baseURL配的是/api后端接口是/api/v1/list。你想着代理配置里的/api匹配到了之后把/api前缀去掉但实际访问的接口路径变成了/v1/list——后端正则给你返回404。为什么因为代理配置的/api只是说“匹配以/api开头的路径”但它不会自动把/api去掉。你要通过rewrite或者pathRewrite做显式替换。同理如果后端接口本身路径里就带/api比如/api/v1/list本身就是一个完整路径你前端再配baseURL: /api就会变成/api/api/v1/listDouble前缀必然404。正确做法是先和后端确认清楚接口的完整路径再决定是否加前缀、加什么前缀。我的习惯是后端接口路径带/api的前端baseURL直接配根路径/后端接口路径不带/api的前端baseURL配/api再通过代理把前缀剥掉。核心原则是前后端在联调前先对齐路径前端代码里的路径和实际请求出去的路径必须一致。4.5 代理生效的验证方法配置完代理后先别急着在页面上操作。直接在浏览器访问一下代理后的路径试试http://localhost:5173/api/v1/users如果返回的是后端JSON数据说明代理生效。如果返回404检查是不是路径重写写错了如果返回504或者502检查target地址和端口是否正确后端有没有启动。这里有个小技巧改完vite.config.js后必须重启Dev ServerVite不会热重载代理配置。不重启直接刷新页面你会发现自己明明改了配置却不生效白白浪费时间。5. 联调中最磨人的几个细节Cookie、文件上传、WebSocket代理配置通了基础接口联调也正常了你以为就万事大吉了还早。Cookie、文件上传、WebSocket这三个场景是联调阶段真正的“拦路虎”。这三个坑我每个都踩过而且每次都能花掉大半天时间。5.1 Cookie跨域问题为什么登录接口通了但登录状态丢了前后端分离项目里最常见的一种认证方式是SessionCookie。后端登录接口验证通过后往响应头里写入Set-Cookie浏览器自动保存这个Cookie后续请求自动带上它。逻辑听起来简单联调时却经常出现一个诡异的现象登录接口明明返回成功了但紧接着访问需要登录的接口后端却说没登录。问题出在哪第一关是前端发出的请求必须携带Cookie。携带Cookie不是默认行为需要前端显式开启// axios 开启携带Cookie const request axios.create({ baseURL: /api, withCredentials: true, // 关键 })注意了withCredentials: true加上之后后端的CORS配置里allowedOrigins就不能写*了必须写具体的域名比如http://localhost:5173同时allowCredentials要设为true。这是第二关。第三关是浏览器策略问题。现代浏览器对Cookie有SameSite限制跨站的请求默认不携带Cookie。本地开发时前端是localhost:5173后端是localhost:8080浏览器认为这是SameSite同一站点问题不大。但如果你用IP地址而非localhost访问前端比如http://192.168.1.10:5173浏览器可能直接拒绝携带Cookie。我踩过一次这个坑后就养成了一个习惯本地联调统一用localhost访问前端地址不要用IP避免Cookie行为不一致。5.2 Token认证与代理的配合如果项目用的是Token认证JWT情况会简单很多。Token一般放在请求头Authorization里前端用axios拦截器统一添加service.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer ${token} } return config })这种方案没有跨域限制的问题只要代理配置正确请求头都会原样转发给后端。唯一要注意的是后端对Authorization头的解析逻辑以及401状态的统一处理——前端在响应拦截器里判断401时跳转登录页。5.3 文件上传二进制数据与Content-Type的坑文件上传接口联调时最常见的报错是后端返回“Missing Content-Type boundary”。原因是前端用axios上传文件时设置错了Content-Type。正确的写法是使用FormData并且不要手动设置Content-Type让浏览器自动生成带有boundary的完整Content-Type// 正确不设置Content-Type const formData new FormData() formData.append(file, file) formData.append(title, 测试文件) axios.post(/api/upload, formData, { headers: { // 这里不要写 Content-Type: multipart/form-data } })如果手动写成Content-Type: multipart/form-data浏览器会缺少boundary标记后端解析Multipart数据时直接报错。这是一个新手极易踩的坑而且报错信息不算直观排查要花不少时间。5.4 WebSocket连接代理转发与直连方案如果项目里有即时通讯、消息推送这类功能联调阶段WebSocket也是一个容易炸的点。本地开发时如果前端和后端WebSocket服务直接跨域连接ws://localhost:8080/ws浏览器控制台会报错。两种解法。一种是在代理配置里加上ws: true// Vite配置 proxy: { /ws: { target: ws://localhost:8080, ws: true, // 支持WebSocket代理 changeOrigin: true, } }另一种是前端直接连接后端的WebSocket地址但要求后端WebSocket服务配置了CORS支持。两种方案我都有实际使用过代理方案更统一因为前端可以继续使用相对路径避免在代码里硬编码IP和端口。这里我要分享一个实用技巧WebSocket联调时不要用浏览器调试面板观察消息因为WebSocket消息不会显示在Network面板的XHR/Fetch列表里。用后端的控制台日志确认连接是否建立用前端代码里的onmessage回调打印数据确认消息是否到达。两步一对照很快能定位是连接没建立还是消息没发出去。6. 并行开发的加速器Mock数据与接口规约联调联调前提是后端接口已经开发好了。但实际项目中前后端往往是并行开发的。前端页面写好了后端接口还在开发中这时候你就只能干等着吗当然不是。合理的做法是前端用Mock数据先跑起来后端接口完成后无缝切换。6.1 轻量级Mock方案写死数据不做额外依赖最轻量的Mock方式是前端代码里封装一个开关开发环境且Mock开关开启时请求直接返回本地伪造的数据。以axios为例// request.js const useMock import.meta.env.DEV localStorage.getItem(useMock) true async function request(config) { if (useMock mockData[config.url]) { return { code: 200, data: mockData[config.url] } } return axios(config).then(res res.data) } // mockData.js export const mockData { /api/v1/users: [ { id: 1, name: 张三, role: admin }, { id: 2, name: 李四, role: editor }, ], }这种方式的好处是零依赖、无额外配置适合小项目快速跑通坏处是Mock逻辑写在业务代码里容易污染正式代码。项目规模变大后我一般会引入更正式的Mock方案。6.2 中间层Mockdev server中间件挡在Dev Server和后端之间如果你用的是Webpack或者Vite可以利用Dev Server的中间件做接口Mock。Vite配置里可以用configureServer钩子// vite.mock.js export function mockPlugin() { return { name: mock-server, configureServer(server) { server.middlewares.use((req, res, next) { if (req.url /api/v1/users) { res.setHeader(Content-Type, application/json) res.end(JSON.stringify({ code: 200, data: [] })) return } next() }) } } }这种方式Mock代码和业务代码彻底分离关闭插件就恢复真实请求非常干净。6.3 接口规约比框架更重要的联调保障Mock数据只是权宜之计真正提升联调效率的是接口规约。我经历过的项目里联调最痛苦的不是技术问题而是“前端以为接口返回的是data字段后端返回的是result字段”“前端传userId后端要user_id”这类简单的字段不一致。一个合理的接口规约通常包含四部分规约项说明示例路径与方法接口的URL和HTTP方法GET /api/v1/users参数命名统一驼峰还是下划线前端传userId后端收userId响应结构统一的数据包裹格式{ code: 0, msg: ok, data: [...] }错误码业务错误码的统一约定40100代表登录过期我的习惯是每个项目在联调之前先拉前后端核心成员开一次“接口规约会”把所有接口的路径、参数、响应对齐一遍形成文档后再动手开发。这个动作能省下后面至少一半的联调时间。7. 从本地环境到测试环境Nginx反代与多环境配置本地联调跑通了接下来就是部署到测试环境、生产环境。这一步如果前面偷懒没规划好会有新的坑等着你。7.1 前端构建产物与Nginx的关系前端项目的构建产物是静态文件HTML、CSS、JS。部署时这些文件被放到Nginx的静态资源目录里Nginx对外提供访问。问题来了页面里的接口请求是相对路径/api/v1/users直接访问Nginx的/api路径时Nginx并不知道该把请求转发给谁。所以Nginx要配置一个反向代理把/api开头的请求转发到后端服务的地址。server { listen 80; server_name your-domain.com; location /api/ { proxy_pass http://127.0.0.1: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; } location / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; } }注意这里的proxy_pass末尾的/。如果写成proxy_pass http://127.0.0.1:8080;不带末尾斜杠请求/api/v1/users会被转发成/api/v1/users——后端还得让他后端自然404。如果写成proxy_pass http://127.0.0.1:8080/;带末尾斜杠请求/api/v1/users会被转发成/v1/users——前缀/api被替换成了/。这个逻辑和前面Vite代理的rewrite完全类似原理是相通的。7.2 多环境变量管理与联调的衔接本地开发时baseURL可以写死成/api但到了测试环境和生产环境API地址可能不一样。前端项目里一般通过环境变量来管理不同环境的配置。在Vite项目里创建三个环境变量文件# .env.development VITE_API_BASE/api VITE_MOCKtrue # .env.test VITE_API_BASE/api VITE_MOCKfalse # .env.production VITE_API_BASE/api VITE_MOCKfalse前端代码里统一用环境变量const request axios.create({ baseURL: import.meta.env.VITE_API_BASE || /api, })构建时通过--mode指定环境vite build --mode test # 打包测试环境 vite build --mode production # 打包生产环境这样本地联调、测试验证、生产上线共用一套前端代码只是构建时的环境不同API基础路径、Mock开关、日志级别等配置随之变化。既减少了联调阶段的切换成本也避免了在上线前夜改配置改到失眠。7.3 本地联调与CI/CD的配合流水线里的联调验证如果你的团队已经上了CI/CD流水线用Jenkins、GitLab CI或者GitHub Actions建议在部署流程里加一个“联调验证”的环节。我见过很多团队流水线里只有构建和部署接口能不能通全靠人肉测试上线后才发现接口路径对不上。一个简单的做法是部署完成后自动执行一个健康检查脚本#!/bin/bash # check_api.sh API_HOSThttp://your-test-env.com HEALTH_URL$API_HOST/api/v1/health response$(curl -s -o /dev/null -w %{http_code} $HEALTH_URL) if [ $response -eq 200 ]; then echo API健康检查通过 else echo API健康检查失败HTTP状态码: $response exit 1 fi在流水线的部署步骤后面挂上这个脚本接口不通流水线就失败问题在第一时间暴露而不是等到测试人员打开页面才发现一片红。写在最后的实际体会做了几年的前后端分离项目我最大的体会是联调这件事工具和配置只是表面真正的难点在于前后端之间的沟通与规矩。代理配不上的时候CORS报错看不懂的时候先冷静下来按文章里的思路一步步排查——先确认两端各自健康再抓包看请求有没有发出去、响应有没有回来最后检查路径和Header是否符合预期。整个排查链路走完绝大多数问题都能定位。再分享一个小技巧联调阶段前后端各自开一个终端窗口后端窗口盯着请求日志前端窗口打开浏览器DevTools的Network面板。出问题时先看后端日志里有没有收到请求——收到了说明是响应阶段或者前端解析阶段的问题没收到说明请求根本没到达后端问题出在前端代理或者CORS配置上。这个“先判断请求有没有到达后端”的思路能帮你省掉大量无效排查。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑