案例研究 - 在 Chrome 中执行拖放式下载

David Tong
David Tong

简介

拖放 (DnD) 是 HTML 5 的众多强大功能之一,Firefox 3.5、Safari、Chrome 和 IE 均支持此功能。 Google 最近发布了一项新功能,可让 Google Chrome 用户将文件从浏览器拖放到桌面。这是一项非常方便的功能,但直到 Ryan Seddon 发布了一篇文章,探讨他在这个新功能上的逆向工程发现。

在 Box.net,我们很高兴地看到,这些新功能可帮助我们改进我们的云内容管理解决方案,并为开发者社区做出更多贡献。我们很高兴地宣布,DnD 下载已集成到我们的产品中。 现在,Box 用户可以将文件直接从 Chrome 浏览器拖到桌面,以下载并保存文件。

我想跟大家分享一下我如何在开发这项新功能的过程中经历了多次迭代。

检查拖放 API 支持

首先需要检查您的浏览器是否完全支持 HTML5 拖放。 一种简单的方法是使用名为 Modernizr 的库来检查某个功能:

if (Modernizr.draganddrop) {
// Browser supports native HTML5 DnD.
} else {
// Fallback to a library solution.
}

迭代 1

我先尝试了 Seddon 在 Gmail 中发现的方法。我添加了一个名为“data-downloadurl”的新属性 用于锚定文件链接此过程使用 HTML5 的自定义数据属性。在 data-downloadurl 中,您需要添加文件的 MIME 类型、目标文件名(已下载文件所需的文件名)和下载网址。因此,这会添加到 HTML 模板中:

<a href="#" class="dnd"
data-downloadurl="{$item.mime}:{$item.filename}:{$item.url}"></a>

这将生成如下所示的输出:

<a href="#" class="dnd" data-downloadurl=
"image/jpeg:Penguins.jpg:https://www.box.net/box_download_file?file_id=f66690"></a>

基于 von Schorsch 基于 Seddon 的文章创建的 jQuery plugin,我添加了一个可以检测浏览器功能的一个 jQuery 插件。其中突出显示了我在 von Schorsch 的版本中添加的几行代码:

(function($) {

$.fn.extend({
dragout: function() {
var files = this;
if (files.length > 0) {
    $(files).each(function() {
    var url = (this.dataset && this.dataset.downloadurl) ||
                this.getAttribute("data-downloadurl");
    if (this.addEventListener) {
        this.addEventListener("dragstart", function(e) {
        if (e.dataTransfer && e.dataTransfer.constructor == Clipboard &&
            e.dataTransfer.setData('DownloadURL', 'http://www.box.net')) {
            e.dataTransfer.setData("DownloadURL", url);
        }
        },false);
    }
    });
}
}
});

})(jQuery);

我之所以这样做,是因为在没有事先检测浏览器的情况下,对 IE 中的 HTML 元素执行 addEventListener() 会导致产生 JavaScript 错误,因为 IE 使用自己的 attachEvent() 方法。e.dataTransfer 在 IE 中未定义(截至目前),e.dataTransfer.structor 会在 Firefox (Mozilla) 中返回 DataTransfer,而 Webkit 浏览器(Chrome 和 Safari)则会实现剪贴板构造函数。 在 Safari 中,e.dataTransfer.setData('DownloadURL','http://www.box.net') 会返回 false,Chrome 则会对此语句返回 true。执行上述所有测试后,只有 Chrome 可以使用该功能。 您可能会认为我只需要执行以下操作:

/chrome/.test( navigator.userAgent.toLowerCase() )

但我更喜欢功能检测而非浏览器检测,尽管从技术层面来讲,这种方法并不能检测出 DnD 下载是否有效。

迭代 1 的问题

