H3 流式响应
流式响应让你「一边生成、一边发」,而不是等全部算完再一次性返回。对大文件、长轮询、实时进度条都很有用。
创建流
let interval: ReturnType<typeof setInterval>;
// 用标准 ReadableStream 构造一个每 100ms 推一个随机数的流
const stream = new ReadableStream({
start(controller) {
controller.enqueue("<ul>"); // 先推开头
interval = setInterval(() => {
controller.enqueue(`<li>${Math.random()}</li>`); // 周期性推数据
}, 100);
setTimeout(() => {
clearInterval(interval);
controller.close(); // 1 秒后关闭流
}, 1000);
},
cancel() {
clearInterval(interval); // 客户端断开时清理定时器,避免泄漏
},
});
start 在流被消费时调用,负责开始生产;cancel 在客户端提前断开或取消时调用,用来释放资源(这里的定时器必须清掉,否则会一直跑)。
直接 return 这个流
import { H3 } from "h3";
export const app = new H3();
app.use((event) => {
event.res.headers.set("Content-Type", "text/html");
event.res.headers.set("Cache-Control", "no-cache");
// 注意:Transfer-Encoding 一般不需要手动设,fetch 响应会自动分块
// 这里保留源示例的写法,便于理解「分块」这一行为
let interval: ReturnType<typeof setInterval>;
const stream = new ReadableStream({
start(controller) {
controller.enqueue("<ul>");
interval = setInterval(() => {
controller.enqueue(`<li>${Math.random()}</li>`);
}, 100);
setTimeout(() => {
clearInterval(interval);
controller.close();
}, 1000);
},
cancel() {
clearInterval(interval);
},
});
return stream; // 直接返回流即可,H3 会把它当作响应体
});
把 ReadableStream 当作返回值直接 return,H3 会自动把它作为响应体发出去。打开浏览器访问,就能看到随机数每隔 100 毫秒冒出来一个。
更简单的写法:从异步迭代器生成
如果你要流的数据本来就来自一个异步迭代器(如数据库游标、分页拉取),不必手写 controller.enqueue 循环,用 H3 导出的 iterable() 适配更省事。它接受函数、同步 / 异步迭代器,把它们包成可响应的流,并替你处理逐块 enqueue 与结束:
import { iterable } from "h3";
// 一个异步生成器:每隔 100ms 产出一个数字
async function* numbers() {
for (let i = 0; i < 10; i++) {
await new Promise((r) => setTimeout(r, 100)); // 模拟逐条产出
yield `<li>${i}</li>`;
}
}
app.use((event) => {
event.res.headers.set("Content-Type", "text/html");
return iterable(numbers()); // 从异步迭代器生成流,比手工 enqueue 简洁
});
错误与取消
两个关键点:
- 取消:客户端断开时,
ReadableStream的cancel()会被调用。任何在start里开的定时器、订阅、上游连接,都要在这里释放,否则会泄漏。 - 错误:在
start里throw会中止流并关闭连接。如果你在循环生产中遇到不可恢复的错误,调用controller.error(err)比直接 throw 更可控,能带上错误原因。
💡 流式响应通常不适合带强缓存(
Cache-Control: no-cache更稳妥),否则中间代理可能缓冲住、反而失去「边生成边发」的效果。
流式响应 vs SSE:怎么选
两者都是「边生成边发」,但场景不同:
- 流式响应(本节):一次性的响应体分块。客户端发一个请求,服务端把整个响应切成多块陆续发完,适合「大文件下载」「长计算的结果逐步返回」。
- SSE(第 5 章):服务端主动向单个长连接持续推送事件,客户端通过
EventSource监听。适合「实时通知」「日志流」「进度广播」这类服务端主动、多轮的场景。
简单记:把「一份结果」慢慢给,用流式响应;把「一连串事件」持续推,用 SSE。
分块传输的 HTTP 语义
返回 ReadableStream 时,底层传输会走 HTTP 分块传输编码(chunked)。由于总长度在发送前未知,响应头里通常不显式带 Content-Length,而是靠连接关闭或最后的空块来表示结束。这也是上面建议 Cache-Control: no-cache 的原因——代理若缓冲整个响应,分块的实时性就丢了。