返回博客

多后端混用 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 或静默返回,最终表现为空数据而非报错。

坑:code: 0 在 TP5 是失败,在 TP8 却是成功

当你把 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 实例,拦截器独享语义

不要共享 axios 实例。每个后端配一个独立 instance,在其 response.use 里做一次统一判断并 unwrap data。页面代码只写业务逻辑,永远不碰 code 值。

一句话外卖

多后端共存时,把每个后端的成功判断封进专属 axios 实例的拦截器——让语义错误在创建实例时就暴露,而不是在页面渲染时静默蒸发。