用PHP做长任务处理时,很多人喜欢配合iframe做实时输出:父页面嵌一个iframe,PHP脚本用ob_flush加flush把处理进度一段一段吐出来,用户就能看到日志滚动。但只要iframe里的页面和父页面不同域,浏览器就会毫不留情地抛出跨域错误,脚本无法互相访问,进度数据也没法传回父页面。这篇文章就把这个问题拆开讲清楚,给出几种真正能落地的解决办法。

先搞清楚:跨域到底拦的是什么
浏览器的同源策略规定,两个页面的协议、域名、端口只要有一个不同,就算跨域。比如父页面在 https://www.ipipp.com/parent.html,iframe指向 https://api.ipipp.com/stream.php,虽然同属一个主域,端口和协议一致但子域不同,同样是跨域。跨域时,iframe内部的DOM、window对象上的属性,父页面都读不到,直接访问会报类似 Blocked a frame with origin 的错误。
但要注意一个容易混淆的点:跨域并不会阻止iframe本身加载页面,也不会阻止PHP正常输出内容。也就是说,iframe里PHP实时输出的日志、进度条,用户在iframe区域里是能正常看到滚动效果的。被拦的是另一件事——父页面想用JavaScript去读取或操作iframe里的内容,比如 iframe.contentWindow.document.body.innerHTML 这种操作。所以先判断你的需求到底是哪一种:只是展示,那根本不存在跨域问题;需要交互和数据传递,才需要下面的方案。
PHP实时输出本身也有几个前提,顺带提一下。Nginx默认会开启缓冲,需要设置 fastcgi_buffering off; 或在PHP里发送 X-Accel-Buffering: no 响应头;代码层面要关闭输出缓冲并周期性刷新:
<?php
// 关闭所有输出缓冲,保证内容即时推送到浏览器
while (ob_get_level() > 0) {
ob_end_flush();
}
echo str_pad('', 4096); // 填充缓冲区,绕过部分浏览器4KB起步缓冲
ob_flush();
flush();
for ($i = 1; $i <= 10; $i++) {
echo "正在处理第 {$i} 步...<br>";
ob_flush();
flush();
sleep(1);
}
echo "任务完成";这段代码在iframe里独立运行是没有问题的。接下来讨论的,都是围绕父页面与iframe之间的数据交互展开。
首选方案:postMessage实现父子页面安全通信
postMessage是HTML5提供的跨文档消息机制,专门用来解决跨域窗口之间的通信问题,也是目前最推荐的方式。它的思路很简单:子页面主动把自己的消息发给父页面,父页面监听消息事件即可,全程不需要直接访问对方DOM,天然绕开同源策略。
先看子页面(也就是PHP实时输出的页面)的写法。PHP每输出一段进度,同时输出一段JavaScript调用parent.postMessage把数据推给父页面:
<?php
while (ob_get_level() > 0) {
ob_end_flush();
}
?>
<script>
// 向父页面发送消息,第二个参数是父页面的源,建议写明确地址而不是*
function sendToParent(msg) {
if (window.parent !== window) {
window.parent.postMessage(msg, 'https://www.ipipp.com');
}
}
</script>
<?php
for ($i = 1; $i <= 10; $i++) {
echo "<script>sendToParent('第 {$i} 步完成');</script>";
echo "第 {$i} 步执行中<br>";
ob_flush();
flush();
sleep(1);
}
echo "<script>sendToParent('ALL_DONE');</script>";父页面这边只需要注册一个message事件监听器。这里有个安全细节必须强调:收到消息后一定要校验event.origin,否则任何第三方页面嵌你的页面都能伪造消息注入:
window.addEventListener('message', function (event) {
// 校验来源,防止恶意页面伪造消息
if (event.origin !== 'https://api.ipipp.com') {
return;
}
console.log('收到iframe消息:', event.data);
if (event.data === 'ALL_DONE') {
document.getElementById('status').textContent = '任务全部完成';
} else {
document.getElementById('status').textContent = event.data;
}
});这种方案的好处是兼容性好,IE8以上都支持,不需要修改服务器配置,也不依赖双方同域。缺点是消息传递是异步单向的,如果需要复杂的双向调用,要自己约定消息格式,比如加上type字段做路由。
替代方案:代理转发与CORS响应头
如果业务上不方便改iframe页面代码,可以考虑用同源代理。思路是把跨域请求转成同源请求:父页面不直接嵌跨域地址,而是嵌自己域名下的一个转发脚本,由服务端去请求真实地址再实时转发内容。以PHP为例:
<?php
// proxy.php 放在与父页面同域的服务器上
$target = 'https://api.ipipp.com/stream.php';
$ch = curl_init($target);
curl_setopt($ch, CURLOPT_WRITEFUNCTION, function ($ch, $data) {
echo $data; // 收到一段就立刻转发一段
ob_flush();
flush();
return strlen($data);
});
curl_setopt($ch, CURLOPT_HEADER, false);
header('Content-Type: text/html; charset=utf-8');
header('X-Accel-Buffering: no');
curl_exec($ch);
curl_close($ch);这样iframe加载的是同源的proxy.php,父页面可以直接访问contentWindow.document,任何DOM操作都不受限制。代价是所有流量都经过你的服务器,增加了带宽和延迟,长连接还会占用PHP-FPM进程,高并发场景要慎重。
另一种情况是父页面只需要从服务端拿数据而不是读iframe,那就压根不用碰跨域,直接给PHP接口加上CORS响应头,用fetch或XMLHttpRequest轮询进度即可:
<?php
header('Access-Control-Allow-Origin: https://www.ipipp.com');
header('Access-Control-Allow-Credentials: true');
// 输出当前进度,父页面用fetch轮询或用EventSource订阅
echo json_encode(['step' => 5, 'total' => 10, 'status' => 'running']);顺便提一下 document.domain 这个老方案:它只适用于协议和端口相同、仅子域不同的场景,比如 a.ipipp.com 嵌 b.ipipp.com,双方都设置 document.domain = 'ipipp.com' 后可以互相访问。但它对新版浏览器已逐步收紧,且解决不了完全不同域名的情况,新项目不建议依赖。另外如果只是想阻止iframe被嵌入自己的站点,可以配合 X-Frame-Options 或 Content-Security-Policy 的 frame-ancestors 指令做反向控制,这与本主题方向相反,但同属iframe跨域知识体系,了解有助于排查问题。
排查思路与常见踩坑总结
实际排查这类问题时,建议按固定顺序走一遍。第一步打开浏览器控制台看报错,如果报错来自 contentWindow.document 之类的访问,是同源策略拦截,走postMessage或代理;如果报错提示是Refused to display frame,那是对方设置了X-Frame-Options: DENY,这种只能通过代理绕过或者联系对方开放。第二步确认PHP实时输出是否真的生效,可以在iframe地址栏直接打开观察内容是否逐段出现,如果一次性全部显示,多半是Nginx缓冲没关,检查 fastcgi_buffering 和 gzip 配置。
还有几个高频坑值得记住。一是postMessage的第二个参数写 * 虽然省事,但等于把消息广播给任意源,生产环境务必写明确地址。二是PHP开启输出压缩(gzip/zlib.output_compression)后,flush 会失效,内容被攒到压缩缓冲里一起发出,实时性直接归零,需要在php.ini或脚本中关闭。三是session锁:PHP的session文件在脚本运行期间是加锁的,如果实时输出脚本持有session,其他带session的请求会被阻塞,必要时用 session_write_close() 提前释放。四是某些浏览器对about:blank或data协议的iframe有特殊限制,初始化阶段发消息可能失败,等iframe的load事件触发后再通信更稳妥。
总结一下选型建议:能在iframe页面里加代码,就用postMessage,干净、安全、通用;不能改对方页面或需要完整DOM访问,用同源代理;只需读数据不做页面嵌入,直接上CORS加轮询或SSE。搞清楚自己被拦的到底是哪一层需求,方案选择其实非常清晰。
PHP实时输出iframe跨域postMessage修改时间:2026-09-07 20:17:51