如何使用JavaScript压缩文件?

81

有没有一种使用JavaScript压缩文件的方法?例如,在Yahoo邮箱中选择下载电子邮件的所有附件时,会将它们压缩并下载为一个zip文件。JavaScript能够实现吗?如果可以,请提供一个编程示例。

我发现了一个叫做jszip的库来完成这个任务,但它存在已知而未解决的问题。

如何解决这个问题?


可能是重复的问题,参考链接:https://dev59.com/g3I95IYBdhLWcg3w7CfL - kvc
1
@kvc 我也看到了,但我的问题是关于压缩文件,不是解压缩。所以想另开一篇帖子来问 :) - Isuru
你可能可以创建一个能够实现这个功能的 Windows Shell JavaScript,但是在浏览器中无法做到这一点。 - kvc
7个回答

61

JSZip多年来一直在更新。现在你可以在它的GitHub代码库上找到它。

它可以与FileSaver.js一起使用。

你可以使用npm来安装它们:

npm install jszip --save
npm install file-saver --save

然后导入并使用它们:

import JSZip from 'jszip';
import FileSaver from 'file-saver';

const zip = new JSZip();
zip.file('idlist.txt', 'PMID:29651880\r\nPMID:29303721');
zip.generateAsync({ type: 'blob' }).then(function (content) {
    FileSaver.saveAs(content, 'download.zip');
});

您将下载一个名为download.zip的压缩文件,解压后您可以在其中找到一个名为idlist.txt的文件,它有两行内容:

PMID:29651880
PMID:29303721

此外,为您提供参考,我已经使用以下浏览器进行了测试,全部通过:

  • Firefox 59.0.2(Windows 10)
  • Chrome 65.0.3325.181(Windows 10)
  • Microsoft Edge 41.16299.371.0(Windows 10)
  • Internet Explorer 11.0.60(Windows 10)
  • Opera 52(Mac OSX 10.13)
  • Safari 11(Mac OSX 10.13)

这对我不起作用。我得到了这个:UnhandledPromiseRejectionWarning: Error: blob is not supported by this platform另外,我如何在同一目录中压缩一个已经存在的文件? - jupiterjelly

20
如果您对IE浏览器不感兴趣,client-zip比JSZip更快且更小,旨在解决这个问题(是的,这是一个无耻但完全相关的宣传我的库)。
您可以像这样使用它(其中files可以是一组fetch响应,例如,尽管有许多支持的输入):
import { downloadZip } from "client-zip/index.js"
import FileSaver from "file-saver"

const content = await downloadZip(files).blob()
FileSaver.saveAs(content, "download.zip");

基本上和Yuci的答案用法相同,但已更新至2020年。


2
但是客户端压缩程序并没有压缩数据。 - Vagner Gon
5
这并不一定是坏事。实际上,许多浏览器会自动解压下载的存档文件,所以压缩只会增加 CPU 负载(用于压缩和再次解压缩),而没有带来任何带宽或存储空间的节省。这里压缩的主要原因似乎是为了实现一键下载多个附件。 - Touffy
4
没说那是一件坏事。但你提到了“确切的问题”,这可能会让人觉得它应该做与JSZip相同的事情。Client-zip似乎很棒,可以完成它所提出的任务。 - Vagner Gon
1
好的。明确一点:我并不声称client-zip解决与JSZip相同的问题组,只是这个问题中的一个。 - Touffy
更新后的客户端压缩包似乎更加简单,而且无需使用FileSaver。 - Nick Chan Abdullah
显示剩余7条评论

