多后端混用 code 语义,页面静默空白却不报错
某后台管理系统的首页,数据看板打开一片空白——数字全是 --,趋势图什么都没有。打开 Network 一看,每个 API 都返回了 HTTP 200,JSON 里 data 字段也有值,就是不渲染。控制台零报错。
这种"数据明明在,页面偏偏空"的现象,十有八九是多后端混用响应 code 语义踩坑了。
现象
项目同时调用两个不同技术栈的后端:一个老 PHP 后台(FastAdmin / TP5),一个新增的 PHP 模块。页面代码从老页面复制而来,对所有接口统一用 res.code === 1 判断成功。
老后台确实用 code: 1 表示成功,新模块却用 code: 200。于是新模块的三个接口每次都走进了"失败"分支,数据被丢弃,组件渲染空状态——整个过程没有任何 JS 异常抛出。
根因
不同后端栈对"成功"的 code 值定义差异极大,且存在语义反转陷阱:
| 后端栈 | 成功 | 错误 |
|---|---|---|
| FastAdmin / TP5 | code: 1 | code: 0 |
| ThinkPHP 8 默认 | code: 0 | code: 42200 |
| Java Spring Boot | err: 0 | err: <非零> |
| HTTP 风格 PHP | code: 200 | code: 400/500 |
注意 TP5 用 0 表示失败,TP8 用 0 表示成功——同一个 body 形状,完全相反的含义。
全局 axios 拦截器是问题的放大器:
// 反模式:一个拦截器硬编码所有后端
axios.interceptors.response.use(res => {
if (res.data.code === 1) return res.data // 只对 FastAdmin 正确
return Promise.reject(res.data) // 其余后端全部"失败"
})这个拦截器让新接入的后端请求全部进入 reject 路径,而页面代码里的 catch 往往只是打个 log 或静默返回,最终表现为空数据而非报错。
当你把 TP5 后台改造为 TP8,或者引入一个新的后端模块时,code: 0 的含义可能发生反转。用 if (res.code === 0) 判断成功——TP5 项目里这句话等价于"永远失败",TP8 项目里却是"永远成功"。一旦两套后端共存,任何全局统一判断都会在某一侧静默出错。
排查方法
用 grep 快速定位混用点:
# 扫描各种成功判断的分布
grep -rn "res\.code\s*===\s*1" src/ | sort -u
grep -rn "res\.code\s*==\s*200" src/ | sort -u
grep -rn "res\.code\s*===\s*0" src/ | sort -u
grep -rn "res\.err\s*===\s*0" src/ | sort -u
# 找同时混用两种约定的文件——这些是最高风险
grep -rln "res\.code === 1" src/ | xargs grep -l "res\.code == 200"出现在最后一条结果里的文件,几乎一定有某个接口用了错误的判断条件。
正解
每个后端独立一个 axios 实例,在各自的拦截器里消化 code 语义,页面层永远只见 resolved data 或 rejected error。
// src/api/clients/legacyClient.js — FastAdmin/TP5,code:1=成功
const legacyClient = axios.create({ baseURL: settings.legacyUrl })
legacyClient.interceptors.response.use(res => {
const body = res.data
if (body.code === 1) return body
return Promise.reject({ source: 'legacy', code: body.code, msg: body.msg })
})
// src/api/clients/newModuleClient.js — HTTP 风格,code:200=成功
const newModuleClient = axios.create({ baseURL: settings.newModuleUrl })
newModuleClient.interceptors.response.use(res => {
const body = res.data
if (body.code === 200) return body
return Promise.reject({ source: 'newModule', code: body.code, msg: body.msg })
})页面代码从此不再写 if (res.code === ?) ——.then(data => ...) 即成功,.catch(err => ...) 即失败,语义在唯一的地方定义一次。
同时维护一份 backend-contracts.md,记录每个客户端实例的成功/失败 code,新成员和 AI 接手任务前先读这份文档,避免凭经验猜约定。
不要共享 axios 实例。每个后端配一个独立 instance,在其 response.use 里做一次统一判断并 unwrap data。页面代码只写业务逻辑,永远不碰 code 值。
一句话外卖
多后端共存时,把每个后端的成功判断封进专属 axios 实例的拦截器——让语义错误在创建实例时就暴露,而不是在页面渲染时静默蒸发。