使用单个操作下载多个文件

132

我不确定是否可以使用标准的Web技术实现这一点。

我想让用户能够在单个操作中下载多个文件。也就是说,点击文件旁边的复选框,然后获取所有选中的文件。

如果可能的话,你推荐什么基本策略?我知道我可以使用comet技术创建服务器端事件来触发HttpResponse,但我希望有更简单的方法。


你可以运行这个程序两次,它能正常工作。 - theshubhagrwl
19个回答

100

var links = [
  'https://s3.amazonaws.com/Minecraft.Download/launcher/Minecraft.exe',
  'https://s3.amazonaws.com/Minecraft.Download/launcher/Minecraft.dmg',
  'https://s3.amazonaws.com/Minecraft.Download/launcher/Minecraft.jar'
];

function downloadAll(urls) {
  var link = document.createElement('a');

  link.setAttribute('download', null);
  link.style.display = 'none';

  document.body.appendChild(link);

  for (var i = 0; i < urls.length; i++) {
    link.setAttribute('href', urls[i]);
    link.click();
  }

  document.body.removeChild(link);
}
<button onclick="downloadAll(window.links)">Test me!</button>


3
我处理多种文件类型,包括图片,在这方面这个方法对我来说最有效。然而,link.setAttribute('download', null);将所有我的文件重命名为null。 - tehlivi
8
不适用于IE 11,在IE11中只会下载.jar文件(列表中的最后一项),这原本是一个完美的解决方案 :( - Immutable Brick
18
在Chrome v75,Windows 10中无法正常工作:唯一下载的文件是Minecraft.jar - andreivictor
3
这不是现代大多数浏览器的正确方法,只有最后一个文件会被下载。 - Ashish Singh
4
无效,只有最后一个文件被下载。 - Gibs
显示剩余10条评论

66

HTTP不支持同时下载多个文件。

有两种解决方案:

  • 打开x个窗口来启动文件下载(这可以通过JavaScript完成)
  • 首选解决方案 创建一个脚本来压缩这些文件。

55
为什么ZIP文件是首选解决方案?这会为用户创建一个额外的步骤(解压缩)。 - speedplane
7
这个页面包含能够创建ZIP文件的JavaScript代码。可以查看该页面上的优秀示例:https://stuk.github.io/jszip/。 - Netsi1964
第三种方法是将文件封装到SVG文件中。如果这些文件在浏览器中显示,SVG似乎是最好的方式。 - VectorVortec
4
HTTP本身支持多部分消息格式。但浏览器在从服务器端解析多部分响应时不具备可移植性,但从技术上讲,这并不困难。 - CMCDragonkai
3
这可以是使用JavaScript的一个很好的解决方案,https://github.com/sindresorhus/multi-download。 - juananruiz
@juananruiz 上述解决方案仅适用于用户请求指定文件下载位置的第一个文件。看起来很有前途 - 感谢分享。 - BenKoshy

60

你可以创建一组临时的隐藏iframe,在其中执行GET或POST请求来启动下载,等待下载开始后再移除这些iframe:

<!DOCTYPE HTML>
<html>
<body>
  <button id="download">Download</button> 

  <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.4/jquery.min.js"></script>
  <script type="text/javascript">

     $('#download').click(function() {
       download('http://nogin.info/cv.doc','http://nogin.info/cv.doc');
     });

     var download = function() {
       for(var i=0; i<arguments.length; i++) {
         var iframe = $('<iframe style="visibility: collapse;"></iframe>');
         $('body').append(iframe);
         var content = iframe[0].contentDocument;
         var form = '<form action="' + arguments[i] + '" method="GET"></form>';
         content.write(form);
         $('form', content).submit();
         setTimeout((function(iframe) {
           return function() { 
             iframe.remove(); 
           }
         })(iframe), 2000);
       }
     }      

  </script>
</body>
</html>

或者,不使用jQuery:

 function download(...urls) {
    urls.forEach(url => {
      let iframe = document.createElement('iframe');
      iframe.style.visibility = 'collapse';
      document.body.append(iframe);

      iframe.contentDocument.write(
        `<form action="${url.replace(/\"/g, '"')}" method="GET"></form>`
      );
      iframe.contentDocument.forms[0].submit();

      setTimeout(() => iframe.remove(), 2000);
    });
  }

很棒,但由于某些原因,文件无法下载。在我看来,原因似乎是脚本执行后页面重新加载,这似乎是文件无法下载的原因。你有什么线索吗?我做错了什么? - Chirag Mehta
我对这个解决方案有多个问题。在IE中,由于我的父窗口更改了document.domain,我遇到了访问被拒绝的问题。有很多关于修复此问题的帖子,但都感觉很不正规。在Chrome中,用户会收到警告消息提示网站尝试下载多个文件(但至少它可以工作)。在Firefox中,我得到了不同的下载框,但当我点击保存时,我没有得到保存文件对话框... - Melanie
1
这对我没有用,因为文件对话框“阻止”其他保存对话框出现。我所做的是有点hacky的 - 鼠标移动操作仅在文件对话框消失后才注册,所以我使用了它 - 但它没有经过测试。我会将其作为另一个答案添加。 - Karel Bílek
2
这在IE10中能用吗?我得到的是:“对象不支持属性或方法'write'”。 - Hoppe
为什么在 setTimeout() 中返回一个函数(闭包)? - robisrob
显示剩余4条评论

53

这个解决方案可以跨浏览器工作,不会触发警告。我们不是创建一个iframe而是为每个文件创建一个链接。这可以防止弹出警告信息。

为了处理循环部分,我们使用setTimeout,这在IE中非常必要。

更新于2021年: 我知道“运行代码片段”不再起作用,但这是由于跨站点cookie问题造成的。如果部署在您自己的网站上,该代码将正常工作。

/**
 * Download a list of files.
 * @author speedplane
 */
function download_files(files) {
  function download_next(i) {
    if (i >= files.length) {
      return;
    }
    var a = document.createElement('a');
    a.href = files[i].download;
    a.target = '_parent';
    // Use a.download if available, it prevents plugins from opening.
    if ('download' in a) {
      a.download = files[i].filename;
    }
    // Add a to the doc for click to work.
    (document.body || document.documentElement).appendChild(a);
    if (a.click) {
      a.click(); // The click method is supported by most browsers.
    } else {
      $(a).click(); // Backup using jquery
    }
    // Delete the temporary link.
    a.parentNode.removeChild(a);
    // Download the next file with a small timeout. The timeout is necessary
    // for IE, which will otherwise only download the first file.
    setTimeout(function() {
      download_next(i + 1);
    }, 500);
  }
  // Initiate the first download.
  download_next(0);
}
<script>
  // Here's a live example that downloads three test text files:
  function do_dl() {
    download_files([
      { download: "https://stackoverflow.com/robots.txt", filename: "robots.txt" },
      { download: "https://www.w3.org/TR/PNG/iso_8859-1.txt", filename: "standards.txt" },
      { download: "http://qiime.org/_static/Examples/File_Formats/Example_Mapping_File.txt", filename: "example.txt" },
    ]);
  };
</script>
<button onclick="do_dl();">Test downloading 3 text files.</button>


这是我在这里唯一有效的方法,因为我必须支持IE。谢谢。 - Øystein Amundsen
1
这个答案是黄金级别的。在所有浏览器中都可以正常工作,不会出现警告信息,特别是IE浏览器。太棒了! - Mukul Goel
4
按钮没有任何反应。Google Chrome版本76.0.3809.100(正式构建)(64位) - user1934286
1
在 Stack Overflow 的运行代码片段中,按钮无法正常工作。浏览器为 Chrome。@speedplane - m b
1
似乎不再起作用了。它只下载第一个文件而没有警告。 - Nikolay
显示剩余6条评论

6
以下脚本优雅地完成了这项工作。
var urls = [
'https://images.pexels.com/photos/432360/pexels-photo-432360.jpeg',
'https://images.pexels.com/photos/39899/rose-red-tea-rose-regatta-39899.jpeg'
];

function downloadAll(urls) {


  for (var i = 0; i < urls.length; i++) {
    forceDownload(urls[i], urls[i].substring(urls[i].lastIndexOf('/')+1,urls[i].length))
  }
}
function forceDownload(url, fileName){
    var xhr = new XMLHttpRequest();
    xhr.open("GET", url, true);
    xhr.responseType = "blob";
    xhr.onload = function(){
        var urlCreator = window.URL || window.webkitURL;
        var imageUrl = urlCreator.createObjectURL(this.response);
        var tag = document.createElement('a');
        tag.href = imageUrl;
        tag.download = fileName;
        document.body.appendChild(tag);
        tag.click();
        document.body.removeChild(tag);
    }
    xhr.send();
}

缺点:没有像常规下载那样的“下载中断-继续”功能(通过浏览器自动与服务器进行范围请求协商)。 - Xenos
这个代码可以运行,但是当我执行它时,似乎会下载两次文件。你有什么想法吗? - cjochum
@cbartell,也许您已经在urls数组中重复添加了URL。请测试上述示例,以查看是否会下载文件两次。 - Shyam Narayan
这实际上是将文件作为 blob 下载。因此用户无法看到进度。 - Hardik Patel
由于urls已经在全局范围内定义,因此无需作为参数传递。 downloadAll() {似乎就足够了。将其放在按钮后面,它可以很好地工作 :-) - Mast

5
最简单的方法是将多个文件捆绑成一个ZIP文件进行提供。
我想你可以使用一堆iframe或弹出窗口来启动多个文件下载,但从可用性的角度来看,ZIP文件仍然更好。谁愿意点击十个浏览器会弹出的“另存为”对话框呢?

3
我知道你的回答是在2010年写的,但现在很多用户都用智能手机浏览网页,其中一些智能手机默认情况下无法打开zip文件(我的朋友告诉我他的三星S4 Active无法打开zip文件)。 - Hydraxan14

4

Angular 解决方案:

HTML

    <!doctype html>
    <html ng-app='app'>
        <head>
            <title>
            </title>
            <link rel="stylesheet" href="http://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css">
            <link rel="stylesheet" href="style.css">
        </head>
        <body ng-cloack>        
            <div class="container" ng-controller='FirstCtrl'>           
              <table class="table table-bordered table-downloads">
                <thead>
                  <tr>
                    <th>Select</th>
                    <th>File name</th>
                    <th>Downloads</th>
                  </tr>
                </thead>
                <tbody>
                  <tr ng-repeat = 'tableData in tableDatas'>
                    <td>
                        <div class="checkbox">
                          <input type="checkbox" name="{{tableData.name}}" id="{{tableData.name}}" value="{{tableData.name}}" ng-model= 'tableData.checked' ng-change="selected()">
                        </div>
                    </td>
                    <td>{{tableData.fileName}}</td>
                    <td>
                        <a target="_self" id="download-{{tableData.name}}" ng-href="{{tableData.filePath}}" class="btn btn-success pull-right downloadable" download>download</a>
                    </td>
                  </tr>              
                </tbody>
              </table>
                <a class="btn btn-success pull-right" ng-click='downloadAll()'>download selected</a>

                <p>{{selectedone}}</p>
            </div>
            <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.0/jquery.min.js"></script>
            <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.8/angular.min.js"></script>
            <script src="script.js"></script>
        </body>
    </html>

app.js

var app = angular.module('app', []);            
app.controller('FirstCtrl', ['$scope','$http', '$filter', function($scope, $http, $filter){

$scope.tableDatas = [
    {name: 'value1', fileName:'file1', filePath: 'data/file1.txt', selected: true},
    {name: 'value2', fileName:'file2', filePath: 'data/file2.txt', selected: true},
    {name: 'value3', fileName:'file3', filePath: 'data/file3.txt', selected: false},
    {name: 'value4', fileName:'file4', filePath: 'data/file4.txt', selected: true},
    {name: 'value5', fileName:'file5', filePath: 'data/file5.txt', selected: true},
    {name: 'value6', fileName:'file6', filePath: 'data/file6.txt', selected: false},
  ];  
$scope.application = [];   

$scope.selected = function() {
    $scope.application = $filter('filter')($scope.tableDatas, {
      checked: true
    });
}

$scope.downloadAll = function(){
    $scope.selectedone = [];     
    angular.forEach($scope.application,function(val){
       $scope.selectedone.push(val.name);
       $scope.id = val.name;        
       angular.element('#'+val.name).closest('tr').find('.downloadable')[0].click();
    });
}         


}]);

工作示例:https://plnkr.co/edit/XynXRS7c742JPfCA3IpE?p=preview

这是一个工作示例的链接,你可以点击进入查看。

4

我认为,zip文件是更整洁的解决方案......但是如果你必须传送多个文件,这里是我想出的解决方案。它适用于IE 9及以上版本(可能也适用于较低版本——我没有测试过),Firefox,Safari和Chrome。Chrome会向用户显示一条消息,要求他同意下载多个文件,这是您的网站首次使用它时。

function deleteIframe (iframe) {
    iframe.remove(); 
}
function createIFrame (fileURL) {
    var iframe = $('<iframe style="display:none"></iframe>');
    iframe[0].src= fileURL;
    $('body').append(iframe);
    timeout(deleteIframe, 60000, iframe);             
 }
 // This function allows to pass parameters to the function in a timeout that are 
 // frozen and that works in IE9
 function timeout(func, time) {
      var args = [];
      if (arguments.length >2) {
           args = Array.prototype.slice.call(arguments, 2);
      }
      return setTimeout(function(){ return func.apply(null, args); }, time);
 }
 // IE will process only the first one if we put no delay
 var wait = (isIE ? 1000 : 0);
 for (var i = 0; i < files.length; i++) {  
      timeout(createIFrame, wait*i, files[i]);
 }

这种技术的唯一副作用是用户会在提交和下载对话框显示之间看到延迟。为了最小化这种影响,我建议您使用描述在这里和这个问题“检测浏览器接收文件下载”中的技巧,该技巧包括使用cookie来知道文件何时开始下载。您需要在客户端检查此cookie,并在服务器端发送它。不要忘记设置cookie的正确路径,否则您可能看不到它。您还需要为多个文件下载调整解决方案。

4

一个 jQuery 版本的 iframe 解决方案:

function download(files) {
    $.each(files, function(key, value) {
        $('<iframe></iframe>')
            .hide()
            .attr('src', value)
            .appendTo($('body'))
            .load(function() {
                var that = this;
                setTimeout(function() {
                    $(that).remove();
                }, 100);
            });
    });
}

每个人都在寻找一个数组。这将起作用:download(['http://nogin.info/cv.doc','http://nogin.info/cv.doc']); 然而,对于下载图像文件,这种方法不起作用。 - tehlivi

3

这适用于所有浏览器(IE11、Firefox、Edge、Chrome和Chrome移动版)。我的文档在多选元素中。当您尝试过快进行操作时,浏览器似乎会遇到问题...因此我使用了超时。

//user clicks a download button to download all selected documents
$('#downloadDocumentsButton').click(function () {
    var interval = 1000;
    //select elements have class name of "document"
    $('.document').each(function (index, element) {
        var doc = $(element).val();
        if (doc) {
            setTimeout(function () {
                window.location = doc;
            }, interval * (index + 1));
        }
    });
});

这是一个使用 Promise 的解决方案:
 function downloadDocs(docs) {
        docs[0].then(function (result) {
            if (result.web) {
                window.open(result.doc);
            }
            else {
                window.location = result.doc;
            }
            if (docs.length > 1) {
                setTimeout(function () { return downloadDocs(docs.slice(1)); }, 2000);
            }
        });
    }

 $('#downloadDocumentsButton').click(function () {
        var files = [];
        $('.document').each(function (index, element) {
            var doc = $(element).val();
            var ext = doc.split('.')[doc.split('.').length - 1];

            if (doc && $.inArray(ext, docTypes) > -1) {
                files.unshift(Promise.resolve({ doc: doc, web: false }));
            }
            else if (doc && ($.inArray(ext, webTypes) > -1 || ext.includes('?'))) {
                files.push(Promise.resolve({ doc: doc, web: true }));
            }
        });

        downloadDocs(files);
    });

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