高效傳輸大的 JSON 數(shù)據(jù),流式處理真香!
什么是 TextDecoder API
TextDecoder[1] API 是一個(gè)用于將二進(jìn)制數(shù)據(jù)(通常是 ArrayBuffer 或 TypedArray)解碼為字符串的 JavaScript API。它是 Web 平臺(tái)的一部分,主要用于處理文本編碼的解碼工作。比如,從服務(wù)器接收到的流式數(shù)據(jù)、文件數(shù)據(jù)等。
為什么使用 TextDecoder API
在處理 Web 應(yīng)用中的二進(jìn)制數(shù)據(jù)時(shí),通常需要將這些數(shù)據(jù)轉(zhuǎn)換為可讀的字符串格式。TextDecoder 提供了一種高效且便捷的方法來實(shí)現(xiàn)這一點(diǎn)。
TextDecoder API 有以下特點(diǎn):
- 高效性:比手動(dòng)逐字節(jié)處理高效,能夠直接解碼為字符串。
- 支持多種編碼:支持多種文本編碼(如 UTF-8、UTF-16、ISO-8859-1 等),提供了靈活性。
- 支持流式處理:能夠逐塊處理數(shù)據(jù),適合處理大數(shù)據(jù)流或需要實(shí)時(shí)處理的數(shù)據(jù)。
如何使用 TextDecoder API
下面我們將介紹 TextDecoder API 的四種使用場(chǎng)景:
- 解碼 ArrayBuffer 數(shù)據(jù)
- 解碼不同編碼的二進(jìn)制數(shù)據(jù)
- 解碼流式 JSON 數(shù)據(jù)
- 解碼大 JSON 文件中的數(shù)據(jù)塊
1.解碼 ArrayBuffer 數(shù)據(jù)
// 創(chuàng)建一個(gè)包含 UTF-8 編碼文本的 Uint8Array
const uint8Array = new Uint8Array([72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100]);
// 創(chuàng)建一個(gè) TextDecoder 實(shí)例,默認(rèn)使用 UTF-8 編碼
const decoder = new TextDecoder('utf-8');
// 將 Uint8Array 解碼為字符串
const decodedString = decoder.decode(uint8Array);
console.log(decodedString); // 輸出 "Hello World"
2.解碼不同編碼的二進(jìn)制數(shù)據(jù)
// 使用不同的編碼創(chuàng)建 TextDecoder 實(shí)例
const utf16Decoder = new TextDecoder('utf-16');
const iso88591Decoder = new TextDecoder('iso-8859-1');
// 示例二進(jìn)制數(shù)據(jù)
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 數(shù)據(jù)
首先,我們先來看一下效果:
圖片
在以上的示例中,我們使用 Node.js 的 http 模塊,快速搭建一個(gè)本地 SSE[2](Server-Sent Events)服務(wù)器。
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 接口中,我們?cè)O(shè)置 Content-Type 響應(yīng)頭的類型為: "text/event-stream",告訴客戶端我們返回的是流式數(shù)據(jù)。
index.html
??:訪問 index.html 網(wǎng)頁前,記得運(yùn)行 node server.js 命令先啟動(dòng) SSE 服務(wù)器。
<!-- public/index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SSE & TextDecoder</title>
</head>
<body>
<h1>Decode Server-Sent Events JSON Stream Data</h1>
<div id="messages"></div>
<script src="client.js"></script>
</body>
</html>
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 事件流是一個(gè)簡(jiǎn)單的文本數(shù)據(jù)流,文本是使用 UTF-8 格式的編碼。所以,在創(chuàng)建 textDecoder 對(duì)象時(shí),我們需要設(shè)置它的編碼為 utf-8。有了 textDecoder 對(duì)象后,調(diào)用它提供的 decode 方法就可以進(jìn)行解碼了。
4.解碼大 JSON 文件中的數(shù)據(jù)塊
當(dāng)遇到傳輸大 JSON 數(shù)據(jù)的時(shí)候,如果使用傳統(tǒng)的方式,我們需要接收完整的 JSON 數(shù)據(jù)后,才能開始進(jìn)行數(shù)據(jù)處理,這對(duì)用戶體驗(yàn)會(huì)造成一定的影響。為了解決這個(gè)問題,我們可以使用一些現(xiàn)成的 JSON Stream 解析庫。比如,@streamparser/json[3],該庫內(nèi)部也使用了本文介紹的 TextDecoder API。
下面,我們先來看一下效果:
上圖中輸出的 JSON 數(shù)據(jù)來自以下的 large.json 文件。我們對(duì)該文件按照 0.5KB 的大小進(jìn)行切割,然后每個(gè) 500ms 發(fā)送下一個(gè)數(shù)據(jù)塊。利用 @streamparser/json 這個(gè)庫,我們實(shí)現(xiàn)了解析 JSON 數(shù)據(jù)流(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 發(fā)送一塊數(shù)據(jù)
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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Stream JSON</title>
</head>
<body>
<h1>Stream JSON</h1>
<div id="messages"></div>
<script type="module">
import { JSONParser } from "https://cdn.jsdelivr.net/npm/@streamparser/json-whatwg@0.0.21/+esm";
const messagesDiv = document.querySelector("#messages");
document.addEventListener("DOMContentLoaded", async () => {
const parser = new JSONParser();
const response = await fetch("http://localhost:3000/stream-json");
const reader = response.body.pipeThrough(parser).getReader();
while (true) {
const { done, value: parsedElementInfo } = await reader.read();
if (done) break;
const { value, key, parent, stack, partial } = parsedElementInfo;
if (partial) {
console.log(`Parsing value: ${value}... (still parsing)`);
} else {
const p = document.createElement("p");
p.textContent = `${JSON.stringify(value)}`;
messagesDiv.appendChild(p);
console.log(`Value parsed: ${JSON.stringify(value)}`);
}
}
});
</script>
</body>
</html>
@streamparser/json 這個(gè)庫還有其他的用法,如果你感興趣的話,可以看一下它的使用文檔。
參考資料
[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