H3 流式响应

2026-10-30 17:48 更新

流式响应让你「一边生成、一边发」,而不是等全部算完再一次性返回。对大文件、长轮询、实时进度条都很有用。

创建流

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 的原因——代理若缓冲整个响应,分块的实时性就丢了。

以上内容是否对您有帮助:
在线笔记
App下载
App下载

扫描二维码

下载编程狮App

公众号
微信公众号

编程狮公众号