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()、finish、close
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 也不保证客户端业务代码已经完整接收和保存文件。
练习题
- 解释
write()返回false为什么不是错误。 - 目标磁盘写入失败后,
finish是否会出现? writableEnded和writableFinished有什么不同?- 写入失败后为什么还需要删除目标文件?