摘要:前后端各自写得好好的,一连调就炸了——跨域报红、图片全 404、时间差了 8 小时、Token 传不上去……今天这篇就是来解决这些"联调地狱"的。从请求封装、Token 自动携带,到商城真实踩过的图片 URL 大坑,再到 CORS 配置和 AI 辅助 Mock 数据,一套组合拳打完,前后端彻底打通。成本?依然是 0。
前后端分离,联调才是真 Boss
做全栈项目,写后端接口不难,写前端页面也不难。
真正难的是:让它俩对上话。
后端 DRF 的 API 写了几十个,前端 Vue 页面也搭得七七八八。结果一开始联调,好家伙——
- 浏览器控制台一片红,
CORS policy报错 - 商品列表加载出来了,图片全是裂的,404
- 用户登录成功,Token 拿到了,但下一个请求又 401 了
- 订单创建时间差了 8 小时,后端显示的是 UTC
- 前端期望
data里直接是列表,后端返回的是{ code: 0, data: { results: [] } }
每一个,都是真金白银踩出来的坑。
所以今天这篇,不写新功能了。集中精力解决联调这件事——把前后端之间的"任督二脉"彻底打通。
搞完今天的内容,你会发现:联调其实不难,难的是没有提前把规范定好。
联调到底在调什么
说到底,前后端联调就是HTTP 请求的协商。
前端说:“我要数据,格式是这样的,地址在那个位置。”
后端说:“好的,数据给你,格式是这样的,你看看能不能用。”
问题就出在:两边说的不是同一种话。
| 维度 | 前端期望 | 后端实际 |
|---|---|---|
| 请求地址 | /api/v1/goods/ | /api/v1/goods/(✅ 对上了) |
| 认证方式 | Authorization: Token xxx | DRF Token Auth(✅ 也对上了) |
| 响应格式 | { code, message, data } | 有些接口返回 DRF 默认格式 |
| 图片路径 | /media/goods/xxx.jpg | goods/xxx.jpg(❌ 少了/media/) |
| 时间格式 | 2025-07-15 10:00:00 | 2025-07-15T02:00:00Z(❌ UTC) |
| 跨域 | 前端localhost:5173 | 后端localhost:8000(❌ 端口不同) |
地址和认证基本没问题(因为一开始就定好了),但数据格式、图片路径、时间格式、跨域这四个,是联调阶段最容易翻车的。
解决思路很清晰,联调三板斧:
① 统一请求层 → BaseURL、Token、错误处理,全部封装 ② 修数据格式 → 图片 URL 补全、时间转换、响应结构统一 ③ 通网络层 → CORS 跨域配置、代理转发、接口联调动手实战
第一步:封装请求层
联调的第一步,不是急着对接口,而是把请求层封装好。
为什么?因为如果你在每个组件里都写一遍axios.get('/api/v1/goods/'),等到 Token 过期、BaseURL 变更、错误处理要改的时候——你得改几十个文件。
封装一次,处处受益。
// src/utils/request.jsimportaxiosfrom'axios'constservice=axios.create({// BaseURL:开发环境走 Vite 代理,生产环境走 Nginx,最终都是 http://后端/api/v1/xxxbaseURL:import.meta.env.VITE_API_BASE_URL||'/api/v1',timeout:30000,// 超时 30 秒(图片上传场景留够时间)})// ---- 请求拦截器:自动携带 Token ----service.interceptors.request.use((config)=>{// 从 localStorage 拿 Token(用有辨识度的 key,避免和其他项目冲突)consttoken=localStorage.getItem('shop_token')if(token){config.headers['Authorization']=`Token${token}`}returnconfig},(error)=>Promise.reject(error))// ---- 响应拦截器:统一错误处理 ----service.interceptors.response.use((response)=>{constres=response.data// 全局约定 { code: 0, message, data } —— code 0 业务成功,直接返回 dataif(res.code!==0){if(res.code===401){// Token 过期 → 清 Token 踢回登录页localStorage.removeItem('shop_token')router.push('/login')}returnPromise.reject(newError(res.message))}returnres.data// 调用方不用写 response.data.data 这种反人类嵌套},(error)=>{conststatus=error.response?.status// 400/401/403/404/500 分别给提示,401 额外清 Token 跳登录returnPromise.reject(error)})exportdefaultservice有了这个封装,业务代码干净利落:
importrequestfrom'@/utils/request'constgoodsList=awaitrequest.get('/goods/')// 不用手动加 Tokenconstorder=awaitrequest.post('/orders/',{...})// 错误自动处理第二步:Token 管理
Token 的管理就两个动作:登录时存、请求时带。
// 登录成功constdata=awaitlogin(username,password)localStorage.setItem('shop_token',data.token)// 存 TokenlocalStorage.setItem('user_info',JSON.stringify(data.user))// 登出localStorage.removeItem('shop_token')localStorage.removeItem('user_info')router.push('/login')为什么用
shop_token这个 key?避免和其他项目冲突。浏览器里同时开着多个本地项目,token这个 key 很可能被覆盖。起个有辨识度的名字省心。
第三步:fixImageUrl——血泪教训
这个函数,是商城联调时踩得最深的一个坑。
事情是这样的:商品详情页写好了,后端接口也通了,数据也返回了。一打开页面——
所有商品图片全是裂的。
打开控制台一看,图片请求的地址是goods/product_01.jpg。但 Django 的 media 文件服务挂在/media/路径下,正确地址应该是/media/goods/product_01.jpg。
少了个/media/前缀,所有图片 404。
为什么?因为 Django 的ImageField返回的path属性是相对于MEDIA_ROOT的路径——它不知道(也不关心)你对外暴露的 URL 前缀是什么。
# Django 后端返回的数据{"id":1,"name":"蓝牙耳机","main_image":"goods/2025/earphone.jpg",# ← 少了 /media/ 前缀!}解决思路:前端加一个中间层,自动给图片路径补上/media/前缀。
/** * fixImageUrl - 图片 URL 修复函数 * 问题:Django ImageField 返回 "goods/xxx.jpg",前端需要 "/media/goods/xxx.jpg" * 规则: * 1. 空值 → 返回默认占位图 * 2. http(s):// 开头 → 原样返回(CDN/外部图片) * 3. /media/ 开头 → 原样返回(已是正确路径) * 4. goods/orders/avatars/ 开头 → 自动补 /media/ 前缀 */exportfunctionfixImageUrl(url,fallback='/images/placeholder.png'){if(!url||url.trim()==='')returnfallback// 空值保护url=url.trim()if(url.startsWith('http://')||url.startsWith('https://'))returnurlif(url.startsWith('/media/'))returnurl// 已带前缀// 以已知上传目录开头 → 补 /media/return/^(goods|orders|avatars|banners)\//.test(url)?`/media/${url}`:url}批量修复整个商品对象(单值字段 + 数组字段)也封装成一个函数:
exportfunctionfixImageFields(item,options={}){const{imageFields=['main_image','cover','avatar'],arrayFields=['detail_images','images','gallery']}=optionsconstfixed={...item}imageFields.forEach((f)=>{if(fixed[f]!==undefined)fixed[f]=fixImageUrl(fixed[f])})arrayFields.forEach((f)=>{if(Array.isArray(fixed[f]))fixed[f]=fixed[f].map(fixImageUrl)})returnfixed}教训:这个问题其实在后端就能解决——在 Serializer 里用
SerializerMethodField返回完整 URL 更干净。但前期没注意,等前端写了十几个页面才发现问题,改后端来不及了,只好在前端兜底。所以联调越早越好,拖到后面改的成本成倍增长。
第四步:CORS 跨域配置
跨域是前后端分离项目的"必经之路"。
原理:浏览器的同源策略要求协议 + 域名 + 端口三者完全一致。开发环境前端跑localhost:5173,后端跑localhost:8000,端口不同,就是跨域。
后端 Django 配django-cors-headers:
# settings.pyINSTALLED_APPS=[...'corsheaders']MIDDLEWARE=['corsheaders.middleware.CorsMiddleware',...]# 尽量靠前# 开发环境放开,生产环境必须写具体域名CORS_ALLOWED_ORIGINS=["http://localhost:5173",# Vite 开发服务器"https://www.yourmall.com",# 生产域名]CORS_ALLOW_CREDENTIALS=TrueCORS_ALLOW_HEADERS=['authorization','content-type','x-csrftoken']前端 Vite 代理(开发环境推荐方案):
// vite.config.jsserver:{port:5173,proxy:{'/api':{target:'http://localhost:8000',changeOrigin:true},// 接口转发'/media':{target:'http://localhost:8000',changeOrigin:true},// 图片转发},}这样前端请求/api/v1/goods/会被 Vite 转发到后端,浏览器以为自己在跟同源服务器通信,跨域问题直接消失。
联调经验:开发环境用 Vite 代理、生产环境用 Nginx 反向代理,基本不会有跨域问题。真正需要
django-cors-headers的场景是前后端部署在不同域名下。但不管怎样,后端配好 CORS 是兜底保障。
第五步:联调试错实录
坑 1:接口格式不一致
部分接口返回 DRF 默认格式,部分返回统一格式{ code, data }。前端一会儿res.data.results,一会儿res.data,代码写得跟猜谜一样。
解决方案:后端统一中间件——写一个 DRF 自定义 Renderer,所有响应强制包装成统一格式:
# utils/renderers.pyfromrest_framework.renderersimportJSONRendererclassUnifiedJSONRenderer(JSONRenderer):"""统一响应格式:{ code: 0, message: 'success', data: ... }"""defrender(self,data,accepted_media_type=None,renderer_context=None):response=renderer_context.get('response')ifrenderer_contextelseNoneifresponseandresponse.status_code>=400:result={'code':response.status_code,'message':data.get('detail','请求失败')ifisinstance(data,dict)elsestr(data),'data':None}else:result={'code':0,'message':'success','data':data}returnsuper().render(result,accepted_media_type,renderer_context)# settings.pyREST_FRAMEWORK={'DEFAULT_RENDERER_CLASSES':['utils.renderers.UnifiedJSONRenderer'],}一个 Renderer,所有接口格式统一。前端再也不用猜了。
坑 2:时区差了 8 小时
后端数据库存的是 UTC 时间,API 返回created_at是2025-07-15T02:00:00Z。前端直接显示——凌晨 2 点创建的订单?不对啊,明明是上午 10 点下的单。
# settings.pyTIME_ZONE='Asia/Shanghai'# 改成中国时区USE_TZ=True# 数据库继续存 UTC,序列化时自动转北京时间AI 加速:Mock 数据 & 联调测试
联调的时候有个尴尬的时期:后端接口还没写完,前端就要开始调了。
怎么办?用 Mock 数据先跑着。以前手写 Mock 数据要半天,现在直接让 AI 生成:
请帮我生成 10 条电商商品 Mock 数据,JSON 格式,字段包括: id, name, price(10-999随机), main_image(用 picsum.photos 占位图), category(从"数码/服饰/食品/家居"随机选), stock(0-500随机), description(50字以内的中文描述), created_at(最近30天内的ISO时间) 要求: 1. 有一个商品库存为 0(测试售罄状态) 2. 有一个商品价格是 0.01(测试边界值) 3. main_image 故意留一个空字符串(测试 fixImageUrl)AI 几秒钟就生成好,还贴心地留了测试点。再让 AI 写一个联调冒烟测试脚本:自动遍历所有接口,检查返回类型对不对、图片 URL 修没修好、Token 有没有带上,输出通过/失败统计。
AI 加速心得:联调阶段的很多"体力活"——Mock 数据生成、测试脚本编写、错误日志分析——都可以交给 AI。把省下来的时间用在理解业务逻辑和排查真正的问题上。
学完来练练手
- 给请求拦截器加"请求去重":同一个接口短时间内不要重复发送(防止用户连点提交按钮)
- 给
fixImageUrl加 CDN 域名替换:如果配置了 CDN,把/media/前缀替换成 CDN 地址 - 写一个响应拦截器的"loading 计数器":有请求在飞时显示全局 loading,全部完成后隐藏
配套文件day18_request_utils.js(请求封装、fixImageUrl、批量修复、冒烟测试脚本)可参考学习。
本系列为梅雅达编程笔记原创,首发于 CSDN。有问题?评论区见 🍃