10 Node.js Writable Stream

1. Writable 是“接收数据的目标”

import { createWriteStream } from "node:fs";

const output = createWriteStream(filePath, {
  flags: "wx",
});

文件 WriteStream、HTTP Response、压缩器输入端都可能是 Writable。

2. write() 的返回值

const canContinue = output.write(chunk);

返回 false 不表示本次写入失败,而表示内部缓冲已经达到阈值,生产者应该暂停并等待:

await once(output, "drain");

忽略它会继续把数据塞入内存,破坏背压。

3. end()finishclose

output.end(finalChunk);

end() 表示调用者不再写入。之后:

finish  所有交给 Writable 的数据已经刷新到底层系统
close   底层资源已经关闭

finish 不等于磁盘硬件在任何断电场景下都已持久化;需要更强保证时才考虑 FileHandle.sync() 和原子替换。

4. 正常时间线

open
  → write() ...
  → end()
  → finish
  → close

finish 属于可写端;不要在 Readable 上等待它。

5. 错误时间线

open
  → write() ...
  → error
  → close

错误发生后,finish 不保证出现。只等待 finish 而不处理错误会让业务 Promise 悬空。

close 也可能出现在成功或失败路径,不能单独作为“文件完整”的证据。

6. 手动处理背压

import { once } from "node:events";

async function writeChunks(
  output: NodeJS.WritableStream,
  chunks: Iterable<Buffer>,
): Promise<void> {
  for (const chunk of chunks) {
    if (!output.write(chunk)) {
      await once(output, "drain");
    }
  }

  output.end();
}

这仍未完整覆盖 error 和提前关闭,实战更推荐把数据做成 Readable 后使用 pipeline()

7. destroy()

output.destroy(new Error("转换失败"));

销毁可能丢弃尚未写出的缓冲数据。失败时这是期望行为,但磁盘上可能已经存在部分文件,因此还要在 Stream 结束清理后删除半成品。

8. 状态属性

console.log({
  destroyed: output.destroyed,
  writableEnded: output.writableEnded,
  writableFinished: output.writableFinished,
  writableNeedDrain: output.writableNeedDrain,
});

writableEnded 表示已经调用 end(),不等同于 finish 已发生。

9. HTTP Response

Express 的 res 基于 http.ServerResponse,它是 Writable。对于响应:

finish  响应数据已交给底层系统发送
close   连接/响应关闭,可能是客户端中断

finish 也不保证客户端业务代码已经完整接收和保存文件。

练习题

  1. 解释 write() 返回 false 为什么不是错误。
  2. 目标磁盘写入失败后,finish 是否会出现?
  3. writableEndedwritableFinished 有什么不同?
  4. 写入失败后为什么还需要删除目标文件?