前端和后端都跑起来了,但它们之间可能连不上。跨域问题、接口路径对不上、数据格式不匹配……这一节讲前后端联调的完整流程。
一、联调前的检查清单
在开始联调前,确认以下条件都满足:
| 检查项 | 确认方式 |
|---|---|
| 后端启动 | 访问后端接口能返回数据 |
| 前端启动 | 浏览器能打开前端页面 |
| 接口地址 | 前端配置的API地址指向后端 |
| 端口不冲突 | 前端和后端端口不同 |
| 数据库有数据 | 至少有测试数据 |
| 登录功能 | 如果有登录,先测试登录接口 |
二、配置前端请求后端地址
1. 找到前端API配置
Vite项目 — 看.env.development:
VITE_API_BASE_URL=http://localhost:8080/api
Vue CLI项目 — 看vue.config.js里的proxy:
module.exports = {
devServer: {
port: 3000,
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
pathRewrite: { '^/api': '' }
}
}
}
}
2. 理解Proxy的作用
前端跑在3000端口,后端跑在8080端口,不同端口就是跨域。
Proxy的原理:前端请求发到前端服务器(3000端口),前端服务器把请求转发到后端(8080端口)。因为转发发生在服务器端,没有浏览器的跨域限制。
浏览器 → http://localhost:3000/api/user → 代理转发 → http://localhost:8080/api/user
3. 没有Proxy的情况
如果前端没配Proxy,直接用完整地址请求:
// src/utils/request.js
const baseURL = 'http://localhost:8080/api'
const instance = axios.create({
baseURL: baseURL,
timeout: 10000
})
这种情况后端必须配跨域(下面讲)。
三、跨域问题处理
什么是跨域
浏览器有个安全策略叫同源策略:A网站不能直接请求B网站的接口。
同源 = 协议相同 + 域名相同 + 端口相同。
前端:http://localhost:3000
后端:http://localhost:8080
端口不同 → 跨域
跨域报错长什么样
浏览器F12 → Console面板:
Access to XMLHttpRequest at 'http://localhost:8080/api/user' from origin
'http://localhost:3000' has been blocked by CORS policy:
No 'Access-Control-Allow-Origin' header is present on the requested resource.
F12 → Network面板,请求状态显示红色(CORS错误)。
解决方案1:前端Proxy(推荐)
最简单的方式,前端配代理,浏览器不报跨域。
// vite.config.js
export default {
server: {
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true
}
}
}
}
前端代码里请求时用相对路径:
axios.get('/api/user/list')
解决方案2:后端配置CORS
如果前端不方便配Proxy,后端加跨域配置。
方式1:全局配置(推荐)
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOriginPatterns("*")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.allowCredentials(true)
.maxAge(3600);
}
}
方式2:注解方式
在Controller类上加:
@CrossOrigin(origins = "*")
@RestController
@RequestMapping("/user")
public class UserController {
// ...
}
方式3:Filter方式
@Component
public class CorsFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletResponse res = (HttpServletResponse) response;
res.setHeader("Access-Control-Allow-Origin", "*");
res.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
res.setHeader("Access-Control-Allow-Headers", "*");
res.setHeader("Access-Control-Max-Age", "3600");
chain.doFilter(request, response);
}
}
解决方案3:Nginx反向代理(进阶)
生产环境一般用Nginx统一入口:
server {
listen 80;
server_name localhost;
# 前端
location / {
root /usr/share/nginx/html;
try_files $uri $uri/ /index.html;
}
# 后端API
location /api/ {
proxy_pass http://localhost:8080/;
}
}
四、联调流程
1. 先测后端接口
用Postman或IDEA HTTP Client单独测试后端接口,确认接口能正常返回数据。
GET http://localhost:8080/api/user/list
返回:
{
"code": 200,
"message": "success",
"data": [
{"id": 1, "username": "admin", "email": "admin@test.com"},
{"id": 2, "username": "zhangsan", "email": "zs@test.com"}
]
}
2. 再测前端页面
浏览器打开前端页面(如http://localhost:3000),看页面能否正常显示。
3. 打开浏览器开发者工具
按F12打开开发者工具,关注以下面板:
Console面板:
- 看有没有红色报错
- 看有没有CORS跨域错误
Network面板:
- 看请求有没有发出去
- 看请求URL对不对
- 看响应状态码
- 看响应数据对不对
Application面板:
- 看Cookie和Token存了没有
- 看Local Storage里的数据
4. 测试完整流程
以用户管理为例:
步骤1:登录
- 在登录页输入账号密码
- 点登录
- F12 → Network,找到登录请求
- 确认请求参数正确
- 确认返回了Token
- 确认Token被保存到Local Storage或Cookie
步骤2:访问需要认证的页面
- 跳转到用户列表页
- F12 → Network,找到列表请求
- 确认请求头带了Token
- 确认返回了用户列表数据
- 确认页面上显示了数据
步骤3:增删改测试
- 新增用户 → 确认列表多了一条
- 编辑用户 → 确认数据更新了
- 删除用户 → 确认列表少了一条
五、常见联调问题
问题1:前端请求404
原因: 前端请求的URL和后端接口路径对不上。
排查:
- F12 → Network → 找到404的请求
- 看Request URL:
http://localhost:8080/api/user/list - 对比后端Controller的路径:
@RestController
@RequestMapping("/api/user") // 后端路径
public class UserController {
@GetMapping("/list") // 完整路径:/api/user/list
public Result list() { ... }
}
- 确认前端请求路径和后端路径一致
常见原因:
- 前端多加了或漏掉了
/api前缀 - context-path没配对
- 路径大小写不一致
问题2:前端请求500
原因: 后端代码报错了。
排查:
- F12 → Network → 找到500的请求
- 看Response,可能有错误信息
- 看后端控制台日志,找到报错的堆栈
- 根据报错信息定位问题
常见原因:
- 空指针异常(最常见)
- 参数没传或格式不对
- 数据库查询出错
问题3:参数没传到后端
前端传了参数但后端收不到。
排查:
- F12 → Network → 看请求的Payload(POST)或Query Params(GET)
- 看参数名和后端接收的参数名是否一致
GET请求参数:
// 前端
axios.get('/api/user/list', {
params: { page: 1, size: 10 }
})
// URL: /api/user/list?page=1&size=10
// 后端
@GetMapping("/list")
public Result list(@RequestParam Integer page, @RequestParam Integer size) {
// 参数名page、size必须和前端传的一致
}
POST请求JSON:
// 前端
axios.post('/api/user/add', {
username: 'zhangsan',
password: '123456'
})
// 后端
@PostMapping("/add")
public Result add(@RequestBody UserDTO dto) {
// UserDTO的属性名要和前端JSON的key一致
// UserDTO需要有无参构造和getter/setter
}
问题4:Token没带上
登录成功了,但后续请求返回401(未授权)。
排查:
- F12 → Application → Local Storage,看Token存了没有
- F12 → Network → 看后续请求的Headers里有没有
Authorization
如果没有Token:
检查前端拦截器是否正确设置:
// src/utils/request.js
import axios from 'axios'
const instance = axios.create({
baseURL: '/api',
timeout: 10000
})
// 请求拦截器:自动添加Token
instance.interceptors.request.use(config => {
const token = localStorage.getItem('token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
})
// 响应拦截器:处理401
instance.interceptors.response.use(
response => response.data,
error => {
if (error.response.status === 401) {
// Token过期,跳转登录
localStorage.removeItem('token')
window.location.href = '/login'
}
return Promise.reject(error)
}
)
export default instance
问题5:数据格式不匹配
前端期望的数据格式和后端返回的不一致。
后端返回:
{
"code": 200,
"data": {
"list": [...],
"total": 100
}
}
前端期望:
{
"list": [...],
"total": 100
}
解决: 统一前后端数据格式,一般后端包一层:
// 后端统一返回格式
@Data
public class Result<T> {
private Integer code;
private String message;
private T data;
public static <T> Result<T> success(T data) {
Result<T> r = new Result<>();
r.setCode(200);
r.setMessage("success");
r.setData(data);
return r;
}
}
前端在响应拦截器里统一处理:
instance.interceptors.response.use(response => {
const res = response.data
if (res.code === 200) {
return res.data // 只返回data部分
} else {
alert(res.message)
return Promise.reject(new Error(res.message))
}
})
问题6:日期格式不一致
后端返回日期格式和前端期望的不一致。
后端返回: 2026-01-01T08:00:00.000+08:00
前端期望: 2026-01-01 08:00:00
解决: 后端统一日期格式:
// 全局配置
@Configuration
public class JacksonConfig {
@Bean
public Jackson2ObjectMapperBuilderCustomizer customizer() {
return builder -> {
builder.simpleDateFormat("yyyy-MM-dd HH:mm:ss");
builder.timeZone("Asia/Shanghai");
};
}
}
// 或在字段上注解
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
private Date createTime;
问题7:OPTIONS预检请求被拦截
跨域请求时浏览器会先发OPTIONS请求(预检),后端没处理就直接返回401。
解决: 后端放开OPTIONS请求:
// 在安全配置里放开OPTIONS
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and()
.authorizeRequests()
.antMatchers(HttpMethod.OPTIONS).permitAll() // 放开OPTIONS
.antMatchers("/login").permitAll()
.anyRequest().authenticated();
}
六、调试技巧
1. 后端打断点
在Controller方法里打断点,前端发请求时后端会暂停:
- 在方法左边栏点一下,出现红点
- 前端触发请求
- 后端停到断点处
- F8单步执行,F9继续
- 查看变量值,确认参数是否正确
2. 前端打断点
F12 → Sources → 找到对应的JS文件 → 断点 → 触发请求 → 逐步调试。
或者在代码里加debugger:
async function getUserList() {
debugger // 代码执行到这里会暂停
const res = await request.get('/api/user/list')
console.log(res)
}
3. Console.log大法
前端最直接的调试方式:
console.log('请求参数:', params)
const res = await request.post('/api/user/add', params)
console.log('返回结果:', res)
4. 后端打印SQL
确认后端执行的SQL是否正确:
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
控制台会输出每条SQL语句和参数。
5. Charles/Fiddler抓包
如果前端请求不知道发了什么,用抓包工具看完整的请求和响应。
七、联调完成检查清单
| 功能 | 测试点 | 结果 |
|---|---|---|
| 登录 | 输入正确账号密码,能登录 | ✅ |
| 登录 | 输入错误密码,提示错误 | ✅ |
| 列表 | 分页、排序、搜索正常 | ✅ |
| 新增 | 数据能保存到数据库 | ✅ |
| 编辑 | 修改后数据更新 | ✅ |
| 删除 | 删除后列表刷新 | ✅ |
| 权限 | 未登录访问跳转登录页 | ✅ |
| Token过期 | 过期后跳转登录 | ✅ |
| 跨域 | 无CORS报错 | ✅ |
小结
| 问题 | 解决方案 |
|---|---|
| 跨域 | 前端Proxy或后端CORS配置 |
| 404 | 检查前后端路径是否一致 |
| 500 | 看后端日志找报错 |
| 参数丢失 | 检查参数名和格式 |
| 401 | 检查Token是否带上 |
| 数据格式不匹配 | 统一前后端返回格式 |
前后端联调通了,整个项目就能本地完整运行了。