ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Day 18 | 前后端联调:打通任督二脉

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.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。把省下来的时间用在理解业务逻辑和排查真正的问题上。


学完来练练手

  1. 给请求拦截器加"请求去重":同一个接口短时间内不要重复发送(防止用户连点提交按钮)
  2. 给fixImageUrl加 CDN 域名替换:如果配置了 CDN,把/media/前缀替换成 CDN 地址
  3. 写一个响应拦截器的"loading 计数器":有请求在飞时显示全局 loading,全部完成后隐藏

配套文件day18_request_utils.js(请求封装、fixImageUrl、批量修复、冒烟测试脚本)可参考学习。

本系列为梅雅达编程笔记原创,首发于 CSDN。有问题?评论区见 🍃

返回列表