12 pipe() 与错误边界
1. 正确的方法签名
常见形式是:
readable.pipe(writable);
并不是一般意义上的 Readable.pipe(Readable)。只有 Transform/Duplex 同时具有可写输入端和可读输出端,才能放在中间:
input
.pipe(transform)
.pipe(output);
2. pipe() 返回目标
const returned = input.pipe(output);
console.log(returned === output); // true
这允许链式调用,但也让新人容易只监听最后一段错误。
3. 怎样知道传输完成
源 Readable 的:
end = 源数据被读取完
目标 Writable 的:
finish = 所有写入数据已处理完
复制文件时,更接近“目标写完”的是 finish,不能只等待源 end。但双方任何一端报错时,对应成功事件都可能不出现。
4. pipe() 不等于完整错误管理
危险写法:
input.pipe(output);
output.on("finish", () => {
console.log("完成");
});
如果 input 报错,不能假设 output 一定按你期望自动销毁;如果 output 报错,也不能依赖所有源自动停止。你需要监听每一段错误、取消其他段、避免重复 reject,并等待资源关闭。
5. 手工封装为何复杂
function copyWithPipe(
input: Readable,
output: Writable,
): Promise<void> {
return new Promise((resolve, reject) => {
let settled = false;
const fail = (error: Error): void => {
if (settled) return;
settled = true;
input.destroy();
output.destroy();
reject(error);
};
input.once("error", fail);
output.once("error", fail);
output.once("finish", () => {
if (settled) return;
settled = true;
resolve();
});
input.pipe(output);
});
}
即使这样还要考虑监听器清理、提前 close、同步抛错、多个 Transform 和销毁完成。Node.js 已经提供 pipeline() 处理这些组合问题,不应重复造一个不完整版本。
6. { end: false }
input.pipe(output, { end: false });
源结束后目标不会自动 end(),适合多个源顺序写入同一个目标,但调用者必须明确何时最终 output.end()。遗漏会导致 finish 永远不发生、句柄持续打开。
7. 部分文件
任一端失败时,目标文件可能已经写入一部分。Stream 销毁只负责资源,不会自动执行:
await rm(outputPath, { force: true });
业务层必须决定删除半成品、保留诊断,还是写入临时路径后永不暴露半成品。
8. 与 yauzl Entry Stream 对照
Entry ReadStream error
→ 目标 WriteStream 不应继续等待
WriteStream error
→ Entry ReadStream 不应继续解压
ZIP Reader error
→ 当前 Entry 和整体任务都应终止
这正是多个 .pipe() 让错误边界难以管理的原因。
练习题
- 为什么源
end不证明目标文件写完? - 为什么目标
close不证明复制成功? { end: false }最容易造成什么资源问题?- 分别模拟输入和输出报错,记录每一端事件。