高效传输大的 JSON 数据,流式处理真香!

2024年 5月 16日 78.3k 0

什么是 TextDecoder API

TextDecoder[1] API 是一个用于将二进制数据(通常是 ArrayBuffer 或 TypedArray)解码为字符串的 JavaScript API。它是 Web 平台的一部分,主要用于处理文本编码的解码工作。比如,从服务器接收到的流式数据、文件数据等。

为什么使用 TextDecoder API

在处理 Web 应用中的二进制数据时,通常需要将这些数据转换为可读的字符串格式。TextDecoder 提供了一种高效且便捷的方法来实现这一点。

TextDecoder API 有以下特点:

  • 高效性:比手动逐字节处理高效,能够直接解码为字符串。
  • 支持多种编码:支持多种文本编码(如 UTF-8、UTF-16、ISO-8859-1 等),提供了灵活性。
  • 支持流式处理:能够逐块处理数据,适合处理大数据流或需要实时处理的数据。

如何使用 TextDecoder API

下面我们将介绍 TextDecoder API 的四种使用场景:

  • 解码 ArrayBuffer 数据
  • 解码不同编码的二进制数据
  • 解码流式 JSON 数据
  • 解码大 JSON 文件中的数据块
  • 1.解码 ArrayBuffer 数据

    // 创建一个包含 UTF-8 编码文本的 Uint8Array
    const uint8Array = new Uint8Array([72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100]);
    
    // 创建一个 TextDecoder 实例,默认使用 UTF-8 编码
    const decoder = new TextDecoder('utf-8');
    
    // 将 Uint8Array 解码为字符串
    const decodedString = decoder.decode(uint8Array);
    
    console.log(decodedString); // 输出 "Hello World"

    2.解码不同编码的二进制数据

    // 使用不同的编码创建 TextDecoder 实例
    const utf16Decoder = new TextDecoder('utf-16');
    const iso88591Decoder = new TextDecoder('iso-8859-1');
    
    // 示例二进制数据
    const utf16Array = new Uint16Array([0x0048, 0x0065, 0x006C, 0x006C, 0x006F]);
    const iso88591Array = new Uint8Array([72, 101, 108, 108, 111]);
    
    // 解码为字符串
    const utf16String = utf16Decoder.decode(utf16Array);
    const iso88591String = iso88591Decoder.decode(iso88591Array);
    
    console.log(utf16String); // 输出 "Hello"
    console.log(iso88591String); // 输出 "Hello"

    3.解码流式 JSON 数据

    首先,我们先来看一下效果:

    高效传输大的 JSON 数据,流式处理真香!-1图片

    在以上的示例中,我们使用 Node.js 的 http 模块,快速搭建一个本地 SSE[2](Server-Sent Events)服务器。

    server.js

    const http = require("http");
    
    const PORT = 3000;
    
    const server = http.createServer((req, res) => {
      if (req.url === "/sse") {
        res.writeHead(200, {
          "Content-Type": "text/event-stream",
          "Cache-Control": "no-cache",
          Connection: "keep-alive",
          "Access-Control-Allow-Origin": "*",
          "Access-Control-Allow-Headers":
            "Origin, X-Requested-With, Content-Type, Accept",
        });
    
        let id = 1;
        const interval = setInterval(() => {
          const data = {
            id: id,
            message: `This is message ${id}`,
            timestamp: +new Date(),
          };
          res.write(`data: ${JSON.stringify(data)}\n\n`);
    
          // 停止条件
          if (id == 5) {
            res.write("event: end\n");
            res.write("data: End of stream\n\n");
            clearInterval(interval);
            res.end();
          }
    
          id++;
        }, 1000);
    
        req.on("close", () => {
          clearInterval(interval);
        });
      } else {
        res.writeHead(404, { "Content-Type": "text/plain" });
        res.end("404 Not Found");
      }
    });
    
    server.listen(PORT, () => {
      console.log(`Server is running on http://localhost:${PORT}`);
    });

    需要注意的是,在 sse 接口中,我们设置 Content-Type 响应头的类型为: "text/event-stream",告诉客户端我们返回的是流式数据。

    index.html

    ⚠️:访问 index.html 网页前,记得运行 node server.js 命令先启动 SSE 服务器。

    
    
    
    
        
        
        SSE & TextDecoder
    
    
        Decode Server-Sent Events JSON Stream Data
        
        
    
    

    client.js

    document.addEventListener("DOMContentLoaded", () => {
      const messagesDiv = document.querySelector("#messages");
      const textDecoder = new TextDecoder("utf-8");
    
      fetch("http://localhost:3000/sse").then((response) => {
        const reader = response.body.getReader();
        return new ReadableStream({
          start(controller) {
            function push() {
              reader.read().then(({ done, value }) => {
                if (done) {
                  controller.close();
                  return;
                }
    
                const chunk = textDecoder.decode(value, { stream: true });
                const lines = chunk.split("\n");
    
                for (const line of lines) {
                  if (line.startsWith("data: ")) {
                    const json = line.slice(6);
                    const data = JSON.parse(json);
                    const p = document.createElement("p");
                    p.textContent = `ID: ${data.id}, Message: ${data.message}, Timestamp: ${data.timestamp}`;
                    messagesDiv.appendChild(p);
                  } else if (line.startsWith("event: end")) {
                    const p = document.createElement("p");
                    p.textContent = "End of stream";
                    messagesDiv.appendChild(p);
                    return;
                  }
                }
    
                push();
              });
            }
    
            push();
          },
        });
      });
    });

    SSE 事件流是一个简单的文本数据流,文本是使用 UTF-8 格式的编码。所以,在创建 textDecoder 对象时,我们需要设置它的编码为 utf-8。有了 textDecoder 对象后,调用它提供的 decode 方法就可以进行解码了。

    4.解码大 JSON 文件中的数据块

    当遇到传输大 JSON 数据的时候,如果使用传统的方式,我们需要接收完整的 JSON 数据后,才能开始进行数据处理,这对用户体验会造成一定的影响。为了解决这个问题,我们可以使用一些现成的 JSON Stream 解析库。比如,@streamparser/json[3],该库内部也使用了本文介绍的 TextDecoder API。

    下面,我们先来看一下效果:

    上图中输出的 JSON 数据来自以下的 large.json 文件。我们对该文件按照 0.5KB 的大小进行切割,然后每个 500ms 发送下一个数据块。利用 @streamparser/json 这个库,我们实现了解析 JSON 数据流(JSON Chunk)的功能。

    large.json

    [
      {},
      {
        "image": [
          {
            "shape": "rect",
            "fill": "#333",
            "stroke": "#999",
            "x": 0.5e1,
            "y": 0.5,
            "z": 0.8,
            "w": 0.5e5,
            "u": 2e10,
            "foo": 2e1,
            "bar": 2,
            "width": 47,
            "height": 47
          }
        ],
        "corners": { "1": true, "3": true, "7": true, "9": true }
      },
     ...
    ]

    json-server.js

    const http = require("http");
    const { join } = require("path");
    const { readFileSync } = require("fs");
    
    const PORT = 3000;
    
    const largeJson = readFileSync(join(__dirname, "large.json")).toString();
    
    const server = http.createServer((req, res) => {
      if (req.url === "/stream-json") {
        res.writeHead(200, {
          "Content-Type": "application/json",
          "Cache-Control": "no-cache",
          Connection: "keep-alive",
          "Access-Control-Allow-Origin": "*",
          "Access-Control-Allow-Headers":
            "Origin, X-Requested-With, Content-Type, Accept",
        });
    
        const CHUNK_SIZE = 512; // 每块 0.5KB
        let position = 0;
    
        const interval = setInterval(() => {
          const chunk = largeJson.slice(position, position + CHUNK_SIZE);
          res.write(chunk);
          position += CHUNK_SIZE;
    
          if (position >= largeJson.length) {
            clearInterval(interval);
            res.end();
          }
        }, 500); // 每 500ms 发送一块数据
    
        req.on("close", () => {
          clearInterval(interval);
        });
      } else {
        res.writeHead(404, { "Content-Type": "text/plain" });
        res.end("404 Not Found");
      }
    });
    
    server.listen(PORT, () => {
      console.log(`Server is running on http://localhost:${PORT}`);
    });

    stream-json.html

    
    
      
        
        
        Stream JSON
      
      
        Stream JSON
        
        
      
    

    @streamparser/json 这个库还有其他的用法,如果你感兴趣的话,可以看一下它的使用文档。

    参考资料

    [1]TextDecoder: https://developer.mozilla.org/zh-CN/docs/Web/API/TextDecoder

    [2]SSE: https://developer.mozilla.org/zh-CN/docs/Web/API/Server-sent_events

    [3]@streamparser/json: https://www.npmjs.com/package/@streamparser/json

    相关文章

    JavaScript2024新功能:Object.groupBy、正则表达式v标志
    PHP trim 函数对多字节字符的使用和限制
    新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
    使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
    为React 19做准备:WordPress 6.6用户指南
    如何删除WordPress中的所有评论

    发布评论