You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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),且下一个标签开头部分被忽略。

求助需求

  1. 如何确保跨多个数据块的HTML标签在客户端被正确处理和展示?
  2. 展示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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.21 11:40:14