后端返回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;
};

浙公网安备 33010602011771号