一、为什么需要流式输出?
在传统 Web 请求中,后端生成完整响应后才一次性返回给前端(即“非流式”)。但当响应内容很长(比如 AI 生成一篇千字作文),用户会经历漫长的等待——首字节延迟高,体验差。
✅ 流式输出的优势:
- 边生成边返回:LLM 每生成一个 token 就立即推送给前端。
- 降低感知延迟:用户几乎立刻看到第一个字,心理等待感大幅下降。
- 节省内存:后端无需缓存完整结果,前端也无需一次性接收大块数据。
📌 技术关键:前后端需支持流式协议(如 HTTP/1.1 的 chunked transfer 或 HTTP/2 Server Push),前端通过
ReadableStream或fetch().body.getReader()逐块读取。
二、前端如何处理流式文本?从字符到二进制
虽然网络传输的是字节流(二进制) ,但开发者更习惯操作字符串。这就需要编码(encode)与解码(decode)。
核心工具:HTML5 的 TextEncoder 与 TextDecoder
| 对象 | 作用 | 输入 → 输出 |
|---|---|---|
TextEncoder | 将字符串 → UTF-8 编码的 Uint8Array | "你好" → [228, 189, 160, 229, 165, 189] |
TextDecoder | 将 ArrayBuffer/Uint8Array → 字符串 | 上述数组 → "你好" |
💡 UTF-8 是 Web 默认编码,兼容 ASCII,且能表示全球所有字符。
三、逐行解析你的示例代码
html
预览
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>HTML Buffer</title>
</head>
<body>
<h1>Buffer</h1>
<div id="output"></div>
<script>
// 1. 创建编码器
const encoder = new TextEncoder();
console.log(encoder); // TextEncoder { encoding: "utf-8" }
// 2. 将字符串编码为 Uint8Array(本质是二进制字节序列)
const myBuffer = encoder.encode('你好 HTML5');
console.log(myBuffer); // Uint8Array(12) [228, 189, 160, 229, 165, 189, 32, 72, 84, 77, 76, 53]
// 3. 手动创建一个 ArrayBuffer(原始二进制缓冲区,12字节)
const buffer = new ArrayBuffer(12);
const view = new Uint8Array(buffer); // 创建视图,以便按字节操作
// 4. 将编码后的字节复制到 buffer 中
for (let i = 0; i < myBuffer.length; i++) {
view[i] = myBuffer[i];
}
// 5. 解码:从 ArrayBuffer 恢复原始字符串
const decoder = new TextDecoder();
const originalText = decoder.decode(buffer);
console.log(originalText); // "你好 HTML5"
// 6. 页面展示结果
const outputDiv = document.getElementById('output');
outputDiv.innerHTML = `
完整数据:[${view}] <br>
第一个字节: ${view[0]} <br>
缓存区的字节长度: ${buffer.byteLength} <br>
原来的文本: ${originalText}
`;
</script>
</body>
</html>
🔍 关键知识点拆解
三、JS 核心逻辑拆解(重点知识点)
1. TextEncoder:文本 → 二进制(UTF-8 编码)
javascript
运行
// 1. 创建TextEncoder实例:默认以UTF-8编码将字符串转为二进制
const encoder = new TextEncoder();
console.log(encoder); // 打印编码器实例,可看其属性(如encoding: "utf-8")
// 2. 编码字符串:将"你好 HTML5"转为UTF-8格式的Uint8Array(8位无符号整数数组)
const myBuffer = encoder.encode('你好 HTML5');
console.log(myBuffer);
核心知识点:TextEncoder
-
作用:属于 HTML5 新增的 Web API,专门将字符串按照指定编码(默认 UTF-8)转换为二进制数据;
-
encode(str)方法返回值:Uint8Array类型(TypedArray 的一种),每个元素是 0-255 的整数,对应字符串的 UTF-8 编码字节; -
编码规则:
- 中文 “你”“好”:UTF-8 下每个中文字占 3 字节;
- 空格、H、T、M、L、5:都是 ASCII 字符,每个占 1 字节;
- 所以 "你好 HTML5" 的字节数:3 (你)+3 (好)+1 (空格)+1 (H)+1 (T)+1 (M)+1 (L)+1 (5) = 12 字节(这是后续创建 12 字节缓冲区的原因)。
2. ArrayBuffer:二进制原始缓冲区
javascript
运行
// 创建一个12字节的二进制原始缓冲区(仅存数据,不能直接操作)
const buffer = new ArrayBuffer(12);
核心知识点:ArrayBuffer
-
作用:表示一段固定长度的二进制数据缓冲区,是 JS 操作二进制数据的 “底层容器”;
-
特点:
- 不能直接读写 / 修改里面的内容(比如
buffer[0] = 123会报错); - 仅存储原始二进制数据,需要通过 “视图(TypedArray/DataView)” 来操作;
byteLength属性:返回缓冲区的字节长度(这里是 12)。
- 不能直接读写 / 修改里面的内容(比如
3. Uint8Array:TypedArray 视图(操作 ArrayBuffer)
javascript
运行
// 创建Uint8Array视图,关联到上面的12字节缓冲区(8位无符号整数视图)
const view = new Uint8Array(buffer);
// 遍历myBuffer(编码后的字节数组),把每个字节赋值到view中(即写入buffer)
for (let i = 0; i < myBuffer.length; i++) {
view[i] = myBuffer[i];
}
核心知识点:TypedArray(以 Uint8Array 为例)
-
什么是 TypedArray:“类型化数组”,是操作 ArrayBuffer 的视图(相当于 “二进制数据的操作接口”);
-
Uint8Array 的含义:
Uint:无符号整数(只能存 0-255,符合 UTF-8 字节范围);8:每个元素占 8 位(1 字节);
-
关联逻辑:
new Uint8Array(buffer)会让view和buffer共享同一块二进制内存 —— 修改view的元素,就是修改buffer里的二进制数据; -
循环赋值的作用:把 “你好 HTML5” 编码后的 12 个字节,逐个写入到 12 字节的
buffer中。
4. TextDecoder:二进制 → 文本(UTF-8 解码)
javascript
运行
// 创建TextDecoder实例:默认以UTF-8解码二进制数据为字符串
const decoder = new TextDecoder();
// 解码buffer中的二进制数据,还原为原始字符串
const originalText = decoder.decode(buffer);
console.log(originalText); // 输出:你好 HTML5
核心知识点:TextDecoder
- 作用:和 TextEncoder 反向,将二进制数据(ArrayBuffer/TypedArray)按照指定编码(默认 UTF-8)解码为字符串;
decode(buffer)方法:入参可以是 ArrayBuffer、TypedArray 等二进制数据,返回解码后的字符串;- 关键:解码的编码格式必须和编码时一致(这里都是 UTF-8),否则会出现乱码。
5. 页面展示结果(DOM 操作)
javascript
运行
// 获取页面上的output容器
const outputDiv = document.getElementById('output');
// 向容器插入HTML,展示关键信息
outputDiv.innerHTML = `
完整数据:[${view}] <br>
第一个字节: ${view[0]} <br>
缓存区的字节长度: ${buffer.byteLength} <br>
原来的文本: ${originalText}
`;
逻辑说明:
-
document.getElementById('output'):获取 DOM 元素(因为脚本在 body 末尾,此时元素已加载); -
模板字符串
`...`:拼接 HTML 内容,展示:[${view}]:Uint8Array 转为字符串,显示 12 个字节的具体数值(比如 “你” 的 UTF-8 编码是 [228,189,160],“好” 是 [229,165,189],后续是空格、H/T/M/L/5 的 ASCII 码);view[0]:第一个字节的数值(即 “你” 的第一个 UTF-8 字节:228);buffer.byteLength:缓冲区的字节长度(12);originalText:解码后的原始字符串(你好 HTML5)。
四、整体流程总结
plaintext
字符串(你好 HTML5)
↓ TextEncoder.encode() 编码
Uint8Array(12个字节)
↓ 循环赋值到Uint8Array视图
ArrayBuffer(12字节二进制缓冲区)
↓ TextDecoder.decode() 解码
字符串(你好 HTML5)
↓ DOM操作
页面展示二进制数据、字节长度、原始文本
五、关键补充(易混淆点)
-
ArrayBuffer vs TypedArray:
- ArrayBuffer:是 “数据仓库”(存原始二进制),不能直接操作;
- TypedArray:是 “操作工具”(视图),通过它读写 ArrayBuffer;
-
TextEncoder/Decoder 的编码格式:
- 默认是 UTF-8(几乎所有场景都用这个),也可以指定其他编码(如
new TextEncoder('gbk'),但兼容性差);
- 默认是 UTF-8(几乎所有场景都用这个),也可以指定其他编码(如
-
为什么用 12 字节缓冲区:
- 因为 “你好 HTML5” 编码后正好 12 字节,缓冲区长度和数据长度一致,不会浪费也不会不够用。
六、运行结果说明
打开页面后:
-
控制台会打印
encoder实例、myBuffer(编码后的 Uint8Array)、originalText(你好 HTML5); -
页面上会显示:
plaintext
完整数据:[228,189,160,229,165,189,32,72,84,77,76,53] 第一个字节: 228 缓存区的字节长度: 12 原来的文本: 你好 HTML5其中:
- 228,189,160 → “你” 的 UTF-8 编码;
- 229,165,189 → “好” 的 UTF-8 编码;
- 32 → 空格的 ASCII 码;
- 72 → H 的 ASCII 码(H 的十进制是 72);
- 84 → T 的 ASCII 码;
- 77 → M 的 ASCII 码;
- 76 → L 的 ASCII 码;
- 53 → 5 的 ASCII 码。
通过这份解析,你可以理解:JS 如何将文本转二进制、如何存储二进制、如何还原文本—— 这是前端处理文件上传 / 下载、WebSocket 二进制通信、Canvas 像素处理等场景的核心基础。