深入理解前端流式输出与二进制编码:从 TextEncoder 到 Buffer 操作

67 阅读7分钟

一、为什么需要流式输出?

在传统 Web 请求中,后端生成完整响应后才一次性返回给前端(即“非流式”)。但当响应内容很长(比如 AI 生成一篇千字作文),用户会经历漫长的等待——首字节延迟高,体验差

✅ 流式输出的优势:

  • 边生成边返回:LLM 每生成一个 token 就立即推送给前端。
  • 降低感知延迟:用户几乎立刻看到第一个字,心理等待感大幅下降。
  • 节省内存:后端无需缓存完整结果,前端也无需一次性接收大块数据。

📌 技术关键:前后端需支持流式协议(如 HTTP/1.1 的 chunked transfer 或 HTTP/2 Server Push),前端通过 ReadableStreamfetch().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) 会让viewbuffer共享同一块二进制内存 —— 修改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() 编码
Uint8Array12个字节) 
    ↓ 循环赋值到Uint8Array视图
ArrayBuffer12字节二进制缓冲区) 
    ↓ TextDecoder.decode() 解码
字符串(你好 HTML5)
    ↓ DOM操作
页面展示二进制数据、字节长度、原始文本

五、关键补充(易混淆点)

  1. ArrayBuffer vs TypedArray:

    • ArrayBuffer:是 “数据仓库”(存原始二进制),不能直接操作;
    • TypedArray:是 “操作工具”(视图),通过它读写 ArrayBuffer;
  2. TextEncoder/Decoder 的编码格式:

    • 默认是 UTF-8(几乎所有场景都用这个),也可以指定其他编码(如new TextEncoder('gbk'),但兼容性差);
  3. 为什么用 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 像素处理等场景的核心基础。