用PHP输出纯JSON别带BOM,前端解析不再崩
你有没有遇到过这种情况:前端高高兴兴调接口,结果 `JSON.parse` 直接报错,白屏一片;后端看了半天代码,逻辑一点毛病没有,数据也正常,最后查半天发现是 输出里藏了个看不见的字符 —— BOM。
对,就是那个写 PHP 时经常被忽略,却能让前端崩溃于无形的 BOM。
什么是 BOM,为什么它这么坑?
BOM 全称是 Byte Order Mark,也就是字节顺序标记,通常出现在文件开头,用 `EF BB BF` 三个字节来表示。它在 UTF-8 编码的文件里其实没啥实质作用,更多是某些编辑器(比如 Windows 记事本)用来“声明”自己是 UTF-8 的。
问题在于,BOM 是字符,不是 PHP 代码。如果 PHP 文件本身带了 BOM,那么 `<?php` 之前就已经输出了一段不可见字符。哪怕你代码里写的是纯 JSON,实际返回给前端的内容也是 `\xEF\xBB\xBF{"code":0}`。
前端拿到这段内容直接 `JSON.parse()`,拜拜,直接崩。
最常见的几种“带 BOM”场景
说实话,BOM 这事儿最坑的地方不是它多难解决,而是它出现得太隐晦,就像鞋里的一粒沙子,不大,但走路就是硌脚。
场景一:编辑器自带 BOM
很多人喜欢用记事本、某些 Windows 编辑器写 PHP 文件,保存时默认就带 BOM。如果你的项目是团队协作,某个人提交了一个带 BOM 的文件,恭喜你,整个接口可能都跟着遭殃。
场景二:文件拼接/引入导致 BOM 累积
有些老项目喜欢用 `include` 拼接模板,或者通过 `ob_start` 缓冲区再统一输出,这时候只要任何一个被引入的文件带了 BOM,最终输出结果里就会插入了额外的 BOM 字符。
场景三:PHP 和 HTML 混写
PHP 文件中如果存在类似下面的代码:
?>
<?php
// 这里其实已经产生了输出
`?>` 后面的换行符、空格也都会变成输出内容。如果文件编码带 BOM,那这只“黑手”就更明显了。
如何排查你的接口有没有带 BOM?
很简单,浏览器打开接口地址,或者用 curl 看一眼:
curl -i http://your-api.com/json.php | head -c 100 | xxd
如果输出的第一个字节是 `efbbbf`,那就基本实锤了。
还有一种办法就是直接看接口响应头和生产环境差异,如果前端在生产环境报错、本地却正常,大概率也是环境编码问题。
PHP 输出纯 JSON 的正确姿势
首先,绝对不要让 PHP 文件和 JSON 输出之间有任何“多余输出”,包括 BOM、空格、空行、隐藏字符。
推荐你直接裸奔输出,不写结束标签:
<?php
// 纯 PHP 文件建议不要写 ?> 结束标签,防止后面误加换行
header('Content-Type: application/json; charset=utf-8');
$data = ['code' => 0, 'msg' => 'ok', 'data' => ['id' => 1]];
echo json_encode($data, JSON_UNESCAPED_UNICODE);
exit;
注意几点:
1. 不要写 `?>`,这能避免文件结尾多余换行被输出。
2. 加 `exit`,防止后面有代码或输出混进来。
3. 设置正确 Content-Type,有些老浏览器对 `text/html` 的 JSON 解析兼容性不太好。
但以上只能规避后续问题,如果文件本身已经带 BOM 了怎么办?
有 BOM 的文件怎么处理?
如果你手头已经有个带 BOM 的 PHP 文件,最直接的办法就是用代码编辑工具转一下编码。VS Code、Sublime、Notepad++ 都能改。
另外也可以在代码里加个“保险丝”:
ob_start(); // 开启缓冲区
// ... 你的逻辑代码
$output = ob_get_clean();
// 移除开头 BOM
if (substr($output, 0, 3) === "\xEF\xBB\xBF") {
$output = substr($output, 3);
}
echo $output;
这个方法不优雅,但作为兜底还算实用。
更推荐在项目入口文件里统一处理,比如写个 `response()` 公共函数,所有 JSON 输出都走它,确保头部被清理干净。
另外一个很常见的坑是 `json_encode` 返回 `false` 的场景,如果之前代码里已经输出了点什么,JSON 解析失败后前端拿到的是 `false` 而不是字符串,同样会崩。所以最稳妥的颜色是——先组装好数据,再用 `exit` 一次输出,不给自己留任何其他输出机会。
前端也不是一无所能,但要抓对方向
如果前端也想保障一下,可以在 `fetch` 后自动剥离 BOM。但说实话这是治标不治本,这锅不能让前端背,接口就得返回干净的数据。
const res = await fetch('/api/user');
let text = await res.text();
text = text.replace(/^\uFEFF/, ''); // 兼容 BOM
const data = JSON.parse(text);
不少团队在联调阶段浪费了大量时间排查这种问题,最后发现就是某个大佬用记事本存了个文件。低级吗?确实低级。但坑吗?真坑。
习惯比技术更重要
说到底,BOM 这类问题,是完全可以靠编码习惯避免的:
1. PHP 文件统一存为 `UTF-8 无 BOM` 格式;
2. 写纯 PHP 文件时,去掉 `?>` 结束标签;
3. 所有接口 JSON 输出走统一封装方法,方便加“保险丝”;
4. 团队统一编辑器配置,禁止记事本直接改代码做开发工具。
技术圈常说要“面向异常编程”,但我觉得输出 JSON 这件事,更该“面向干净编程”。你输出的不是一串字符串,而是前后端之间的一份契约。BOM 虽小,但它足以让整份契约变成废纸。
希望看到这篇文章的你,下次再被前端喊“接口崩了”的时候,能想得起这三个字母:BOM。
年卡会员