7

我正在创建一个需要从网站下载多个文件(图像和/或视频)的 Chrome 扩展程序。这些文件可能很大,所以我想向用户显示下载进度。经过一些研究,我发现目前可能的解决方案可能是:

  1. 使用 XMLHttpRequests 下载所有文件。
  2. 下载后,使用 JavaScript 库(例如 JSZip.js、zip.js)将所有文件压缩到一个存档中。
  3. 使用 SaveAs 对话框提示用户保存 zip。

我被困在第 2 段),如何压缩下载的文件?

为了理解,这是一个代码示例:

var fileURLs = ['http://www.test.com/img.jpg',...];
var zip = new JSZip();

var count = 0;
for (var i = 0; i < fileURLs.length; i++){
    var xhr = new XMLHttpRequest();
    xhr.onprogress = calculateAndUpdateProgress;
    xhr.open('GET', fileURLs[i], true);
    xhr.responseType = "blob";
    xhr.onreadystatechange = function () {
        if (xhr.readyState == 4) {
               var blob_url = URL.createObjectURL(response);
            // add downloaded file to zip:
            var fileName = fileURLs[count].substring(fileURLs[count].lastIndexOf('/')+1);
            zip.file(fileName, blob_url); // <- here's one problem

            count++;
            if (count == fileURLs.length){
                // all download are completed, create the zip
                var content = zip.generate();

                // then trigger the download link:
                var zipName = 'download.zip';
                var a = document.createElement('a'); 
                a.href = "data:application/zip;base64," + content;
                a.download = zipName;
                a.click();
            }
        }
    };
    xhr.send();
}

function calculateAndUpdateProgress(evt) {
    if (evt.lengthComputable) {
        // get download progress by performing some average 
        // calculations with evt.loaded, evt.total and the number
        // of file to download / already downloaded
        ...
        // then update the GUI elements (eg. page-action icon and popup if showed)
        ...
    }
}

上面的代码生成一个包含损坏的小文件的可下载存档。文件名同步还有一个问题:blob 对象不包含文件名,所以如果例如。fileURLs[0]下载时间比fileURLs[1]名称错误(倒置)要多。

注意:我知道 Chrome 有一个下载 API,但它在开发通道中,所以不幸的是它现在不是一个解决方案,我想避免使用 NPAPI 来完成这样一个简单的任务。

4

3 回答 3

12

我想起了这个问题..因为它还没有答案,我写了一个可能的解决方案,以防它对其他人有用:

  • 如前所述,第一个问题是将 blob url 传递给 jszip(它不支持 blob,但它也不会抛出任何错误来通知它,并且它成功生成损坏文件的存档):要纠正这个问题,只需传递一个 base64 字符串数据而不是其 blob 对象 url;
  • 第二个问题是文件名同步:这里最简单的解决方法是一次下载一个文件,而不是使用并行 xhr 请求。

所以,修改后的上层代码可以是:

var fileURLs = ['http://www.test.com/img.jpg',...];
var zip = new JSZip();
var count = 0;

downloadFile(fileURLs[count], onDownloadComplete);