1) 由于我们当前启用了页内 DnD,以便在文件夹之间移动/复制文件,因此我们需要一种区分 DnD 下载和页内 DnD 的方法。从技术层面来讲,我们无法 将这两项操作结合起来使用我们无法预测用户是想要将文件移至 Box.net 帐号中的其他文件夹,还是将文件拖到桌面。这两项操作完全不同。 此外,没有什么简单的方法可以检测光标是否位于浏览器窗口之外。 您可以使用 window.onmouseout (IE) 和 document.onmouseout(其他浏览器)将鼠标移开事件附加到文档,然后检查 e.relatedTarget.nodeName == "HTML" 是否为 mouseout 事件或 window.event,以可用者为准。但由于有事件气泡,这非常困难。当您查看某个图片或图层时,该事件可能会随机触发,尤其是在 Box.net 等复杂的 Web 应用中。

2) 我们希望用户明确执行相应的操作,以防止他们错误地将某些内容拖出到桌面。Box 文件夹的编辑者可能会上传一个可执行文件,此文件会在任何下载者的计算机上执行一些不良操作。我们希望用户确切知道何时将文件下载到桌面。

迭代 2

我们决定尝试按住 Control + 拖动(按下 Windows Ctrl 键时拖动文件)。 此操作与用户在 Windows 桌面上复制文件的操作一致。 它还需要用户执行额外的操作(但不需要额外的步骤),以防止文件被意外下载。

第 1 次迭代中的 jQuery 插件现已弃用,因为我们需要将 DnD 下载与页内 DnD 紧密集成。对于感兴趣的用户,我们使用 jQuery 界面的 Draggable 插件的修改版本。在目标元素的 mousedown 事件中,我们添加以下代码:

// DnD to desktop when the Ctrl key is pressed while dragging
if (e.ctrlKey) {
var that = $(e.target);
// make sure it is not IE (attachEvent).
if (that[0].addEventListener) {
    that[0].addEventListener("dragstart",function(e) {
        // e.dataTransfer in Firefox uses the DataTransfer constructor
        // instead of Clipboard
        // make sure it's Chrome and not Safari (both webkit-based).
        // setData on DownloadURL returns true on Chrome, and false on Safari
        if (e.dataTransfer && e.dataTransfer.constructor == Clipboard &&
            e.dataTransfer.setData('DownloadURL','http://www.box.net')) {
        var url = (this.dataset && this.dataset.downloadurl) ||
                    this.getAttribute("data-downloadurl");
        e.dataTransfer.setData("DownloadURL", url);
        }
    }, false);
    return;
}
}

除了启用 Ctrl 键之外,我们还添加了一个小烤箱提示,当用户执行常规页面拖动操作时,系统就会显示该提示。如果在按住 Ctrl 键的同时将文件图标拖动到桌面,则会告知用户可下载文件。

迭代 2 的问题

出于安全方面的考虑,Box.net 不会公开用于直接访问静态文件的永久网址。这并非 Box.net 所独有。任何在线存储服务都不应提供永久网址,除非此类网址没有额外的安全保护,以检查文件是否公开以及是否由具有相应权限的用户请求下载。

