archiver 是一个高级归档接口,支持 ZIP 和 TAR 格式,通过流式处理简化了文件打包逻辑,适合大多数通用场景。tar 是处理 TAR 格式的标准底层库,提供了对 POSIX tar 规范的完整支持,适合需要精细控制 TAR 头部信息的场景。tar-fs 专注于将 TAR 流与文件系统直接交互,极大地简化了磁盘文件的打包与解压操作。tar-stream 提供了构建和解析 TAR 流的底层原语,适合需要自定义流逻辑或处理非文件数据源的高级场景。zip-stream 是 archiver 的底层依赖之一,专注于生成 ZIP 数据流,适合需要独立 ZIP 生成能力且不依赖 archiver 高层抽象的场景。
在 Node.js 生态中,处理文件压缩和归档(Archiving)是后端服务、构建工具和自动化脚本的常见需求。虽然 zip 和 tar 都是常见的归档格式,但它们的底层机制完全不同,导致对应的 npm 包在设计哲学和适用场景上也有显著差异。本文将深入对比 archiver、tar、tar-fs、tar-stream 和 zip-stream,帮助你在架构设计中做出正确选择。
这五个包可以分为两类:高层封装和底层引擎。
archiver 是典型的高层封装。它不直接处理压缩算法,而是作为一个“指挥官”,协调不同的格式引擎(如 zip-stream 或 tar-stream)来完成工作。它的目标是让开发者用统一的 API 处理不同格式。
// archiver: 统一接口处理不同格式
const archiver = require('archiver');
// 创建 ZIP 归档
const zipOutput = fs.createWriteStream('archive.zip');
const zipArchive = archiver('zip', { zlib: { level: 9 } });
zipArchive.pipe(zipOutput);
zipArchive.directory('src/', false);
zipArchive.finalize();
// 创建 TAR 归档只需改变第一个参数
const tarOutput = fs.createWriteStream('archive.tar');
const tarArchive = archiver('tar', { gzip: true });
tarArchive.pipe(tarOutput);
tarArchive.directory('src/', false);
tarArchive.finalize();
zip-stream 和 tar-stream 则是底层引擎。它们专注于生成特定格式的流数据,不提供文件系统的自动遍历或高层的便捷方法。你需要手动将文件内容推入流中。
// zip-stream: 手动添加条目
const ZipStream = require('zip-stream');
const zip = new ZipStream();
zip.pipe(fs.createWriteStream('output.zip'));
// 必须手动读取文件内容并添加
const content = fs.readFileSync('file.txt');
zip.entry(content, { name: 'file.txt' }, (err) => {
zip.finish();
});
tar 和 tar-fs 处于中间地带。tar 提供了符合 POSIX 标准的 TAR 格式实现,而 tar-fs 则在 tar-stream 的基础上增加了对文件系统的便捷支持。
在处理本地文件时,开发效率往往取决于库对文件系统(FS)的抽象程度。
tar-fs 是处理本地文件夹打包和解压的神器。它暴露了 pack 和 extract 两个核心方法,直接接受路径字符串,自动处理文件遍历、权限保留和符号链接。
// tar-fs: 一键打包整个目录
const tarFs = require('tar-fs');
// 打包:目录 -> 流
const stream = tarFs.pack('./my-directory');
stream.pipe(fs.createWriteStream('backup.tar'));
// 解压:流 -> 目录
fs.createReadStream('backup.tar')
.pipe(tarFs.extract('./restored-directory'));
相比之下,使用 tar-stream 或 zip-stream 处理文件夹时,你需要自己编写递归逻辑来遍历文件,读取每个文件的内容,然后手动调用 entry 方法。这不仅代码量大,还容易出错(例如忘记处理异步读取或路径分隔符)。
// tar-stream: 需要手动遍历和读取
const tar = require('tar-stream');
const pack = tar.pack();
// 假设我们有一个文件列表
const files = ['file1.txt', 'file2.txt'];
files.forEach(file => {
const stat = fs.statSync(file);
const header = { name: file, size: stat.size, mode: stat.mode };
pack.entry(header, fs.readFileSync(file));
});
pack.finalize();
pack.pipe(fs.createWriteStream('manual.tar'));
archiver 在这方面做了很好的平衡。它提供了 directory() 方法,可以像 tar-fs 一样自动遍历文件夹,但同时保留了流式的灵活性。
// archiver: 自动遍历目录
const archive = archiver('zip');
archive.directory('src/', 'subfolder-in-zip'); // 第二个参数指定内部路径
archive.finalize();
在处理大文件或海量小文件时,内存管理至关重要。所有这五个库都基于 Node.js Stream API 设计,支持流式传输,避免将整个归档加载到内存中。
tar-stream 和 zip-stream 提供了最纯粹的流式体验。你可以从网络请求、数据库游标或其他非文件源直接读取数据块并写入归档,而不需要中间文件。
// tar-stream: 从网络流直接打包
const pack = require('tar-stream').pack();
const entry = pack.entry({ name: 'remote-data.bin', size: 1024 });
// 假设 response 是一个 HTTP 流
http.get('http://example.com/data', (res) => {
res.pipe(entry); // 直接管道连接
res.on('end', () => pack.finalize());
});
pack.pipe(fs.createWriteStream('remote.tar'));
archiver 同样支持流式添加,但它对输入流的类型做了一些封装。你可以直接传入流对象,它会自动处理结束事件。
// archiver: 添加流
const archive = archiver('zip');
const stream = fs.createReadStream('large-file.log');
archive.append(stream, { name: 'logs/large-file.log' });
archive.finalize();
tar-fs 虽然基于流,但其 pack 方法主要针对文件系统路径优化。如果你需要打包非文件路径的数据源,tar-fs 不如 tar-stream 直接。
ZIP vs TAR 的选择通常取决于目标平台和使用场景。
archiver, zip-stream): Windows 原生支持,支持随机访问(无需解压即可读取单个文件),压缩率通常较高(支持 Deflate 等算法)。适合分发给最终用户或在 Windows 环境中交换数据。tar, tar-fs, tar-stream): Unix/Linux 标准,保留文件权限、所有者、符号链接等元数据更完整。通常与 Gzip 或 Bzip2 配合使用(如 .tar.gz)。适合服务器备份、容器镜像层或 Linux 环境下的数据交换。tar 包特别强调对 POSIX 标准的兼容性。如果你需要精确控制 TAR 头部中的 uid、gid、uname 或 mtime,tar 包提供了最细致的配置选项,而 tar-fs 可能会为了便捷性隐藏部分细节。
// tar: 精细控制头部信息
const tar = require('tar');
tar.create({
file: 'archive.tar',
cwd: 'src',
portable: true, // 确保跨平台兼容性
mtime: new Date('2023-01-01'), // 强制设置修改时间
owner: 'root',
mode: 0o755
}, ['file.txt']);
在流式操作中,错误处理往往比较棘手。
archiver 提供了相对完善的事件系统(error, warning, progress),并且能够较好地处理流中断的情况。它会在内部捕获底层引擎的错误并统一抛出,减少了开发者手动绑定多个流错误监听器的负担。
tar-stream 和 zip-stream 作为底层库,要求开发者自己管理流的错误事件。如果忘记监听 error 事件,可能会导致进程崩溃。
// 底层库需要手动处理错误
const pack = require('tar-stream').pack();
pack.on('error', (err) => {
console.error('打包失败:', err);
});
| 场景需求 | 推荐包 | 理由 |
|---|---|---|
| 快速实现文件备份/导出 | archiver | API 简单,支持多格式,自动处理目录遍历。 |
| Linux 服务器整目录备份 | tar-fs | 原生支持权限和符号链接,API 极简,性能优秀。 |
| 需要精确控制 TAR 头部 | tar | 符合 POSIX 标准,提供详细的元数据配置项。 |
| 动态生成归档(非文件源) | tar-stream / zip-stream | 不依赖文件系统,完全手动控制条目添加,灵活性最高。 |
| 仅需 ZIP 且追求轻量 | zip-stream | 无 archiver 的额外封装开销,直接生成 ZIP 流。 |
在大多数现代 Web 后端应用中,archiver 通常是最佳起点。它的抽象层次恰到好处,既能满足 90% 的归档需求,又不会牺牲太多的性能。当你发现 archiver 的某些限制(如无法精细控制 TAR 头部,或需要处理非常特殊的流逻辑)时,再考虑下沉到 tar-stream 或 tar-fs。
避免在非必要时直接使用 zip-stream,除非你对 ZIP 格式有极深的理解且需要剔除 archiver 的依赖。对于 TAR 格式,如果涉及文件系统操作,tar-fs 的便利性远超手动组合 tar-stream 和 fs 模块。
最后,无论选择哪个库,务必记住它们是流式的。永远不要试图将大文件读入内存后再传给这些库,而应该直接使用 fs.createReadStream 并通过 pipe 或 append 连接,这样才能保证你的服务在高负载下依然稳定运行。
如果你的项目需要同时支持 ZIP 和 TAR 格式,且希望用最少的代码实现文件归档,archiver 是首选。它封装了复杂的流逻辑,提供了简单的 append 接口,非常适合构建备份工具、导出功能或需要多格式支持的通用服务。
当你需要严格遵循 TAR 规范,或者需要手动构造 TAR 头部元数据(如权限、所有者、时间戳)时,选择 tar。它比 tar-fs 更底层,适合处理非文件系统的流数据或需要高度定制化 TAR 包结构的场景。
如果你的主要需求是将本地文件夹打包成 TAR 文件,或者将 TAR 文件解压到本地磁盘,tar-fs 是最快捷的选择。它将文件系统操作与 TAR 流处理完美结合,避免了手动管理文件读取流和路径映射的繁琐工作。
当你需要在内存中动态构建 TAR 包,或者处理来自数据库、网络 socket 等非文件系统的数据流时,tar-stream 提供了最大的灵活性。它不绑定文件系统,允许你完全控制条目的添加顺序和元数据,适合高级流式处理管道。
如果你只需要生成 ZIP 格式,且希望比 archiver 更轻量或更直接地控制 ZIP 流的生成过程,可以选择 zip-stream。它通常作为 archiver 的引擎使用,但在需要独立 ZIP 流生成而不引入 archiver 额外抽象层时非常有用。
A streaming interface for archive generation
Visit the API documentation for a list of all methods available.
npm install archiver --save
import fs from "fs";
import { ZipArchive } from "archiver";
// create a file to stream archive data to.
const output = fs.createWriteStream(__dirname + "/example.zip");
const archive = new ZipArchive({
zlib: { level: 9 }, // Sets the compression level.
});
// listen for all archive data to be written
// 'close' event is fired only when a file descriptor is involved
output.on("close", function () {
console.log(archive.pointer() + " total bytes");
console.log(
"archiver has been finalized and the output file descriptor has closed.",
);
});
// This event is fired when the data source is drained no matter what was the data source.
// It is not part of this library but rather from the NodeJS Stream API.
// @see: https://nodejs.org/api/stream.html#stream_event_end
output.on("end", function () {
console.log("Data has been drained");
});
// good practice to catch warnings (ie stat failures and other non-blocking errors)
archive.on("warning", function (err) {
if (err.code === "ENOENT") {
// log warning
} else {
// throw error
throw err;
}
});
// good practice to catch this error explicitly
archive.on("error", function (err) {
throw err;
});
// pipe archive data to the file
archive.pipe(output);
// append a file from stream
const file1 = __dirname + "/file1.txt";
archive.append(fs.createReadStream(file1), { name: "file1.txt" });
// append a file from string
archive.append("string cheese!", { name: "file2.txt" });
// append a file from buffer
const buffer3 = Buffer.from("buff it!");
archive.append(buffer3, { name: "file3.txt" });
// append a file
archive.file("file1.txt", { name: "file4.txt" });
// append files from a sub-directory and naming it `new-subdir` within the archive
archive.directory("subdir/", "new-subdir");
// append files from a sub-directory, putting its contents at the root of archive
archive.directory("subdir/", false);
// append files from a glob pattern
archive.glob("file*.txt", { cwd: __dirname });
// finalize the archive (ie we are done appending files but streams have to finish yet)
// 'close', 'end' or 'finish' may be fired right after calling this method so register to them beforehand
archive.finalize();
Archiver ships with out of the box support for TAR and ZIP archives.