function downloadFile(url, onSuccess) {
    var xhr = new XMLHttpRequest();
    xhr.onprogress = calculateAndUpdateProgress;
    xhr.open('GET', url, true);
    xhr.responseType = "blob";
    xhr.onreadystatechange = function () {
        if (xhr.readyState == 4) {
            if (onSuccess) onSuccess(xhr.response);
}

function onDownloadComplete(blobData){
    if (count < fileURLs.length) {
        blobToBase64(blobData, function(binaryData){
                // add downloaded file to zip:
                var fileName = fileURLs[count].substring(fileURLs[count].lastIndexOf('/')+1);
                zip.file(fileName, binaryData, {base64: true});
                if (count < fileURLs.length -1){
                    count++;
                    downloadFile(fileURLs[count], onDownloadCompleted);
                }
                else {
                    // all files have been downloaded, create the zip
                    var content = zip.generate();

                    // then trigger the download link:        
                    var zipName = 'download.zip';
                    var a = document.createElement('a'); 
                    a.href = "data:application/zip;base64," + content;
                    a.download = zipName;
                    a.click();
                }
            });
    }
}

function blobToBase64(blob, callback) {
    var reader = new FileReader();
    reader.onload = function() {
        var dataUrl = reader.result;
        var base64 = dataUrl.split(',')[1];
        callback(base64);
    };
    reader.readAsDataURL(blob);
}

function calculateAndUpdateProgress(evt) {
    if (evt.lengthComputable) {
        ...
    }
}

最后请注意,如果您下载的文件很少(少于 10 个文件的整体大小大约小于 1MB),则此解决方案效果很好,在其他情况下,JSZip 会在要生成存档时崩溃浏览器选项卡,所以它使用单独的线程进行压缩将是一个更好的选择(WebWorker,就像 zip.js 一样)。

如果在生成存档之后,浏览器仍然会因大文件而崩溃并且没有报告任何错误,请尝试在不传递二进制数据的情况下触发 saveAs 窗口,而是通过传递 blob 引用a.href = URL.createObjectURL(zippedBlobData);zippedBlobData引用生成的存档数据);

于 2013-07-17T10:33:44.597 回答
2
import JSZip from 'jszip'
import JSZipUtils from 'jszip-utils'
import FileSaver from 'file-saver'

const downloadZip = async (urls) => {
      const urlToPromise = (url) => {
        return new Promise((resolve, reject) => {
          JSZipUtils.getBinaryContent(url, (err, data) => {
            if (err) reject(err)
            else resolve(data)
          })
        })
      }

      const getExtension = (binary) => {
        const arr = (new Uint8Array(binary)).subarray(0, 4)
        let hex = ''
        for (var i = 0; i < arr.length; i++) {
          hex += arr[i].toString(16)
        }
        switch (hex) {
          case '89504e47':
            return 'png'
          case '47494638':
            return 'gif'
          case 'ffd8ffe0':
          case 'ffd8ffe1':
          case 'ffd8ffe2':
          case 'ffd8ffe3':
          case 'ffd8ffe8':
            return 'jpg'
          default:
            return ''
        }
      }

      this.progress = true

      const zip = new JSZip()
      for (const index in urls) {
        const url = urls[index]
        const binary = await urlToPromise(url)
        const extension = getExtension(binary) || url.split('.').pop().split(/#|\?/)[0]
        const filename = `${index}.${extension}`
        zip.file(filename, binary, { binary: true })
      }
      await zip.generateAsync({ type: 'blob' })
        .then((blob) => {
          FileSaver.saveAs(blob, 'download.zip')
        })
}

downloadZip(['https://example.net/1.jpg', 'https://example.net/some_picture_generator'])
于 2020-07-29T09:46:46.807 回答
1

基于@guari的代码,我在本地进行了测试,并应用到了react应用中,附上代码供他人参考。

import JSZip from "jszip";
import saveAs from "jszip/vendor/FileSaver.js";

// .......

// download button click event
btnDownloadAudio = record =>{
    let fileURLs = ['https://www.test.com/52f6c50.AMR', 'https://www.test.com/061940.AMR'];
    let count = 0;
    let zip = new JSZip();
    const query = { record, fileURLs, count, zip };
    this.downloadFile(query, this.onDownloadComplete);
}
downloadFile = (query, onSuccess) => {
    const { fileURLs, count, } = query;
    var xhr = new XMLHttpRequest();
    xhr.onprogress = this.calculateAndUpdateProgress;
    xhr.open('GET', fileURLs[count], true);
    xhr.responseType = "blob";
    xhr.onreadystatechange = function (e) {
        if (xhr.readyState == 4) {
            if (onSuccess) onSuccess(query, xhr.response);
        }
    }
    xhr.send();
}
onDownloadComplete = (query, blobData) => {
    let { record, fileURLs, count, zip } = query;
    if (count < fileURLs.length) {
      const _this = this;
      const { audio_list, customer_user_id, } = record;
      this.blobToBase64(blobData, function(binaryData){
        // add downloaded file to zip:
        var sourceFileName = fileURLs[count].substring(fileURLs[count].lastIndexOf('/')+1);
        // convert the source file name to the file name to display
        var displayFileName = audio_list[count].seq + sourceFileName.substring(sourceFileName.lastIndexOf('.'));
        zip.file(displayFileName, binaryData, {base64: true});
        if (count < fileURLs.length -1){
            count++;
            _this.downloadFile({ ...query, count }, _this.onDownloadComplete);
        }
        else {
            // all files have been downloaded, create the zip
            zip.generateAsync({type:"blob"}).then(function(content) {
                // see FileSaver.js
                saveAs(content, `${customer_user_id}.zip`);
            });
        }
      });
    }
}
blobToBase64 = (blob, callback) => {
    var reader = new FileReader();
    reader.onload = function() {
        var dataUrl = reader.result;
        var base64 = dataUrl.split(',')[1];
        callback(base64);
    };
    reader.readAsDataURL(blob);
}
calculateAndUpdateProgress = (evt) => {
    if (evt.lengthComputable) {
        // console.log(evt);
    }
}

于 2019-09-05T08:09:40.727 回答