如果跟踪某项内容的“下载网址”(例如 https://www.box.net/box_download_file?file_id=f_60466690),它会返回“302 Found”状态代码,并重定向到一个随机网址(例如 https://www.box.net/dl/6045?a=1f1207a084&m=168299,11211&t=2&b=aca15820d924e3b),即文件的临时“实际网址”。挑战在于,它每隔几分钟就会过期,因此将其放入 HTML 输出中是不切实际的。当用户尝试通过几分钟前生成的 HTML 输出中的链接下载文件时,它可能会返回“404”。

DnD 下载功能仅适用于直接指向资源的实际网址。 如果涉及重定向,目前跟踪链的智能程度还不够高(出于安全性考虑,它绝不能跟踪链)。因此,当您在浏览器地址栏中输入文件时,可通过上述链接 https://www.box.net/box_download_file?file_id=f_60466690 下载文件,但无法使用 DnD。

为了更好地说明“实际网址”和“重定向网址”之间的区别,请参见以下屏幕截图:

302 重定向网址
302 重定向网址
实际网址
实际网址

迭代 3

我们来试试 Ajax。

我们在上一次迭代中略微修改了代码,结果如下:

// DnD to desktop when the Ctrl key is pressed while dragging
if (e.ctrlKey) {
var that = $(e.target);
// make sure it is not IE (attachEvent).
if (that[0].addEventListener) {
that[0].addEventListener("dragstart", function(e) {
    // e.dataTransfer in Firefox uses the DataTransfer constructor
    // instead of Clipboard
    // make sure it's Chrome and not Safari (both webkit-based).
    // setData on DownloadURL returns true on Chrome, and false on Safari
    if (e.dataTransfer && e.dataTransfer.constructor == Clipboard &&
        e.dataTransfer.setData('DownloadURL', 'http://www.box.net')) {
    var url = (this.dataset && this.dataset.downloadurl) ||
                this.getAttribute("data-downloadurl");
    $.ajax({
        complete: function(data) {
        e.dataTransfer.setData("DownloadURL", data.responseText);
        },
        type:'GET',
        url: url
    });
    }
}, false);
return;
}
}

这是有道理的。Dragstart 会立即对服务器发出 Ajax 调用,以检索文件的最新下载网址。但是,它不起作用。

事实证明,它必须是同步调用(或者我喜欢称之为 Sjax)。 setData 似乎必须在附加事件监听器时完成。根据 jQuery 的 API,突出显示的行变为:

$.ajax({
async: false,
complete: function(data) {
e.dataTransfer.setData("DownloadURL", data.responseText);
},
type: 'GET',
url: url
});

在我拔掉网络连接前,连接可以正常使用。由于它会执行同步调用,因此浏览器会冻结,直到调用成功为止。如果 Ajax 调用失败(404 错误,或根本没有响应),浏览器根本不会像崩溃那样发生。

如下所示,这种做法会更安全:

$.ajax({
async: false,
complete: function(data) {
e.dataTransfer.setData("DownloadURL", data.responseText);
},
error: function(xhr) {
if (xhr.status == 404) {
    xhr.abort();
}
},
type: 'GET',
timeout: 3000,
url: url
});

如需观看此功能的演示,请随时将静态文件上传到 Box.net 帐号。 按住 Ctrl 键的同时将文件图标拖出桌面。没有帐号 只需不到 30 秒的时间即可创建一个

借助此功能,您可以发挥创造力,让很多事情变成可能。 将图片拖动到 Windows 打印机对话框中即可立即打印图片。 您可以将歌曲从 Box 复制到手机驱动器中,将文件从 Box 拖到您的即时通讯客户端,以便直接传输给您的朋友... 这为提高工作效率带来了无限可能。

将文件发送到打印机
将文件拖到打印机中。
将文件拖到 IM 客户端
将文件拖动到 IM 客户端。

想法和未来改进

这仍然不理想,因为同步调用可能会短暂锁定浏览器。HTML 5 Web Worker 也无法提供帮助,因为 Web Worker 必须是异步的。似乎必须在附加事件监听器时完成 setData。

实际上,其效果是可以接受的。同步 Ajax (Sjax) 调用只是检索网址字符串,所以速度应该非常快。它的 HTTP 标头会产生巨大的开销,这或许可以通过 WebSocket 解决。但是,在我们看到这种技术的更多应用之前,不值得使用 WebSocket 将每个小的更新都发送到客户端。

我还希望将来能为 API 增加多文件下载功能。如果再结合使用自定义复选框在界面中选择多个文件,效果会非常棒。此外,如果能够下载客户端生成的文件(例如根据提交表单的结果生成的文本文件),效果会更好。

  • 列 DND
  • 重新排列列表
  • 创建图库
  • 导出画布图片

参考