JavaScript流式HTML渲染问题:标签开头丢失如何解决?
问题描述
通过Server-Sent Events(SSE)流式传输HTML内容到客户端实时展示时,HTML标签开头部分(如<h、<p)在客户端渲染时丢失。例如<h3>被拆分为<h、3、>,其中<h未被正确添加到展示容器。
现有实现
服务端PHP代码(OpenAI API代理)
function process_openai_stream_data($data, &$full_response) { if (connection_aborted()) { return 0; // 客户端断开则停止流 } $lines = explode("\n", $data); foreach ($lines as $line) { if (strpos($line, 'data: ') === 0) { $line = trim(substr($line, 6)); // 移除'data: '前缀 if ($line === '[DONE]') { // 流结束信号 echo "data: [END]\n\n"; flush(); return 0; // 停止流 } $json = json_decode($line, true); if (json_last_error() !== JSON_ERROR_NONE) { continue; } if (isset($json['choices'][0]['delta']['content'])) { $content = $json['choices'][0]['delta']['content']; $full_response .= $content; echo "data: " . $content . "\n\n"; flush(); } } } return strlen($data); }
客户端JavaScript代码
function downloadResults(requestId) { const formData = new FormData(); formData.append('action', 'download_feedback_streaming'); formData.append('request_id', requestId); formData.append('nonce', beyond.nonce); formData.append('ajax', 'frontend'); const resultDisplay = document.getElementById('result-content'); if (!resultDisplay) { console.error('未找到结果展示元素'); return; } resultDisplay.innerHTML = ''; // 清空之前的内容 fetch(beyond.ajaxurl, { method: 'POST', body: formData }) .then(response => { if (!response.ok) { throw new Error('网络响应异常'); } const reader = response.body.getReader(); const decoder = new TextDecoder(); let buffer = ''; function readStream() { reader.read().then(({ done, value }) => { if (done) { displayAudioFeedback(); console.log('流传输完成'); return; } buffer += decoder.decode(value, { stream: true }); const events = buffer.split('\n\n'); buffer = events.pop(); // 保留最后一个不完整的事件在缓冲区 events.forEach(event => { if (event.startsWith('data: ')) { const data = event.slice(6); // 移除'data: '前缀 // 添加到结果展示容器 resultDisplay.insertAdjacentHTML('beforeend', data); } }); readStream(); // 继续读取下一个块 }).catch(error => { console.error('读取流时出错:', error); displayError("获取反馈时发生错误,请重试。"); }); } readStream(); // 开始读取流 }) .catch(error => { console.error('下载结果时出错:', error); displayError("获取反馈时发生错误,请重试。"); }); }
问题细节
流式传输时,HTML标签被拆分到多个数据块,导致标签开头部分丢失。例如<h3>拆分为<h、3、>,<h未被渲染;<p>等标签也存在同样问题。已确认服务端和客户端API调用阶段数据正常,但发现闭合标签带有换行符(如>\n),且下一个标签开头部分被忽略。
求助需求
- 如何确保跨多个数据块的HTML标签在客户端被正确处理和展示?
- 展示HTML格式内容是否有更优的处理方式?
解决方案
1. 修复客户端缓冲区处理逻辑
问题核心在于不完整的HTML片段直接插入DOM会被浏览器自动修正,比如单独的<h会被浏览器忽略。需要等到片段能构成合法的HTML(或至少是不会被自动丢弃的内容)再插入,或者维护一个完整的缓冲区,确保每次插入的是可以被正确解析的内容。
修改客户端代码,调整缓冲区处理方式:
function downloadResults(requestId) { const formData = new FormData(); formData.append('action', 'download_feedback_streaming'); formData.append('request_id', requestId); formData.append('nonce', beyond.nonce); formData.append('ajax', 'frontend'); const resultDisplay = document.getElementById('result-content'); if (!resultDisplay) { console.error('未找到结果展示元素'); return; } resultDisplay.innerHTML = ''; fetch(beyond.ajaxurl, { method: 'POST', body: formData }) .then(response => { if (!response.ok) { throw new Error('网络响应异常'); } const reader = response.body.getReader(); const decoder = new TextDecoder(); let buffer = ''; // 用于跟踪未闭合的标签或不完整的HTML片段 const tagRegex = /<\/?[a-z][a-z0-9]*[^>]*$/i; function readStream() { reader.read().then(({ done, value }) => { if (done) { // 流结束时,插入剩余的缓冲区内容 if (buffer.trim()) { resultDisplay.insertAdjacentHTML('beforeend', buffer); } displayAudioFeedback(); console.log('流传输完成'); return; } buffer += decoder.decode(value, { stream: true }); const events = buffer.split('\n\n'); buffer = events.pop(); events.forEach(event => { if (event.startsWith('data: ')) { let data = event.slice(6); // 检查当前数据是否包含不完整的标签 const incompleteMatch = data.match(tagRegex); if (incompleteMatch) { // 将不完整部分移回缓冲区,只处理前面完整的部分 const splitIndex = data.lastIndexOf(incompleteMatch[0]); buffer = incompleteMatch[0] + buffer; data = data.slice(0, splitIndex); } if (data.trim()) { resultDisplay.insertAdjacentHTML('beforeend', data); } } }); readStream(); }).catch(error => { console.error('读取流时出错:', error); displayError("获取反馈时发生错误,请重试。"); }); } readStream(); }) .catch(error => { console.error('下载结果时出错:', error); displayError("获取反馈时发生错误,请重试。"); }); }
2. 服务端优化
可以在服务端对OpenAI返回的delta内容做简单的缓冲,积累到完整的标签或文本块再发送,减少客户端的拆分压力:
function process_openai_stream_data($data, &$full_response, &$stream_buffer = '') { if (connection_aborted()) { return 0; } $lines = explode("\n", $data); foreach ($lines as $line) { if (strpos($line, 'data: ') === 0) { $line = trim(substr($line, 6)); if ($line === '[DONE]') { // 发送剩余缓冲区内容 if (!empty($stream_buffer)) { echo "data: " . $stream_buffer . "\n\n"; flush(); $full_response .= $stream_buffer; $stream_buffer = ''; } echo "data: [END]\n\n"; flush(); return 0; } $json = json_decode($line, true); if (json_last_error() !== JSON_ERROR_NONE) { continue; } if (isset($json['choices'][0]['delta']['content'])) { $content = $json['choices'][0]['delta']['content']; $stream_buffer .= $content; // 检查缓冲区是否包含完整的标签或文本段落(简单判断:遇到闭合标签或换行) if (strpos($stream_buffer, '>') !== false || strpos($stream_buffer, "\n") !== false) { echo "data: " . $stream_buffer . "\n\n"; flush(); $full_response .= $stream_buffer; $stream_buffer = ''; } } } } return strlen($data); }
3. 更优的HTML流式展示方案
如果追求更稳定的渲染效果,可以:
- 使用DocumentFragment暂存内容,积累到一定量或完整标签后再插入DOM,减少重绘次数
- 对于复杂HTML,考虑在服务端返回结构化的内容(如JSON格式的段落、标题),客户端根据结构动态生成DOM元素,避免直接插入原始HTML片段的兼容性问题
内容的提问来源于stack exchange,提问作者khuss
相关产品推荐
相关产品推荐

