直接 return json($data) 无法统一结构,因其仅输出 ['data' => $data],而异常响应为 ['error' => 'xxx'] 或 html,导致前端需多套解析逻辑;应重写 json 响应类接管显式 json(),再通过 setsendcallback 中间件统一封装所有 json 响应。

为什么直接 return json($data) 无法统一结构
因为 json() 只是调用框架内置的 think\response\Json 类,它默认输出 ['data' => $data],不带 code 和 msg 字段;而异常(如 ValidateException)走的是另一套响应路径,返回 ['error' => 'xxx'] 或 HTML,前端必须写多套解析逻辑。
常见错误现象:前端 JSON.parse() 报 Unexpected token e in JSON at position 0,或 data 字段是字符串而非对象——本质是后端混用了至少 3 种结构:return json()、throw new ValidateException()、未捕获异常。
- 不要试图在每个控制器里手动拼数组再
json(),维护成本高且易漏 - 不要重写
Json类的__construct或init方法,会破坏状态码映射(比如json($data, 400)中的400就失效) - 关键点在于:显式
json()和隐式异常响应,必须由不同机制分别接管,再汇入同一出口
重写 Json 响应类,只接管显式 json() 调用
新建 app/common/response/Json.php,继承 think\response\Json,仅重写 output($data):
namespace app\common\response;
use think\response\Json as BaseJson;
class Json extends BaseJson
{
protected function output($data): string
{
$result = [
'code' => $this->code,
'msg' => $this->message,
'data' => $data,
];
return json_encode($result, JSON_UNESCAPED_UNICODE);
}
}
然后在 AppServiceProvider::register() 中绑定:
$this->app->bind('think\response\Json', \app\common\response\Json::class);
这样所有 return json($data)、return json($data, 400) 都自动套上 code/msg,且状态码仍生效。但注意:它对 throw 出来的异常完全无效。
用中间件在 send() 前拦截并统一封装所有响应
必须用 Response::setSendCallback(),而不是 after 中间件——后者执行时响应已发出,改了也白改。
新建中间件 app/middleware/UniformJsonResponse.php:
public function handle($request, \Closure $next)
{
\think\Response::setSendCallback(function ($response) {
// 只处理 application/json 响应
$type = $response->getHeader('Content-Type')[0] ?? '';
if (strpos($type, 'application/json') === false) {
return $response;
}
$content = $response->getContent();
$raw = json_decode($content, true);
// 提取原始结构中的 code/msg/data,或兜底为 500
$code = $raw['code'] ?? ($response->getCode() ?: 500);
$msg = $raw['msg'] ?? ($raw['error'] ?? 'server error');
$data = $raw['data'] ?? ($raw ?? null);
$newBody = json_encode([
'code' => $code,
'msg' => $msg,
'data' => $data
], JSON_UNESCAPED_UNICODE);
return \think\Response::create($newBody, 'json')
->header('Content-Length', strlen($newBody));
});
return $next($request);
}
注册为全局中间件,并确保它排在 think\middleware\JsonResponse 之前(否则会被覆盖)。验证方式:在中间件中 dump(\think\Pipeline::getMiddleware()) 查顺序。
异常类 render() 里别硬编码 JSON 返回
app/exception/Handler.php 的 render() 方法,只负责把异常转成响应对象,**不负责格式封装**——那该由上面的 UniformJsonResponse 中间件统一干。
所以 render() 里只需做两件事:
- 判断请求是否期望 JSON(检查
Accept头),不是就交给父类处理(返回 HTML 错误页) - 是 JSON 请求,就返回
json(['error' => $e->getMessage()], $e->getStatusCode()),让后续中间件去套壳
别在这里手动拼 ['code'=>500,'msg'=>'xxx','data'=>null],否则和中间件逻辑重复,且容易漏掉 ValidateException 等框架级异常的字段提取逻辑。
最易被忽略的一点:中间件里的 setSendCallback 是唯一能真正拦截所有响应类型的钩子,包括文件下载、视图渲染失败等非 JSON 场景——你得靠 Content-Type 主动过滤,而不是无差别重写。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











