adm-zip、archiver、compressing、node-zip、yauzl、yazl 和 zip-lib 都是 Node.js 生态中用于处理 ZIP 压缩文件的工具库,但它们在 API 设计、功能完整度、维护状态和适用场景上有显著差异。archiver 专注于流式压缩,适合处理大文件;yauzl 和 yazl 分别提供低级别的解压和压缩能力;adm-zip 提供同步和异步的完整 ZIP 操作;compressing 是阿里团队维护的多格式压缩工具;node-zip 已停止维护;zip-lib 提供现代化的异步 API。选择哪个库取决于你的具体需求:是需要流式处理、同步操作、还是现代异步 API。
在 Node.js 项目中处理 ZIP 文件是常见需求,但选择合适的库并不简单。adm-zip、archiver、compressing、node-zip、yauzl、yazl 和 zip-lib 各有特点,理解它们的差异能帮你避免生产环境中的性能问题和维护陷阱。让我们从实际工程角度深入对比。
adm-zip 提供完整的 ZIP 文件操作能力,支持读取、创建、修改现有 ZIP。
const AdmZip = require('adm-zip');
const zip = new AdmZip();
zip.addLocalFile('./file.txt');
zip.writeZip('./output.zip');
archiver 专注于流式压缩,通过管道将数据写入目标。
const archiver = require('archiver');
const output = fs.createWriteStream('./output.zip');
const archive = archiver('zip', { zlib: { level: 9 } });
archive.pipe(output);
archive.file('./file.txt', { name: 'file.txt' });
archive.finalize();
compressing 提供多格式统一 API,支持 ZIP、TAR、GZIP 等。
const compressing = require('compressing');
await compressing.zip.compressFile('./file.txt', './output.zip');
node-zip 提供基础的 ZIP 创建和解压,但已停止维护。
const zip = require('node-zip')();
zip.file('file.txt', 'content');
const data = zip.generate({ type: 'nodebuffer' });
yauzl 只负责解压,提供细粒度的读取控制。
const yauzl = require('yauzl');
yauzl.open('./input.zip', { lazyEntries: true }, (err, zipfile) => {
zipfile.readEntry();
zipfile.on('entry', (entry) => {
zipfile.openReadStream(entry, (err, readStream) => {
// 处理文件流
});
});
});
yazl 只负责创建 ZIP,与 yauzl 配合使用。
const yazl = require('yazl');
const zipFile = new yazl.ZipFile();
zipFile.addFile('./file.txt', 'file.txt');
zipFile.outputStream.pipe(fs.createWriteStream('./output.zip'));
zipFile.end();
zip-lib 提供现代化的异步 API,支持压缩和解压。
const { zip, unzip } = require('zip-lib');
await zip(['./file.txt'], './output.zip');
await unzip('./input.zip', './output-dir');
执行模式直接影响性能和内存使用,这是生产环境的关键考量。
adm-zip 支持同步和异步,但默认将 ZIP 加载到内存。
// 同步方式
const zip = new AdmZip('./input.zip');
const entries = zip.getEntries();
// 异步方式
zip.toAsyncBuffer((data) => {
fs.writeFileSync('./output.zip', data);
});
archiver 纯流式异步,适合大文件处理。
const archive = archiver('zip');
archive.pipe(fs.createWriteStream('./output.zip'));
archive.directory('./source-dir', false);
archive.on('finish', () => {
console.log('压缩完成');
});
compressing 同时支持 Promise 和流式。
// Promise 方式
await compressing.zip.compressDir('./source', './output.zip');
// 流式方式
const stream = compressing.zip.createStream();
stream.pipe(fs.createWriteStream('./output.zip'));
node-zip 同步操作,无流式支持。
const zip = require('node-zip')();
zip.file('test.txt', 'hello');
const buffer = zip.generate({ type: 'nodebuffer' });
yauzl 基于事件的异步 API。
yauzl.open('./input.zip', { lazyEntries: true }, (err, zipfile) => {
zipfile.on('entry', (entry) => {
// 逐个处理文件
});
zipfile.on('end', () => {
console.log('解压完成');
});
});
yazl 基于流的异步 API。
const zipFile = new yazl.ZipFile();
zipFile.addFile('./file.txt', 'file.txt');
zipFile.outputStream.on('finish', () => {
console.log('创建完成');
});
zip-lib 纯 Promise/async-await 风格。
await zip(['./file1.txt', './file2.txt'], './output.zip');
await unzip('./input.zip', './output-dir');
处理大文件时,内存使用是关键指标。
| 库 | 内存模式 | 适合文件大小 | 流式支持 |
|---|---|---|---|
adm-zip | 全量加载 | 中小文件 (<100MB) | ❌ |
archiver | 流式处理 | 大文件 (无限制) | ✅ |
compressing | 可选 | 中等文件 | ✅ |
node-zip | 全量加载 | 小文件 | ❌ |
yauzl | 流式读取 | 大文件 | ✅ |
yazl | 流式写入 | 大文件 | ✅ |
zip-lib | 流式处理 | 中等文件 | ✅ |
adm-zip 在读取 ZIP 时会加载整个文件到内存,处理大文件可能导致内存溢出。
// 风险:大文件可能耗尽内存
const zip = new AdmZip('./large-file.zip');
const entries = zip.getEntries(); // 全部加载到内存
archiver 使用流式处理,内存占用稳定。
// 安全:流式处理大文件
const archive = archiver('zip');
archive.pipe(fs.createWriteStream('./output.zip'));
archive.directory('./large-dir', false); // 流式读取目录
yauzl 使用 lazyEntries 选项可控制内存使用。
// 逐个处理,控制内存
yauzl.open('./large.zip', { lazyEntries: true }, (err, zipfile) => {
zipfile.readEntry(); // 手动控制读取节奏
});
adm-zip 采用面向对象设计,状态保存在实例中。
const zip = new AdmZip();
zip.addFile('name.txt', Buffer.from('content'));
zip.addLocalFolder('./dir');
zip.writeZip('./output.zip');
archiver 采用链式 API,配置与执行分离。
archiver('zip', { level: 9 })
.pipe(fs.createWriteStream('./output.zip'))
.file('./file.txt', { name: 'custom.txt' })
.directory('./src', 'source')
.finalize();
compressing 提供简洁的函数式 API。
await compressing.zip.compressFile('./file.txt', './output.zip');
await compressing.zip.unzip('./input.zip', './output-dir');
zip-lib 纯函数式,返回 Promise。
await zip(['./file1.txt'], './output.zip');
await unzip('./input.zip', './output-dir');
yauzl/yazl 基于事件/流,需要手动管理状态。
// yauzl 事件驱动
zipfile.on('entry', (entry) => { /* 处理 */ });
zipfile.on('end', () => { /* 完成 */ });
这是选择库时最重要的考量因素之一。
node-zip 已停止维护,最后更新在 2015 年,存在已知安全漏洞。
// ❌ 不建议在新项目中使用
const zip = require('node-zip')();
adm-zip 仍在维护,但更新频率较低,有报告的安全问题需关注。
// ⚠️ 可用但需关注安全更新
const AdmZip = require('adm-zip');
archiver 活跃维护,社区广泛使用,定期安全更新。
// ✅ 生产环境推荐
const archiver = require('archiver');
compressing 阿里团队维护,企业级支持,更新稳定。
// ✅ 企业项目推荐
const compressing = require('compressing');
yauzl/yazl 稳定维护,专注于单一职责,安全性较好。
// ✅ 需要精细控制时推荐
const yauzl = require('yauzl');
const yazl = require('yazl');
zip-lib 较新,社区较小,但 API 现代化。
// ⚠️ 新项目可考虑,但需评估社区支持
const { zip, unzip } = require('zip-lib');
需要压缩大量小文件,对速度要求高。
// ✅ 推荐:archiver
const archive = archiver('zip', { zlib: { level: 6 } });
archive.pipe(fs.createWriteStream('./dist.zip'));
archive.directory('./dist', false);
archive.finalize();
需要安全地解压用户上传的 ZIP 文件。
// ✅ 推荐:yauzl (可控制解压路径)
yauzl.open('./upload.zip', { lazyEntries: true }, (err, zipfile) => {
zipfile.on('entry', (entry) => {
// 验证路径安全性
if (entry.fileName.startsWith('..')) {
return; // 跳过危险路径
}
zipfile.openReadStream(entry, (err, readStream) => {
readStream.pipe(fs.createWriteStream('./safe-path/' + entry.fileName));
});
});
});
需要流式传输到 HTTP 响应。
// ✅ 推荐:archiver
app.get('/download', (req, res) => {
const archive = archiver('zip');
archive.pipe(res);
archive.directory('./files', false);
archive.finalize();
});
只需要基础压缩功能,代码简洁优先。
// ✅ 推荐:compressing 或 zip-lib
await compressing.zip.compressFile('./file.txt', './output.zip');
// 或
await zip(['./file.txt'], './output.zip');
需要添加或删除 ZIP 中的文件。
// ✅ 推荐:adm-zip
const zip = new AdmZip('./existing.zip');
zip.addFile('new.txt', Buffer.from('content'));
zip.deleteFile('old.txt');
zip.writeZip('./updated.zip');
| 功能 | adm-zip | archiver | compressing | node-zip | yauzl | yazl | zip-lib |
|---|---|---|---|---|---|---|---|
| 创建 ZIP | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ |
| 解压 ZIP | ✅ | ❌ | ✅ | ✅ | ✅ | ❌ | ✅ |
| 修改 ZIP | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
| 流式处理 | ❌ | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ |
| 多格式支持 | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ |
| 异步 API | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ |
| 维护状态 | ⚠️ | ✅ | ✅ | ❌ | ✅ | ✅ | ⚠️ |
| 内存友好 | ❌ | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ |
生产环境首选:archiver(压缩)+ yauzl(解压)组合,经过大量项目验证,性能和安全都有保障。
企业级项目:compressing,阿里团队维护,多格式支持,适合需要统一压缩方案的场景。
简单需求:zip-lib,API 简洁现代,适合新项目快速开发。
避免使用:node-zip,已停止维护,存在安全风险。
特殊需求:adm-zip 适合需要修改现有 ZIP 的场景,但注意内存限制。
选择 ZIP 处理库时,优先考虑维护状态和内存模型,其次才是 API 便利性。生产环境中,稳定性和安全性永远比代码简洁更重要。
选择 compressing 如果你需要统一处理多种压缩格式(ZIP、TAR、GZIP 等),且希望使用阿里团队维护的稳定库。适合需要多格式支持的企业级应用,API 设计简洁,支持流式和 Promise 两种方式。
选择 adm-zip 如果你需要同步或异步地读取和修改现有 ZIP 文件,支持添加、删除、更新 ZIP 中的文件。适合需要完整 ZIP 操作能力的场景,如构建工具、文件管理器。但注意它在处理超大文件时可能占用较多内存,因为需要将整个 ZIP 加载到内存中。
选择 archiver 如果你需要流式压缩大量文件或目录,支持管道传输到文件、HTTP 响应或其他可写流。适合备份系统、文件下载服务等需要高效处理大文件的场景。它是生产环境中最常用的压缩库之一,社区活跃且文档完善。
不建议在新项目中使用 node-zip,该库已停止维护多年,存在已知安全问题且不再接收更新。如果现有项目依赖它,应尽快迁移到 adm-zip 或 archiver 等活跃维护的替代方案。
选择 yauzl 如果你只需要读取/解压 ZIP 文件,且希望使用轻量级、低级别的 API。适合需要精细控制解压过程的场景,如自定义文件提取逻辑。它只支持解压,不支持创建 ZIP 文件。
选择 yazl 如果你只需要创建 ZIP 文件,且希望使用轻量级、低级别的 API。适合需要精细控制压缩过程的场景。它只支持创建 ZIP,不支持解压。常与 yauzl 配合使用。
选择 zip-lib 如果你需要现代化的异步 API 和流式处理能力,同时支持压缩和解压。适合新项目,特别是需要 Promise/async-await 风格的代码。但社区相对较小,文档不如 archiver 完善。
The missing compressing and uncompressing lib for node.
Currently supported:
npm install compressing
Use gzip as an example, tar, tgz and zip is same as gzip.
promise style
const compressing = require('compressing');
// compress a file
compressing.gzip.compressFile('file/path/to/compress', 'path/to/destination.gz')
.then(compressDone)
.catch(handleError);
// compress a file buffer
compressing.gzip.compressFile(buffer, 'path/to/destination.gz')
.then(compressDone)
.catch(handleError);
// compress a stream
compressing.gzip.compressFile(stream, 'path/to/destination.gz')
.then(compressDone)
.catch(handleError);
stream style
const compressing = require('compressing');
new compressing.gzip.FileStream({ source: 'file/path/to/compress' })
.on('error', handleError)
.pipe(fs.createWriteStream('path/to/destination.gz'))
.on('error', handleError);
// It's a transform stream, so you can pipe to it
fs.createReadStream('file/path/to/compress')
.on('error', handleError)
.pipe(new compressing.gzip.FileStream())
.on('error', handleError)
.pipe(fs.createWriteStream('path/to/destination.gz'))
.on('error', handleError);
// You should take care of stream errors in caution, use pump to handle error in one place
const { pipeline: pump } = require('stream');
const sourceStream = fs.createReadStream('file/path/to/compress');
const gzipStream = new compressing.gzip.FileStream();
const destStream = fs.createWriteStream('path/to/destination.gz');
pump(sourceStream, gzipStream, destStream, handleError);
Use tar as an example, tgz and zip is same as gzip.
Gzip only support compressing a single file. if you want to compress a dir with gzip, then you may need tgz instead.
promise style
const compressing = require('compressing');
compressing.tar.compressDir('dir/path/to/compress', 'path/to/destination.tar')
.then(compressDone)
.catch(handleError);
stream style
const compressing = require('compressing');
const tarStream = new compressing.tar.Stream();
tarStream.addEntry('dir/path/to/compress');
tarStream
.on('error', handleError)
.pipe(fs.createWriteStream('path/to/destination.tar'))
.on('error', handleError);
// You should take care of stream errors in caution, use pump to handle error in one place
const tarStream = new compressing.tar.Stream();
tarStream.addEntry('dir/path/to/compress');
const destStream = fs.createWriteStream('path/to/destination.tar');
pump(tarStream, destStream, handleError);
Stream is very powerful, you can compress multiple entries in it;
const tarStream = new compressing.tar.Stream();
// dir
tarStream.addEntry('dir/path/to/compress');
// file
tarStream.addEntry('file/path/to/compress');
// buffer
tarStream.addEntry(buffer);
// stream
tarStream.addEntry(stream);
const destStream = fs.createWriteStream('path/to/destination.tar');
pipe(tarStream, destStream, handleError);
promise style
const compressing = require('compressing');
// uncompress a file
compressing.tgz.uncompress('file/path/to/uncompress.tgz', 'path/to/destination/dir')
.then(uncompressDone)
.catch(handleError);
// uncompress a file buffer
compressing.tgz.uncompress(buffer, 'path/to/destination/dir')
.then(uncompressDone)
.catch(handleError);
// uncompress a stream
compressing.tgz.uncompress(stream, 'path/to/destination/dir')
.then(uncompressDone)
.catch(handleError);
Note: tar, tgz and zip have the same uncompressing API as above: destination should be a path of a directory, while that of gzip is slightly different: destination must be a file or filestream.
And working with urllib is super easy. Let's download a tgz file and uncompress to a directory:
const urllib = require('urllib');
const targetDir = require('os').tmpdir();
const compressing = require('compressing');
urllib.request('http://registry.npmjs.org/compressing/-/compressing-2.0.0.tgz', {
streaming: true,
followRedirect: true,
})
.then(result => compressing.tgz.uncompress(result.res, targetDir))
.then(() => console.log('uncompress done'))
.catch(console.error);
stream style
const compressing = require('compressing');
const mkdirp = require('mkdirp');
function onEntry(header, stream, next) => {
stream.on('end', next);
// header.type => file | directory
// header.name => path name
if (header.type === 'file') {
stream.pipe(fs.createWriteStream(path.join(destDir, header.name)));
} else { // directory
fs.mkdir(path.join(destDir, header.name), { recursive: true }, err => {
if (err) return handleError(err);
stream.resume();
});
}
}
new compressing.tgz.UncompressStream({ source: 'file/path/to/uncompress.tgz' })
.on('error', handleError)
.on('finish', handleFinish) // uncompressing is done
.on('entry', onEntry);
// It's a writable stream, so you can pipe to it
fs.createReadStream('file/path/to/uncompress')
.on('error', handleError)
.pipe(new compressing.tgz.UncompressStream())
.on('error', handleError)
.on('finish', handleFinish) // uncompressing is done
.on('entry', onEntry);
Note: tar, tgz and zip have the same uncompressing streaming API as above: it's a writable stream, and entries will be emitted while uncompressing one after one another, while that of gzip is slightly different: gzip.UncompressStream is a transform stream, so no entry event will be emitted and you can just pipe to another stream
This constrants is brought by Gzip algorithm itself, it only support compressing one file and uncompress one file.
new compressing.gzip.UncompressStream({ source: 'file/path/to/uncompress.gz' })
.on('error', handleError)
.pipe(fs.createWriteStream('path/to/dest/file'))
.on('error', handleError);
Use this API to compress a single file. This is a convenient method, which wraps FileStream API below, but you can handle error in one place.
Params
/path/to/xx.tgz), or a writable stream.Returns a promise object.
Use this API to compress a dir. This is a convenient method, which wraps Stream API below, but you can handle error in one place.
Note: gzip do not have a compressDir method, you may need tgz instead.
Params
/path/to/xx.tgz), or a writable stream.Use this API to uncompress a file. This is a convenient method, which wraps UncompressStream API below, but you can handle error in one place. RECOMMANDED.
Params
/path/to/xx). When uncompressing gzip, it should be a file path or a writable stream.opts.zipFileNameEncoding {String} - Only work on zip format, default is 'utf8'. Major non-UTF8 encodings by languages:
opts.strip {Number} - Strip leading path segments when extracting (tar/tgz/zip). Default is 0.
The transform stream to compress a single file.
Note: If you are not very familiar with streams, just use compressFile() API, error can be handled in one place.
Common params:
Gzip params:
Tar params:
Tgz params:
tgz.FileStream is a combination of tar.FileStream and gzip.FileStream, so the params are the combination of params of tar and gzip.
Zip params:
The readable stream to compress anything as you need.
Note: If you are not very familiar with streams, just use compressFile() and compressDir() API, error can be handled in one place.
Gzip only support compressing a single file. So gzip.Stream is not available.
Constructor
No options in all constructors.
Instance methods
Params
The writable stream to uncompress anything as you need.
Note: If you are not very familiar with streams, just use uncompress() API, error can be handled in one place.
Gzip only support compressing and uncompressing one single file. So gzip.UncompressStream is a transform stream which is different from others.
Constructor
Common params:
CAUTION for zip.UncompressStream
Due to the design of the .zip file format, it's impossible to interpret a .zip file without loading all data into memory.
Although the API is streaming style(try to keep it handy), it still loads all data into memory.
https://github.com/thejoshwolfe/yauzl#no-streaming-unzip-api
Made with contributors-img.