下载 <href="blob:http://url/guid" download="filename" /> 忽略下载属性。

3

我通过在线教程和示例构建了一个基于jQuery客户端和MS WebAPI服务器的文件下载系统。

由于API需要身份验证,因此无法直接提供文件链接,因此文件URL是API终点而不是文件位置。

在服务器上,我有以下内容:

[HttpGet]
[Route("download/{filename}")]
public HttpResponseMessage DownloadFile(string filename)
{
    try
    {
        // https://gist.github.com/joeriks/3714093
        string path = string.Format("{0}/Exports/{1}", root, filename);

        HttpResponseMessage result = new HttpResponseMessage(HttpStatusCode.OK);
        var stream = new FileStream(path, FileMode.Open);
        result.Content = new StreamContent(stream);
        result.Content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
        result.Content.Headers.ContentDisposition = new ContentDispositionHeaderValue("attachment");
        result.Content.Headers.ContentDisposition.FileName = "download.txt";
        return result;

    }
    catch (Exception ex)
    {
        throw new HttpResponseException(HttpStatusCode.InternalServerError);
    }
}

返回预期的响应:

HTTP/1.1 200 OK
Cache-Control: no-cache
Pragma: no-cache
Content-Length: 4809
Content-Type: application/octet-stream
Expires: -1
Server: Microsoft-IIS/10.0
Content-Disposition: attachment; filename=download.txt
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: POST, PUT, DELETE, GET, OPTIONS
Access-Control-Allow-Headers: content-Type, accept, origin, X-Requested-With, X-Authentication, X-Nonce, name
Date: Thu, 25 Oct 2018 13:07:25 GMT

在响应中有文本内容。到目前为止,一切都如我所希望的。

在客户端上,我有以下代码来处理从API返回的响应:

// https://dev59.com/e2Qo5IYBdhLWcg3wZepg#23797348
let disposition = jqXHR.getResponseHeader('Content-Disposition');

