啊!!!Stream API 居然这么好用!
在前端开发中,我们总会遇到 “大文件”“实时数据” 的场景:比如上传 1GB 的视频、加载 10 万条数据的 API 响应、实时接收后端日志…… 此时,一次性加载全量数据的方案(比如 Blob)往往会导致内存溢出、页面卡顿。
然而,有一个 API 却能轻松解决这些问题 —— 它就是Stream API。它像 “流水处理器” 一样,将数据拆分成小块逐个处理,既节省内存,又能实现实时交互,却常常被开发者忽略。
今天,就让我们揭开 Stream API 的面纱,看看它到底有多实用。
一、Stream API 是什么?
从字面意思看,Stream 是 “流” 的意思。在 JavaScript 中,Stream API 是一套处理 “流数据” 的标准接口,它将连续的数据拆分为可逐个处理的 “块(Chunk)”,而非一次性加载全部数据。
核心优势
- 低内存占用:无需加载全量数据,仅处理当前块,避免大文件导致的内存溢出。
- 实时性:数据到达后可立即处理,无需等待全部传输完成(如实时渲染日志、边下载边播放)。
- 灵活性:支持数据转换(压缩、加密)、分流、合并等复杂操作。
核心对象
Stream API 的核心由三个对象组成,覆盖 “数据来源→数据处理→数据目的地” 全流程:
- ReadableStream:可读流,数据的 “来源”(如文件、API 响应、WebSocket 消息)。
- WritableStream:可写流,数据的 “目的地”(如 DOM、Blob、服务器请求)。
- TransformStream:转换流,数据的 “处理器”(如压缩、加密、格式转换),连接可读流和可写流。
简单示例:创建一个基础可读流
// 创建一个简单的可读流,每秒产生一个数字
const readableStream = new ReadableStream({
start(controller) {
let count = 1;
const timer = setInterval(() => {
controller.enqueue(count); // 向流中添加数据块
if (count >= 5) {
clearInterval(timer);
controller.close(); // 数据生成完成,关闭流
}
count++;
}, 1000);
}
});
// 读取流中的数据
const reader = readableStream.getReader();
async function readStream() {
while (true) {
const { done, value } = await reader.read();
if (done) break; // 流已关闭,停止读取
console.log("读取到数据:", value); // 依次输出 1、2、3、4、5
}
}
readStream();
二、Stream API 的 “超能力” 有哪些?
Stream API 的真正价值,在于解决传统方案(如 Blob、全量 JSON)无法应对的场景,每个能力都附带可直接复用的代码示例。
1. 大文件分片上传:避免超时与内存溢出
传统大文件上传需用Blob.slice()手动分片,而 Stream API 可自动分块,且支持暂停 / 恢复,更适合 1GB + 的视频、压缩包等文件。
// 大文件分片上传实现(5MB/块)
const fileInput = document.getElementById("file-input");
fileInput.addEventListener("change", async (e) => {
const file = e.target.files[0];
if (!file) return;
const chunkSize = 5 * 1024 * 1024; // 5MB每块
const totalChunks = Math.ceil(file.size / chunkSize);
let currentChunk = 0;
const fileName = file.name;
// 1. 创建可读流,分块读取文件
const fileStream = file.stream(); // 文件自带的ReadableStream
const chunkReader = fileStream.getReader();
// 2. 循环读取并上传每一块
while (true) {
const { done, value: chunkBuffer } = await chunkReader.read();
if (done) break;
// 3. 构建分片请求参数
const formData = new FormData();
formData.append("fileChunk", new Blob([chunkBuffer]), `${fileName}.chunk`);
formData.append("fileName", fileName);
formData.append("chunkIndex", currentChunk);
formData.append("totalChunks", totalChunks);
// 4. 上传当前分片(需后端配合接收分片)
await fetch("/api/upload-chunk", {
method: "POST",
body: formData,
});
// 5. 更新上传进度
currentChunk++;
const progress = (currentChunk / totalChunks) * 100;
document.getElementById("progress").textContent = `上传进度:${progress.toFixed(2)}%`;
}
// 6. 通知后端合并所有分片
await fetch("/api/merge-chunks", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ fileName, totalChunks }),
});
alert("大文件上传完成!");
});
2. 实时解析大 API 响应:边加载边渲染
如果 API 返回 10 万条数据的 JSON 列表,传统方案需等待全部数据加载完才能渲染,导致页面 “卡死”。Stream API 可边接收数据边解析,实现 “数据到了就渲染”。
// 实时解析大JSON响应,边接收边渲染表格
async function loadLargeTableData() {
const tableBody = document.getElementById("data-table-body");
try {
const response = await fetch("/api/large-user-list", {
headers: { "Accept": "application/json" },
});
if (!response.ok) throw new Error("请求失败");
if (!response.body) throw new Error("浏览器不支持流响应");
// 1. 获取流读取器和解码器(处理UTF-8文本)
const reader = response.body.getReader();
const decoder = new TextDecoder("utf-8");
let partialData = ""; // 存储未解析的部分数据(防止JSON被截断)
// 2. 循环读取流数据
while (true) {
const { done, value: chunk } = await reader.read();
if (done) break;
// 3. 解码当前块并拼接未解析部分
const text = partialData + decoder.decode(chunk, { stream: true });
// 分割JSON数组(假设响应格式为 [{id:1}, {id:2}, ...])
const items = text.split(/,(?={)/); // 按逗号分割,确保分割在对象开头前
// 4. 渲染已完整的JSON对象
for (let i = 0; i < items.length - 1; i++) {
const itemStr = items[i].replace(/^[[{]+|[]}]+$/g, ""); // 去除前后括号
if (!itemStr) continue;
const user = JSON.parse(itemStr);
// 实时添加表格行
const row = document.createElement("tr");
row.innerHTML = `<td>${user.id}</td><td>${user.name}</td><td>${user.age}</td>`;
tableBody.appendChild(row);
}
// 5. 保存最后一个可能不完整的项,供下一次拼接
partialData = items[items.length - 1];
}
// 6. 处理最后剩余的部分数据
if (partialData) {
const itemStr = partialData.replace(/^[[{]+|[]}]+$/g, "");
if (itemStr) {
const user = JSON.parse(itemStr);
const row = document.createElement("tr");
row.innerHTML = `<td>${user.id}</td><td>${user.name}</td><td>${user.age}</td>`;
tableBody.appendChild(row);
}
}
console.log("所有数据渲染完成");
} catch (err) {
console.error("处理失败:", err);
}
}
// 调用函数加载数据
loadLargeTableData();
3. 前端实时压缩文本:减少传输体积
无需等待全量文本输入,可通过TransformStream实时压缩数据(如日志、富文本),减少后续上传的流量。
// 用TransformStream实时压缩文本(依赖pako库:https://github.com/nodeca/pako)
import pako from "pako";
async function compressTextStream(text) {
// 1. 创建可读流(文本来源)
const readable = new ReadableStream({
start(controller) {
controller.enqueue(text);
controller.close();
},
});
// 2. 创建转换流(压缩文本)
const transform = new TransformStream({
transform(chunk, controller) {
// 压缩当前文本块(gzip格式)
const compressed = pako.gzip(chunk, { to: "Uint8Array" });
controller.enqueue(compressed);
},
});
// 3. 创建可写流(接收压缩后的数据)
const writable = new WritableStream({
start() {
this.compressedData = [];
},
write(chunk) {
this.compressedData.push(chunk);
},
close() {
// 合并压缩块,生成Blob(可用于下载或上传)
const compressedBlob = new Blob(this.compressedData);
console.log("压缩前大小:", text.length, "字节");
console.log("压缩后大小:", compressedBlob.size, "字节");
// 下载压缩文件
const url = URL.createObjectURL(compressedBlob);
const a = document.createElement("a");
a.href = url;
a.download = "compressed.txt.gz";
a.click();
URL.revokeObjectURL(url);
},
});
// 4. 连接流:可读流 → 转换流 → 可写流
await readable.pipeThrough(transform).pipeTo(writable);
}
// 测试:压缩一段长文本
const longText = "这是一段很长很长的文本...".repeat(1000);
compressTextStream(longText);
4. 实时接收后端日志:SSE + Stream 组合
后端通过 SSE(Server-Sent Events)推送实时日志时,Stream API 可逐行处理日志,避免日志堆积导致的页面卡顿。
// 实时接收并展示后端SSE日志
function loadRealTimeLogs() {
const logContainer = document.getElementById("log-container");
const sse = new EventSource("/api/realtime-logs"); // 建立SSE连接
// 监听SSE消息(后端每次推送一条日志)
sse.onmessage = (event) => {
// 创建可读流处理单条日志(可扩展为批量处理)
const logStream = new ReadableStream({
start(controller) {
controller.enqueue(event.data);
controller.close();
},
});
// 读取流并渲染日志
const reader = logStream.getReader();
reader.read().then(({ done, value: log }) => {
if (done) return;
// 给不同级别日志添加颜色(假设日志格式为 "INFO: 内容")
const [level, content] = log.split(": ", 2);
const logElement = document.createElement("div");
logElement.className = `log log-${level.toLowerCase()}`;
logElement.textContent = `[${new Date().toLocaleTimeString()}] ${log}`;
logContainer.appendChild(logElement);
// 滚动到最新日志
logContainer.scrollTop = logContainer.scrollHeight;
});
};
// 处理SSE错误
sse.onerror = (err) => {
console.error("SSE连接错误:", err);
sse.close();
};
}
// 启动实时日志接收
loadRealTimeLogs();
三、Stream + 其他 API:强强联合
Stream API 很少单独使用,与 Fetch、Blob、Web Worker 等组合后,能发挥更强大的作用。
1. Stream + Fetch + Blob:大文件边下载边保存
下载 GB 级文件时,传统方案会将全量数据存入内存,导致页面崩溃。通过 Stream 可边下载边生成 Blob,最终通过a标签触发下载。
// 大文件边下载边保存,避免内存溢出
async function downloadLargeFile(fileUrl, fileName) {
try {
const response = await fetch(fileUrl);
if (!response.ok) throw new Error("下载失败");
if (!response.body) throw new Error("浏览器不支持流下载");
// 1. 创建可写流,将数据写入Blob
const blobWriter = new WritableStream({
start() {
this.blobParts = [];
},
write(chunk) {
this.blobParts.push(chunk); // 逐块添加到Blob数组
},
close() {
// 合并所有块为Blob,触发下载
const blob = new Blob(this.blobParts);
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = fileName;
a.click();
URL.revokeObjectURL(url);
},
});
// 2. 连接流:Fetch响应流 → 可写流(Blob)
await response.body.pipeTo(blobWriter);
console.log("文件下载完成");
} catch (err) {
console.error("下载错误:", err);
}
}
// 调用:下载1GB的视频文件
downloadLargeFile("https://example.com/large-video.mp4", "my-video.mp4");
2. Stream + Web Worker:流处理不阻塞主线程
处理大文件流(如加密、解析)时,会占用大量 CPU,导致页面卡顿。将流处理逻辑放入 Web Worker,可避免阻塞主线程。
// 主线程:将文件流传给Worker处理
const fileInput = document.getElementById("file-input");
fileInput.addEventListener("change", async (e) => {
const file = e.target.files[0];
if (!file) return;
const worker = new Worker("stream-processor.js");
// 将文件流传给Worker(Stream可通过结构化克隆传递)
worker.postMessage({ type: "PROCESS_STREAM", stream: file.stream() });
// 接收Worker处理后的结果
worker.onmessage = (e) => {
if (e.data.type === "PROCESS_DONE") {
const { resultBlob } = e.data;
// 下载处理后的文件
const url = URL.createObjectURL(resultBlob);
const a = document.createElement("a");
a.href = url;
a.download = `processed-${file.name}`;
a.click();
URL.revokeObjectURL(url);
worker.terminate();
}
};
});
// stream-processor.js(Web Worker)
self.onmessage = async (e) => {
if (e.data.type !== "PROCESS_STREAM") return;
const { stream } = e.data;
const blobParts = [];
// 1. 创建转换流(示例:给文本文件添加水印)
const watermarkTransform = new TransformStream({
transform(chunk, controller) {
const decoder = new TextDecoder("utf-8");
const encoder = new TextEncoder();
// 给每块文本添加水印
const text = decoder.decode(chunk) + "\n[已处理]";
controller.enqueue(encoder.encode(text));
},
});
// 2. 创建可写流,收集处理后的数据
const writable = new WritableStream({
write(chunk) {
blobParts.push(chunk);
},
close() {
// 生成处理后的Blob,发送给主线程
const resultBlob = new Blob(blobParts, { type: "text/plain" });
self.postMessage({ type: "PROCESS_DONE", resultBlob });
},
});
// 3. 处理流:可读流 → 转换流 → 可写流
await stream.pipeThrough(watermarkTransform).pipeTo(writable);
};
四、Stream API 的局限与注意事项
虽然 Stream API 很强大,但在使用时仍需注意以下问题:
- 兼容性:现代浏览器(Chrome 63+、Firefox 65+、Safari 14.1+)支持良好,但IE 完全不支持,旧版 Edge(≤18)也不支持。如需兼容,可使用@streamparser/json(JSON 流解析)、web-streams-polyfill(全量 polyfill)等库。
- 学习成本:相比 Blob 的 “创建→使用” 简单流程,Stream 涉及Readable/Writable/Transform三个对象,且需理解 “流状态(暂停 / 继续 / 关闭)”“背压(Backpressure,防止写入过快)” 等概念,逻辑更复杂。
- 错误处理:流处理过程中可能出现断流(如网络断开、文件损坏),需在start/transform/write方法中监听error事件,并手动处理重试逻辑(如重新连接流、跳过损坏块)。
- 服务器端配合:部分场景(如分片上传、SSE 流)需要后端支持对应的协议(如接收分片并合并、返回text/event-stream类型响应),前端单独使用 Stream 无法实现完整功能。
五、结语:Stream API 是前端的 “流处理专家”
Stream API 不是最炫酷的前端技术,没有 Vue/React 那样的生态,也没有 Tailwind 那样的易用性,但它却是处理大文件、实时数据的 “刚需工具” 。
它解决了 Blob、全量 JSON 等方案的核心痛点 —— 内存占用过高、实时性差,让前端能够从容应对 “大文件上传下载”“实时日志”“大 API 响应” 等复杂场景。
在现代 Web 开发中,理解并熟练使用 Stream API,不仅能提升项目的性能和用户体验,还能让你在处理 “海量数据” 时更有底气。
所以,下次遇到大文件或实时数据场景时,别忘了:Stream API 居然这么好用!