后端返回sse流

背景

前端不需要流式展示,只接收最后的结果,简单做一个接收sse请求的功能

请求方式

const response = await fetch(${请求的url地址}, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json; charset=utf-8' },
    body: JSON.stringify({ ...请求的参数 }),
});

注意 流式返回时,response对象本身是固定不变的,它在fetch()返回的那一刻就确定了(包括 status、headers 等)。变化的是 response.body(ReadableStream)里的数据,它是一个流,数据会随着服务器持续推送而逐块到达。

对请求的处理

if (!response.ok) {
   // 错误处理
}

 /** iOS WebView 不支持 ReadableStream,用 text() 兜底 */
if (!response.body) {
    const text = await response.text();

    return parseSseText(text);  // 解析文本
}

return await readSseStream(response.body);    // 解析流

解析流

从 SSE 流中读取并解析审核结果

补充两个浏览器内置API

ReadableStream

是浏览器提供的一个 流式读取 API,可以把一大段数据拆成一小块一小块地读取,而不是等全部数据到齐再处理。

核心用法

// 1. 获取流
const stream: ReadableStream<Uint8Array> = response.body;

// 2. 获取 reader(读取器),一个流只能有一个 reader
const reader: ReadableStreamDefaultReader = stream.getReader();

// 3. 读取一块数据(异步,数据到了才返回)
const { value, done } = await reader.read();
//       value: Uint8Array    done: boolean(true=流结束)

TextDecoder

用于将二进制数据(Uint8Array)转换为字符串。

基本用法

const decoder = new TextDecoder();  // 默认 UTF-8 编码

// 普通解码
decoder.decode(new Uint8Array([72, 105]), { stream: true });  // → "Hi"

{ stream: true } 的意义
UTF-8 编码中,英文占 1 字节,中文占 3 字节。网络传输不关心字符边界,可能把一个字符切成两半:

服务器发送: "审核通过" (12字节)

第1次到达: [229, 174, 161, 230, 160, 184, 233, 128]     → 前 8 字节
第2次到达: [154, 232, 191, 135]                           → 后 4 字节

"通"字的 3 个字节 [233, 128, 154] 被切成了 [233, 128] 和 [154]:

// ❌ 不加 stream → 硬解码不完整字节,产生乱码 "�"
decoder.decode(Uint8Array[..., 233, 128])  // → "审核�"

// ✅ 加 stream: true → 内部暂存 [233, 128],等下次凑齐
decoder.decode(Uint8Array[..., 233, 128], { stream: true })  // → "审核"(暂存2字节)
decoder.decode(Uint8Array[154, ...], { stream: true })        // → "通过"(拼上暂存的)

核心需求代码

/** 从 SSE 流中读取并解析结果 */
const readSseStream = async (body) => {
  const reader = body.getReader();
  const decoder = new TextDecoder();
  let buffer = '';
  let result = {  };
  let done = false;

  while (!done) {
    const chunk = await reader.read();

    done = chunk.done;

    if (chunk.value) {
      buffer += decoder.decode(chunk.value, { stream: true });
    }

    /** SSE 事件以 \n\n 分隔 */
    const blocks = buffer.split('\n\n');

    buffer = blocks.pop() || '';

    for (const block of blocks) {
      const { eventType, dataStr } = parseSseBlock(block);
      const parsed = parseSseResult(eventType, dataStr);

      if (parsed) {
        result = parsed;
      }
    }
  }

  return result;
};

我这个需求的流式返回格式是这样的

event: COMPLETED
data: {"Code": 0, "Msg": "ok"}

所以parseSseBlock函数是这样的

/** 从 SSE 事件块文本中提取 event 和 data */
const parseSseBlock = (block) => {
  let eventType = '';
  let dataStr = '';

  for (const line of block.split('\n')) {
    if (line.startsWith('event: ')) {
      eventType = line.slice(7).trim();
    } else if (line.startsWith('data: ')) {
      dataStr = line.slice(6).trim();
    }
  }

  return { eventType, dataStr };
};

parseSseResult函数是这样的

/** 解析 SSE 事件块(event + data),提取结果 */
const parseSseResult = (eventType, dataStr) => {
  // 只过滤你需要的事件
  if (eventType !== 'xxx') return null;

  const parsed = JSON.parse(dataStr);

  if (!parsed) return null;

  return parsed;
};

兜底解析文本

/** iOS WebView 不支持 ReadableStream 时,用 text() 一次性读取后解析 SSE */
const parseSseText = (text) => {
  let result = { };
  const blocks = text.split('\n\n');

  for (const block of blocks) {
    const { eventType, dataStr } = parseSseBlock(block);
    const parsed = parseSseAuditResult(eventType, dataStr);

    if (parsed) {
      result = parsed;
    }
  }

  return result;
};
posted @ 2026-05-09 15:26  懒懒同学不懒  阅读(19)  评论(0)    收藏  举报