Day 18 | 前后端联调:打通任督二脉 摘要前后端各自写得好好的一连调就炸了——跨域报红、图片全 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 xxxDRF Token Auth✅ 也对上了响应格式{ code, message, data }有些接口返回 DRF 默认格式图片路径/media/goods/xxx.jpggoods/xxx.jpg❌ 少了/media/时间格式2025-07-15 10:00:002025-07-15T02:00:00Z❌ UTC跨域前端localhost:5173后端localhost:8000❌ 端口不同地址和认证基本没问题因为一开始就定好了但数据格式、图片路径、时间格式、跨域这四个是联调阶段最容易翻车的。解决思路很清晰联调三板斧① 统一请求层 → BaseURL、Token、错误处理全部封装 ② 修数据格式 → 图片 URL 补全、时间转换、响应结构统一 ③ 通网络层 → CORS 跨域配置、代理转发、接口联调动手实战第一步封装请求层联调的第一步不是急着对接口而是把请求层封装好。为什么因为如果你在每个组件里都写一遍axios.get(/api/v1/goods/)等到 Token 过期、BaseURL 变更、错误处理要改的时候——你得改几十个文件。封装一次处处受益。// src/utils/request.jsimportaxiosfromaxiosconstserviceaxios.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避免和其他项目冲突consttokenlocalStorage.getItem(shop_token)if(token){config.headers[Authorization]Token${token}}returnconfig},(error)Promise.reject(error))// ---- 响应拦截器统一错误处理 ----service.interceptors.response.use((response){constresresponse.data// 全局约定 { code: 0, message, data } —— code 0 业务成功直接返回 dataif(res.code!0){if(res.code401){// Token 过期 → 清 Token 踢回登录页localStorage.removeItem(shop_token)router.push(/login)}returnPromise.reject(newError(res.message))}returnres.data// 调用方不用写 response.data.data 这种反人类嵌套},(error){conststatuserror.response?.status// 400/401/403/404/500 分别给提示401 额外清 Token 跳登录returnPromise.reject(error)})exportdefaultservice有了这个封装业务代码干净利落importrequestfrom/utils/requestconstgoodsListawaitrequest.get(/goods/)// 不用手动加 Tokenconstorderawaitrequest.post(/orders/,{...})// 错误自动处理第二步Token 管理Token 的管理就两个动作登录时存、请求时带。// 登录成功constdataawaitlogin(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// 空值保护urlurl.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_CREDENTIALSTrueCORS_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_typeNone,renderer_contextNone):responserenderer_context.get(response)ifrenderer_contextelseNoneifresponseandresponse.status_code400: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_ZONEAsia/Shanghai# 改成中国时区USE_TZTrue# 数据库继续存 UTC序列化时自动转北京时间AI 加速Mock 数据 联调测试联调的时候有个尴尬的时期后端接口还没写完前端就要开始调了。怎么办用 Mock 数据先跑着。以前手写 Mock 数据要半天现在直接让 AI 生成请帮我生成 10 条电商商品 Mock 数据JSON 格式字段包括 id, name, price10-999随机, main_image用 picsum.photos 占位图, category从数码/服饰/食品/家居随机选, stock0-500随机, description50字以内的中文描述, created_at最近30天内的ISO时间 要求 1. 有一个商品库存为 0测试售罄状态 2. 有一个商品价格是 0.01测试边界值 3. main_image 故意留一个空字符串测试 fixImageUrlAI 几秒钟就生成好还贴心地留了测试点。再让 AI 写一个联调冒烟测试脚本自动遍历所有接口检查返回类型对不对、图片 URL 修没修好、Token 有没有带上输出通过/失败统计。AI 加速心得联调阶段的很多体力活——Mock 数据生成、测试脚本编写、错误日志分析——都可以交给 AI。把省下来的时间用在理解业务逻辑和排查真正的问题上。学完来练练手给请求拦截器加请求去重同一个接口短时间内不要重复发送防止用户连点提交按钮给fixImageUrl加 CDN 域名替换如果配置了 CDN把/media/前缀替换成 CDN 地址写一个响应拦截器的loading 计数器有请求在飞时显示全局 loading全部完成后隐藏配套文件day18_request_utils.js请求封装、fixImageUrl、批量修复、冒烟测试脚本可参考学习。本系列为梅雅达编程笔记原创首发于 CSDN。有问题评论区见