前后端联调:代码都没错,怎么就跑不起来?
前后端联调:代码都没错,怎么就跑不起来?
技术不玄学 · 前端开发手记 02
前端开发中,有一个相当微妙的阶段。
页面写完了,按钮能点,弹窗能开,表格也排列得整整齐齐。
你对自己的工作很满意。
直到后端发来一句话:
“接口好了,可以联调了。”
从这一刻开始,项目进入了一个新的阶段。
你发现登录按钮点击没反应,列表数据加载不出来,提交表单提示参数错误。
而后端告诉你:“我这边测试是正常的。”
两边都正常,合在一起却不正常。
这就是前后端联调。
一、什么是前后端联调?

简单来说,前后端联调就是让前端页面和后端接口真正配合起来,验证整个业务流程能不能正常运行。
联调之前,前端可能使用模拟数据:
const user = { id: 1, name: '小明', avatar: '/default.png' };
这些数据很听话。你想让它叫什么,它就叫什么;你想让它有头像,它就有头像。
真实接口可能返回:
{
"code": 0,
"data": {
"id": 1,
"nickname": "小明",
"avatarUrl": null
}
}
你原本读取 user.name,后端返回的却是 nickname;你以为头像一定存在,结果得到 null。
模拟数据是你想象中的世界,联调才是项目真正生活的地方。
二、联调之前,先把接口文档看明白

假设我们需要实现用户登录:
POST /api/user/login
请求体(JSON):
{
"username": "admin",
"password": "123456"
}
响应示例:
{
"code": 0,
"message": "success",
"data": { "token": "example-token" }
}
至少要确认:
- 请求方法是
GET还是POST? - 参数放在 URL、请求体,还是请求头?
- 请求体使用 JSON 还是 FormData?
- 业务成功状态码是
0还是200? - Token 从哪里获取,后续如何携带?
计算机不会替你理解“差不多”。userName 和 username 可能就是两个不同的字段。
三、Network:联调现场最可靠的证人
打开浏览器开发者工具(通常按 F12),进入 Network → Fetch/XHR,重新触发请求。
重点检查四件事。
1. 请求发出去了吗?
如果 Network 中没有请求,先检查按钮事件、条件判断和调用链:
<template>
<button @click="handleLogin">登录</button>
</template>
<script setup>
function handleLogin() {
console.log('点击了登录按钮');
}
</script>
连日志都没出现,就先别急着找后端。请求还没出门呢。
2. 请求地址对吗?
你期望请求:
https://test.example.com/api/user
实际却请求:
http://localhost:5173/api/user
如果没有正确配置开发代理,请求可能根本没有到后端。
3. 参数对吗?
后端要求:
{ "pageNum": 1, "pageSize": 10 }
前端却发送:
{ "page": 1, "size": 10 }
双方的命名都挺合理,只是没有达成共识。
4. 响应说了什么?
HTTP 200 OK 不等于业务成功:
{
"code": 40101,
"message": "登录已过期",
"data": null
}
服务器成功地告诉你:登录失败了。这两件事并不矛盾。
四、同样是加载失败,原因可能完全不同
| Network 现象 | 优先排查方向 |
|---|---|
404 Not Found |
请求路径、开发代理、后端路由 |
200 OK,业务提示 Token 过期 |
鉴权请求头、Token 有效期 |
200 OK,records: [] |
查询条件、数据环境、空状态展示 |
| 请求显示 CORS 错误 | 预检请求、跨域响应头、代理配置 |
| 没有任何请求 | 点击事件、代码执行路径、请求是否被提前拦截 |
先拿到证据,再判断问题属于哪一层。
五、跨域:浏览器为什么不让请求过去?
前端运行在 http://localhost:5173,后端运行在 http://localhost:8080。端口不同,属于不同源。
浏览器会实施同源策略相关限制。如果后端没有正确配置 CORS,前端 JavaScript 就可能无法读取响应。
跨域不等于请求一定没到服务器。 有些请求已经到达后端,但响应缺少必要的 CORS 头,浏览器不允许前端代码读取。对于需要预检的请求,OPTIONS 预检失败后,实际业务请求可能根本不会发出。

本地开发可以使用 Vite 代理:
// vite.config.js
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
export default defineConfig({
plugins: [vue()],
server: {
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true
}
}
}
});
前端请求:
fetch('/api/user');
开发服务器会转发到后端。注意:开发代理不等于生产环境自动解决跨域,上线后仍需配置反向代理或后端 CORS。
不要把 mode: 'no-cors' 当作通用解决方案,它通常只会让你拿到无法读取内容的 opaque response。
就像有人告诉你快递已经送到了,却不允许你打开包装。
六、接口数据结构:小问题,大麻烦

后端返回:
{
"code": 0,
"data": {
"records": [{ "id": 1, "name": "小明" }],
"total": 1
}
}
如果使用 Axios,且请求封装直接返回原始 Axios 响应:
const response = await axios.get('/api/users');
const records = response.data.data.records;
第一个 data 是 Axios 响应体,第二个 data 是业务响应字段。
但如果项目里的请求拦截器已经返回 response.data,取值方式就要相应调整。
先确认拿到的是哪一层数据,再写取值代码。
七、Vue 页面不更新,不一定是接口的错
例如:
const list = ref([]);
async function loadData() {
const response = await getUserList();
list.value = response.data.records;
}
这段代码能否正常工作,取决于 getUserList() 返回的数据结构。
如果它返回业务响应体,这样写可能正确;如果它直接返回 records 数组,就不能再访问 response.data.records。
接口文档描述的是约定,运行时拿到的是事实。两者不一致时,先找出差异,再决定修改哪一边。
八、怎么跟后端高效沟通?
“接口有问题,你看看。”
这句话的信息量很有限。
不如这样说:
用户列表接口
/api/users,测试环境,GET 请求。请求参数:
pageNum=1&pageSize=10。HTTP 状态码为 200,业务 code 为 0。
但
data.records是 null,接口文档约定的是数组。麻烦确认一下,空数据场景是否应该返回
[]?
必要时附上脱敏后的请求与响应,不要在公共群里暴露真实 Token、Cookie、密码或用户隐私信息。
好的联调沟通,不是急着证明谁写错了,而是让问题尽快变得清楚。
九、前后端联调排查清单
写在最后

前后端联调,表面上是在检查接口,实际上是在验证一份双方共同遵守的约定。
一个字段名,一种数据类型,一条错误信息,甚至一个不起眼的请求头,都可能决定功能能否顺利运行。
下次遇到问题,不妨先打开 Network,看看请求究竟经历了什么。
比起猜测,证据通常更可靠。
而比起争论是谁的问题,尽快解决问题显然更重要。
毕竟,项目上线之后,用户不会关心这是前端的问题,还是后端的问题。

他们只会说:
“你们这个网站怎么打不开?”
《技术不玄学》· 松弛派技术手记

浙公网安备 33010602011771号