if (disposition && disposition.indexOf('attachment') !== -1) {
    let filename = "scada-download.txt";
    let matches = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/.exec(disposition);
    if (matches != null && matches[1]) filename = matches[1].replace(/['"]/g, '');

    let type = jqXHR.getResponseHeader('Content-Type');
    let blob = new Blob([data], { type: "text/csv" });

    var downloadUrl = URL.createObjectURL(blob);
    let $a = $("<a id='temp_download_link' style='display: none;' />").attr("href", downloadUrl).attr("download", filename);
    $("body").append($a);
    $a.trigger("click");
}

这个功能的确如广告所说,它会在页面上添加一个锚点并点击它以触发下载。
下载可以正常工作,并保存具有正确内容的文件。
唯一不起作用的部分是,在我测试过的两个浏览器中(Chrome 69、FF: 62),提供的默认文件名只是一个GUID。
目前,api和客户端代码都运行在我的本地开发机上,http://127.0.0.1:9000/[client | api],因此跨域应该不会造成任何问题。
通过ajax调用api。最终使用jQuery $.ajax()方法。
为了清晰起见,插入到DOM中的锚点是:
<a id="temp_download_link" style="display: none;" href="blob:http://127.0.0.1:9000/c2c5ffb5-3f22-4a57-8775-4e0bbfbfef9e" download="download.txt"></a>

Chrome默认提供的文件名是URL中的GUID,而FF则生成一个看似无关的6个字符的随机字符串。 具体地,为什么浏览器都忽略锚点的download="download.txt"属性和Content-Disposition: attachment; filename=download.txt? 更新: 我forked了这个fiddle:

http://jsfiddle.net/Qjvb3/

添加了一些其他值到href属性中,似乎整个设置文件名的过程最好不要轻易改动:

http://jsfiddle.net/yubjqwvs/

更新2

我将一个可行示例的链接复制到了我的fiddle中,它可以从原始网站工作,但不能从fiddle中工作。

http://jsfiddle.net/yubjqwvs/2/

我有一种感觉,答案归结为“为什么它在David Walsh博客上有效,但在fiddle上无效?”

也许这是个愚蠢的问题,但你尝试过filename="download.txt"吗?同时,清除了缓存(真的,完全清除了整个缓存)吗? - Alex
@Alex,我之前没有尝试过,但现在我已经尝试了。没有任何区别。 - Morvael
我认为这个小样将帮助我们进行更多的调试。无论看起来有多奇怪,一定有一个原因。 - noobed
在你提供的例子中,有几个需要注意的地方。首先,“It doesn't save”按钮实际上按预期工作,因为它试图打开链接而不是下载它。(在这里的规范中有描述:https://html.spec.whatwg.org/multipage/links.html#as-a-download)。 其次,由于下载属性,.woff文件未被下载。实际上发生的是浏览器试图将您的页面重定向到该链接,但不知道如何呈现.woff,因此会下载它。您可以通过将.woff更改为css/font-awesome.css来进行测试。 :) - noobed
@Morvael 我已经玩过并稍微调整了一下你的演示 - http://jsfiddle.net/kos3rjfq/19/. 我可以编辑我的初始答案,因为它对这个情况确实没有意义。 :) - noobed
显示剩余3条评论
3个回答

0

在我的情况下,我个人使用downloadjs来避免类似的问题:

  • 发出一个AJAX请求以下载文件;这只是一个带有适当头部(Content-Disposition: attachment; filename="...")的PHP脚本
  • 用户点击链接,使用哈希更改事件触发下载,然后将AJAX响应传递给downloadjs。我想可以使用Blob URL完成相同的操作。

downloadjs的代码与jsfiddle非常相似,基于您的示例。它在Firefox 63、Opera 56上运行良好(我猜它应该在Chrome上运行),但在IE 11上失败了,但是,那是IE。

  • downloadjs创建一个隐藏的链接,并将超时设置为66ms(我想这是为了等待DOM准备就绪或类似的事情)。
  • 超时调用链接上的click(),可能调用本机处理程序。

使用您的代码,并使用click()以及document.getElementById()而不是jQuery,给我们提供了这个jsfiddle。与downloadjs相反,我没有使用window.setTimeout,它可以正常工作,至少在Fx 63中。这可能只是绕过浏览器错误的一种方式。

在您的情况下,我猜想当您调用trigger('click')时,jQuery没有触发默认处理程序,这让我无法理解,因为trigger文档似乎告诉我们它会:

从jQuery 1.3开始,.trigger()触发的事件会冒泡到DOM树上;事件处理程序可以通过从处理程序返回false或调用传递给事件的事件对象上的.stopPropagation()方法来停止冒泡。虽然.trigger()模拟了事件激活,包括合成的事件对象,但它并不能完美地复制自然发生的事件。
要触发通过jQuery绑定的处理程序而不触发本机事件,请改用.triggerHandler()。 (引用自jquery文档)
也许默认情况下,链接上的默认点击不被认为是jquery的处理程序,并且不会被触发。

0
我看到的原因是您正在使用jQuery的.attr()函数而不是.prop()函数。 HTML5属性和属性之间有区别。第一个为元素提供标记,用于事件绑定等情况,而另一个则访问/设置DOM元素本身的值。
我会链接一个可能有用的解释这里

这对我来说听起来不太可能,因为一旦我将节点插入DOM中,就不会再对其进行操作,因此不应该有任何影响(初始值从未更改,因此即使它不是纯反射属性,我也应该得到相同的结果)。无论如何,我还是尝试了一下,但结果仍然相同。 - Morvael

0
当这个问题变得紧急需要修复时,我最终回到了它。结果发现,我的(自制的)SPA框架是问题所在,因为它覆盖了浏览器默认的<a />点击事件。
我通过添加一个针对blob链接的catch来解决了这个问题,然后触发默认的点击事件:
// convert all a/href to a#href
$("body").delegate("a", "click", function () {
    let href: string = $(this).attr("href"); 

    // check its not an external / absolute URL
    let regex: RegExp = new RegExp("^(blob:)?(http|https)(:\/\/)", "ig");
    let match = regex.exec(href);
    if (match) {
        // match[0] is the full match, match[1] is lookign for "blob:"
        // it will either be undefined or blob:
        if (match[1]) { // its a blob url, call the default
            return true; // !! this line is the core of the fix !!
        }
        else { // load a normal link
            // see if its got a target.
            let target: string = $(this).attr("target");

            switch (target) {
                default: document.location.href = href; break;
                case "_blank": window.open(href); break;
            }
        }   
    }
    else
        // SPA stuff
    return false;
});

所以本质上这是一个没有人会遇到的问题,除非他们像我一样愚蠢到自己开发SPA框架。

它在这里 https://github.com/JohnRayson/JSPA


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