UniApp + Dify 实战:详解 SSE 流式响应的解析与前端渲染

1. 理解核心机制:拼接而非替换

Dify 的 streaming 模式下,服务器会不断推送形如 data: {"event": "message", "answer": "字"} 的数据包。
核心逻辑是: 收到一个包,解析出 answer 字段,将其**追加(Append)**到当前正在显示的对话变量后,而不是直接替换。

2. 关键数据解析逻辑

Dify 返回的数据流格式如下:

text 复制代码
data: {"event": "message", "answer": "我", ...}\n\n
data: {"event": "message", "answer": "是", ...}\n\n
data: {"event": "message_end", ...}\n\n

处理难点:

  1. 前缀处理 :每行数据都以 data: 开头,解析 JSON 前必须去掉。
  2. 粘包处理 :有时候一次网络请求回调会收到多条 data,需要用 \n\n 分割。
  3. 事件区分 :必须判断 event 字段。
    • message: 文本块,核心展示内容
    • message_replace: 敏感词替换,需要替换整段文本。
    • message_end: 结束标志。
    • ping: 心跳,忽略即可。

3. UniApp 代码实现方案

在 UniApp 中(特别是微信小程序端),不能直接使用浏览器原生的 EventSource。推荐使用 uni.requestenableChunked: true 参数。

以下是一个完整的处理示例代码:

javascript 复制代码
// 假设这是发送消息的方法
sendMessage(userQuery) {
    const that = this;
    
    // 1. 在界面先创建一个空的回答占位(为了立刻显示 loading 或光标)
    this.messageList.push({
        role: 'user',
        content: userQuery
    });
    this.messageList.push({
        role: 'assistant',
        content: '' // 初始为空,稍后拼接
    });
    
    // 获取当前正在更新的这条消息在数组中的索引
    const currentMsgIndex = this.messageList.length - 1;

    // 2. 发起请求
    const requestTask = uni.request({
        url: 'http://47.243.127.167:4010/v1/chat-messages',
        method: 'POST',
        header: {
            'Authorization': 'Bearer {API_KEY}', // 替换为真实 Key
            'Content-Type': 'application/json'
        },
        data: {
            inputs: {},
            query: userQuery,
            response_mode: "streaming", // 必须是 streaming
            user: "uni-user-123",
            conversation_id: that.conversationId || "" // 如果是连续对话,需传入
        },
        enableChunked: true, // 【关键】开启流式传输支持
        success: (res) => {
            // 这里是请求完成后的回调,流式通常不在这里处理数据
        }
    });

    // 3. 监听流式数据头(可选)
    requestTask.onHeadersReceived((headers) => {
        // console.log('Header received', headers);
    });

    // 4. 【核心】监听分片数据
    requestTask.onChunkReceived((res) => {
        // res.data 是 ArrayBuffer,需要转换
        const arrayBuffer = res.data;
        // 小程序/App端需要 TextDecoder,或者使用第三方库转换
        // 如果环境不支持 TextDecoder,需使用类似 text-encoding 的 polyfill
        const uint8Array = new Uint8Array(arrayBuffer);
        let text = "";
        
        // 简易转换 (注意:中文可能乱码,生产环境建议用专业库如 fast-text-encoding)
        // 微信小程序基础库高版本已支持 TextDecoder
        try {
            const decoder = new TextDecoder('utf-8');
            text = decoder.decode(uint8Array, { stream: true });
        } catch (e) {
            // 兼容写法,逐字节处理(此处仅为示意,建议引入库)
            text = String.fromCharCode.apply(null, uint8Array); 
            // 实际开发请务必处理 UTF-8 多字节中文乱码问题
            text = decodeURIComponent(escape(text)); 
        }

        // 5. 处理 Dify 返回的原始数据字符串
        that.processDifyStream(text, currentMsgIndex);
    });
},

