thinkphp 8.0 通过重写 json 响应类、添加全局中间件兜底异常响应、封装 apiresponser trait 及规范分页处理,统一 json 返回结构为 {code, msg, data},解决前后端联调解析不一致问题。

ThinkPHP 8.0 项目中,控制器 return ['code'=>0,'data'=>[]] 与 throw new ValidateException() 返回的 JSON 结构不一致,导致前端需写多套解析逻辑,联调时频繁报 Unexpected token 或 data 字段为字符串而非对象。
重写 Json 响应类,接管所有显式 json() 调用
这一步只影响你主动调用 json() 的地方,比如 return json($data),不碰异常流,是最干净的起点。
在 app/common/response/Json.php 中新建类,命名空间设为 app\common\response,继承 think\response\Json。
只重写 output($data) 方法:构造 ['code' => $this->code, 'msg' => $this->message, 'data' => $data] 后执行 json_encode($result, JSON_UNESCAPED_UNICODE) 并返回字符串。
【不要重写 __construct 或 init 方法】 因为 $this->code 和 $this->message 是框架根据 json($data, 400, [], ['code'=>1]) 第四个参数自动设置的,改掉会导致状态码失效。
在 AppServiceProvider::register() 中绑定:$this->app->bind('think\response\Json', \app\common\response\Json::class);
中间件兜底所有非 Json 响应
上面只管住了 return json(),但 throw new ValidateException()、未捕获异常、HttpException 仍走原生响应路径,结构不一致。必须在响应发出前强制统一封装。
新建中间件 app/middleware/UniformJsonResponse.php,handle() 方法中判断:若 $response instanceof \think\Response 且不是 \think\response\Json 实例,则进入封装流程。
若原始响应体是数组(如验证失败默认结构),提取其中 'code'、'msg'、'data' 字段;若不是数组,统一包装为 ['code' => 500, 'msg' => 'server error', 'data' => null]。
用 json($wrapped)->header(['Content-Type' => 'application/json; charset=utf-8']) 替换原响应对象,并 return 它。
在 app/middleware.php 中注册为全局中间件,确保它在 ResponseTrace 之后、输出之前执行。
控制器层封装 success / fail 方法(Trait 方式)
让业务代码更直观,避免手写数组结构出错,也方便后期加 timestamp、version 等字段。
方法一:在 app/common/traits/ApiResponser.php 中定义 Trait,包含 success($data, $msg = '操作成功', $code = 200) 和 fail($msg = '操作失败', $data = null, $code = 400) 两个方法。
方法二:每个方法内部都调用 $this->json(['code' => $code, 'msg' => $msg, 'data' => $data]),不直接 return,而是交由框架自动处理响应对象。
方法三:在 BaseController 中 use 该 Trait,并确保所有业务控制器继承 BaseController。
注意:CLI 环境下禁止调用 header(),需在 success/fail 内部提前判断 PHP_SAPI !== 'cli',否则会触发警告并中断输出。
分页数据必须手动提取字段再响应
ThinkPHP 8 的 paginate() 返回的是 think\Paginator 对象,不是数组。直接 return $list 会丢失 total、last_page、has_next_page 等关键元信息,前端无法做分页控制。
第一步:调用 $list->items() 获取当前页数据数组。
第二步:用 $list->toArray() 提取完整分页结构,但注意它包含 items、meta 等嵌套字段,需扁平化或按需裁剪。
第三步:将处理后的数据传入 success() 方法,例如 return $this->success(['list' => $list->items(), 'pagination' => ['total' => $list->total(), 'per_page' => $list->listRows(), 'current_page' => $list->currentPage()]]);
这一步漏掉就会导致前端拿不到总条数,分页器永远显示“第 1 页,共 0 页”。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