11
使用JSZIP,我们可以在JavaScript中生成并下载zip文件。为此,您需要按照以下步骤进行操作:
  1. http://github.com/Stuk/jszip/zipball/master下载jszip zip文件。
  2. 解压缩该zip文件,并在dist文件夹内找到jszip.js文件。
  3. 像下面这样在您的html文件中导入jszip.js文件

    <script type="text/javascript" src="jszip.js"></script>
    
  4. 在您的代码中添加下面的函数并调用它

  5. onClickDownload: function () {
        var zip = new JSZip();
        for (var i = 0; i < 5; i++) {
            var txt = 'hello';
            zip.file("file" + i + ".txt", txt);
        }
        zip.generateAsync({
            type: "base64"
        }).then(function(content) {
            window.location.href = "data:application/zip;base64," + content;
        });       
    }
    
  6. 您可以从我的git存储库下载示例代码,链接在此处(GIT 链接


5
将输出生成为Base64格式不是一个好主意,使用Data URI更是一个更糟糕的想法。这会浪费内存和CPU资源。应该使用Blob URL替代。 - Brad
太好了,我将类型更改为“blob”,它就像魔法一样奏效!我在页面上使用锚链接来触发下载,而不是更改页面URL。var uriContent = URL.createObjectURL(contentBlob); var lnkDownload = document.getElementById('lnkDownload'); lnkDownload.download = 'MyDownload.zip'; lnkDownload.href = uriContent; lnkDownload.click(); - tgraupmann

6
我用JavaScript开发了一个新的生成zip文件的解决方案。这个解决方案处于公共领域。
Zip类。
Zip {

    constructor(name) {
        this.name = name;
        this.zip = new Array();
        this.file = new Array();
    }
    
    dec2bin=(dec,size)=>dec.toString(2).padStart(size,'0');
    str2dec=str=>Array.from(new TextEncoder().encode(str));
    str2hex=str=>[...new TextEncoder().encode(str)].map(x=>x.toString(16).padStart(2,'0'));
    hex2buf=hex=>new Uint8Array(hex.split(' ').map(x=>parseInt(x,16)));
    bin2hex=bin=>(parseInt(bin.slice(8),2).toString(16).padStart(2,'0')+' '+parseInt(bin.slice(0,8),2).toString(16).padStart(2,'0'));
    
    reverse=hex=>{
        let hexArray=new Array();
        for(let i=0;i<hex.length;i=i+2)hexArray[i]=hex[i]+''+hex[i+1];
        return hexArray.filter((a)=>a).reverse().join(' '); 
    }
    
    crc32=r=>{
        for(var a,o=[],c=0;c<256;c++){
            a=c;
            for(var f=0;f<8;f++)a=1&a?3988292384^a>>>1:a>>>1;
            o[c]=a;
        }
        for(var n=-1,t=0;t<r.length;t++)n=n>>>8^o[255&(n^r[t])];
        return this.reverse(((-1^n)>>>0).toString(16).padStart(8,'0'));
    }
    
    fecth2zip(filesArray,folder=''){
        filesArray.forEach(fileUrl=>{
            let resp;               
            fetch(fileUrl).then(response=>{
                resp=response;
                return response.arrayBuffer();
            }).then(blob=>{
                new Response(blob).arrayBuffer().then(buffer=>{
                    console.log(`File: ${fileUrl} load`);
                    let uint=[...new Uint8Array(buffer)];
                    uint.modTime=resp.headers.get('Last-Modified');
                    uint.fileUrl=`${this.name}/${folder}${fileUrl}`;                            
                    this.zip[fileUrl]=uint;
                });
            });             
        });
    }
    
    str2zip(name,str,folder){
        let uint=[...new Uint8Array(this.str2dec(str))];
        uint.name=name;
        uint.modTime=new Date();
        uint.fileUrl=`${this.name}/${folder}${name}`;
        this.zip[uint.fileUrl]=uint;
    }
    
    files2zip(files,folder){
        for(let i=0;i<files.length;i++){
            files[i].arrayBuffer().then(data=>{
                let uint=[...new Uint8Array(data)];
                uint.name=files[i].name;
                uint.modTime=files[i].lastModifiedDate;
                uint.fileUrl=`${this.name}/${folder}${files[i].name}`;
                this.zip[uint.fileUrl]=uint;                            
            });
        }
    }
    
    makeZip(){
        let count=0;
        let fileHeader='';
        let centralDirectoryFileHeader='';
        let directoryInit=0;
        let offSetLocalHeader='00 00 00 00';
        let zip=this.zip;
        for(const name in zip){
            let modTime=()=>{
                lastMod=new Date(zip[name].modTime);
                hour=this.dec2bin(lastMod.getHours(),5);
                minutes=this.dec2bin(lastMod.getMinutes(),6);
                seconds=this.dec2bin(Math.round(lastMod.getSeconds()/2),5);
                year=this.dec2bin(lastMod.getFullYear()-1980,7);
                month=this.dec2bin(lastMod.getMonth()+1,4);
                day=this.dec2bin(lastMod.getDate(),5);                      
                return this.bin2hex(`${hour}${minutes}${seconds}`)+' '+this.bin2hex(`${year}${month}${day}`);
            }                   
            let crc=this.crc32(zip[name]);
            let size=this.reverse(parseInt(zip[name].length).toString(16).padStart(8,'0'));
            let nameFile=this.str2hex(zip[name].fileUrl).join(' ');
            let nameSize=this.reverse(zip[name].fileUrl.length.toString(16).padStart(4,'0'));
            let fileHeader=`50 4B 03 04 14 00 00 00 00 00 ${modTime} ${crc} ${size} ${size} ${nameSize} 00 00 ${nameFile}`;
            let fileHeaderBuffer=this.hex2buf(fileHeader);
            directoryInit=directoryInit+fileHeaderBuffer.length+zip[name].length;
            centralDirectoryFileHeader=`${centralDirectoryFileHeader}50 4B 01 02 14 00 14 00 00 00 00 00 ${modTime} ${crc} ${size} ${size} ${nameSize} 00 00 00 00 00 00 01 00 20 00 00 00 ${offSetLocalHeader} ${nameFile} `;
            offSetLocalHeader=this.reverse(directoryInit.toString(16).padStart(8,'0'));
            this.file.push(fileHeaderBuffer,new Uint8Array(zip[name]));
            count++;
        }
        centralDirectoryFileHeader=centralDirectoryFileHeader.trim();
        let entries=this.reverse(count.toString(16).padStart(4,'0'));
        let dirSize=this.reverse(centralDirectoryFileHeader.split(' ').length.toString(16).padStart(8,'0'));
        let dirInit=this.reverse(directoryInit.toString(16).padStart(8,'0'));
        let centralDirectory=`50 4b 05 06 00 00 00 00 ${entries} ${entries} ${dirSize} ${dirInit} 00 00`;
        this.file.push(this.hex2buf(centralDirectoryFileHeader),this.hex2buf(centralDirectory));                
        let a = document.createElement('a');
        a.href = URL.createObjectURL(new Blob([...this.file],{type:'application/octet-stream'}));
        a.download = `${this.name}.zip`;
        a.click();              
    }
}

接下来,创建一个名为 Zip 的新对象。

  z=new Zip('myZipFileName');

您可以:
  • 使用fecth2zip(filesArray,folder)将目录中的文件加载到zip对象中。
  filesArray=[
    'file01.ext',
    'file02.ext',
    'file...'
  ];
  z.fecth2zip(filesArray,'public/');
  • 使用 str2zip(nameFile,content,directory) 从字符串创建一个新文件。
  z.str2zip('test.txt','content','public/teste/');
  • 或者上传到zip文件中。

    将onchange事件放置在输入文件中,将文件发送到函数files2zip(this.files)。

  <input type="file" onchange="z.files2zip(this.files)" value='files' multiple>

将所有对象放入Zip对象文件中后,只需下载该文件。

  <input type="button" onclick="z.makeZip()" value='Zip'>

可以在此处找到该类: https://github.com/pwasystem/zip/


很棒的想法 - 写得很好 - 但对我不起作用。我尝试了以下操作,它下载了一个文件,但不是有效的压缩文件? var z=new Zip('myZipFileName'); z.str2zip('test.txt','content'); z.makeZip(); - Simon Sawyer
@SimonSawyer 我之前也遇到了同样的问题,但后来发现了错误:modTime是一个函数,而不是返回的值。 - undefined

4

这是一个比较旧的问题,但我在搜索创建zip归档文件的解决方案时遇到了它。

对于我的用例,我正在使用node.js从非常大的日志源每分钟创建数千个zip归档文件,每个归档文件最多包含200个文件。

我尝试过JSZip,由于性能问题和不值得追踪的内存泄漏而导致非常糟糕的结果。由于我的用例相当极端,所以它是一次很大的压力测试。

我在这里提到另一个值得其他人检查的纯JavaScript zip库。

最终我使用了fflate

https://github.com/101arrowz/fflate

这个库表现非常出色。我只使用9级压缩的zip归档功能,但fflate是一个全功能库,可在服务器端使用,并支持完整的浏览器(2011+)。我没有在这里包含任何示例,因为fflate文档非常详细。我强烈推荐它作为替代选择。


谢谢分享,这个 fflate 库看起来非常干净。 - Gin Quin

3
我建议您直接使用Node的内置库Zlib来处理这个问题,其中包括图像;使用“buffers”将其编码为base 64。而不是使用npm包。原因如下:
  • Zlib是Node本地库 - 近10年来一直保持最新版本 - 因此可以长期支持
  • Node允许您使用Buffers - 即您可以将文本字符串/图像转换为原始二进制数据,并使用Zlib进行压缩
  • 轻松压缩和解压大文件 - 利用节点流在MB或GB中压缩文件
事实上,您正在使用jszip,我可以猜测您同时在使用npm和node;假设您已正确设置环境,即全局安装了node。

示例:将input.txt压缩为input.txt.gz

const zlib = require('zlib');
const fs = require('fs');
const gzip = zlib.createGzip();
const input = fs.createReadStream('input.txt');
const output = fs.createWriteStream('input.txt.gz');

input.pipe(gzip).pipe(output);

步骤1:您需要从node中require每个本地模块 - require是ES5的一部分。如先前提到的Zlib,以及fs模块,即文件系统模块。

const zlib = require('zlib');
const fs = require('fs');

步骤二: 使用fs模块,可以创建readstream, 用于读取数据块。此方法返回一个readstream对象;可读流。

const input = fs.createReadStream(FILE PATH HERE);

注意:此读取流对象将再次进行管道传输;对读取流对象进行的这种管道串联可以无限进行,使得管道非常灵活。

ReadStream.pipe(DoesSomething).pipe(SomethingElse).pipe(ConvertToWriteStream)

步骤3:已经被管道处理和压缩的读取流对象接着就会转换成写入流对象。

const output = fs.createWriteStream('input.txt.gz');

input.pipe(gzip).pipe(output); // returned filename input.txt.gz, within local directory

这个库可以让你轻松输入文件路径并决定压缩文件的位置。如果需要,你也可以选择反向操作。


7
谢谢分享。不过我猜这个问题寻求的是浏览器端的解决方案,而不是服务器端的。你有关于浏览器端的想法吗?谢谢。 - Yuci

1

通过新的HTML5文件API和类型化数组,您几乎可以在JavaScript中实现任何想要的功能。然而,浏览器支持并不是很好。我猜这就是你所说的“未解决问题”。我建议暂时在服务器上进行操作。例如,在PHP中,您可以使用此扩展程序


这个答案并不是错误的,但当文件和用户数量增加时,PHP也会达到其极限。 - Nikita Fuchs

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接