// 处理 Dify 数据流的专用函数
processDifyStream(chunkText, msgIndex) {
    // Dify 的数据块以 \n\n 分隔
    const lines = chunkText.split('\n\n');

    lines.forEach(line => {
        // 去掉 data: 前缀
        if (line.startsWith('data: ')) {
            const jsonStr = line.replace('data: ', '');
            try {
                const data = JSON.parse(jsonStr);
                
                // 根据 Dify 文档判断 event 类型
                if (data.event === 'message') {
                    // 【关键步骤】拼接 answer 字段到当前消息
                    this.messageList[msgIndex].content += data.answer;
                    
                    // 保存 conversation_id 以便下一轮对话
                    if (!this.conversationId && data.conversation_id) {
                        this.conversationId = data.conversation_id;
                    }
                } 
                else if (data.event === 'message_replace') {
                    // 内容审查替换,直接覆盖
                    this.messageList[msgIndex].content = data.answer;
                }
                else if (data.event === 'message_end') {
                    console.log('生成结束', data);
                    // 可以在这里处理 metadata,比如 token 消耗
                }
                else if (data.event === 'error') {
                    console.error('Dify 报错:', data);
                    this.messageList[msgIndex].content += "\n[出错: " + data.message + "]";
                }
                
                // 【重要】强制触发 Vue 视图更新(如果在某些层级深的结构中)
                // 这一步在 Vue2 中可能不需要,但在某些 UniApp 场景下需要
                // this.$forceUpdate(); 
                
            } catch (e) {
                // JSON 解析失败通常是因为数据包不完整(被截断),
                // 生产环境需要做一个 buffer 缓存上一块未解析完的字符串
                // 暂时忽略或存入 buffer
                console.log('JSON parse error (ignore partial chunk):', e);
            }
        }
    });
}

4. 常见坑排查清单

如果还是展示不出来,请按以下顺序检查:

  1. ArrayBuffer 解码乱码

    • UniApp 的 onChunkReceived 返回的是 ArrayBuffer。如果不进行 UTF-8 解码直接转字符串,中文会显示乱码或空白。
    • 解决 :确保使用了 TextDecoder 或者 decodeURIComponent(escape(String.fromCharCode(...))) 这种方式正确解码。
  2. Vue 响应式失效

    • 如果在 onChunkReceived 这种异步回调中,this 指向可能丢失。
    • 解决 :确保在外部定义了 const that = this;,或者使用箭头函数。
    • 解决 :如果是 Vue 2,修改数组索引可能不会触发视图更新。使用 this.$set(this.messageList, index, newValue) 或者直接修改对象属性 this.messageList[index].content += '...' 通常是有效的,但要确保 messageList 是在 data 中定义的。
  3. Markdown 渲染

    • Dify 输出的是 Markdown 格式(包含 **加粗**Code Block 等)。
    • 如果直接用 <text>{``{ content }}</text>,只能显示纯文本。
    • 建议 :在 UniApp 中引入 mp-htmltowxml 等组件来渲染 Markdown,这样能正确展示代码块和格式。
  4. JSON 解析报错

    • 流式传输网络抖动时,JSON 可能会被截断(比如 {"answer": "你好 后面断了)。
    • 解决 :需要实现一个 buffer 变量,如果 JSON.parse 失败,将当前字符串存起来,等下一个 chunk 来了拼接到头部再解析。
相关推荐
ywf121514 小时前
前端的dist包放到后端springboot项目下一起打包
前端·spring boot·后端
恋猫de小郭14 小时前
2026,Android Compose 终于支持 Hot Reload 了,但是收费
android·前端·flutter
hpoenixf20 小时前
2026 年前端面试问什么
前端·面试
还是大剑师兰特20 小时前
Vue3 中的 defineExpose 完全指南
前端·javascript·vue.js
泯泷20 小时前
阶段一:从 0 看懂 JSVMP 架构,先在脑子里搭出一台最小 JSVM
前端·javascript·架构
mengchanmian21 小时前
前端node常用配置
前端
华洛21 小时前
利好打工人,openclaw不是企业提效工具,而是个人助理
前端·javascript·产品经理
xkxnq21 小时前
第六阶段:Vue生态高级整合与优化(第93天)Element Plus进阶:自定义主题(变量覆盖)+ 全局配置与组件按需加载优化
前端·javascript·vue.js
A黄俊辉A1 天前
vue css中 :global的使用
前端·javascript·vue.js
小码哥_常1 天前
被EdgeToEdge适配折磨疯了,谁懂!
前端