简介
拖放 (DnD) 是 HTML5 的众多强大功能之一,Firefox 3.5、Safari、Chrome 和 IE 均支持此功能。 Google 近期推出了一项新功能,让 Google Chrome 用户可以将文件从浏览器拖放到桌面。这是一个非常方便的功能,但直到 Ryan Seddon 发布了一篇文章,介绍了他在对这项新功能进行逆向工程后所做的发现,它才广为人知。
Box.net 团队非常高兴这些新功能能够帮助我们改进云内容管理解决方案,并为开发者社区做出更多贡献。很高兴地通知您,DnD Download 已集成到我们的产品中。 现在,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>
我添加了一个 jQuery 插件,用于执行一些浏览器功能检测,该插件基于 von Schorsch 创建的 jQuery plugin,该插件又基于 Seddon 的文章。突出显示的行是我添加到 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.constructor 在 Firefox (Mozilla) 中会返回 DataTransfer,而 Webkit 浏览器(Chrome 和 Safari)会实现 Clipboard 构造函数。在 Safari 中,e.dataTransfer.setData('DownloadURL','http://www.box.net')
会返回 false,而 Chrome 会针对此语句返回 true。完成上述所有测试后,该功能将仅适用于 Chrome。您可能会说,我可以直接执行以下操作:
/chrome/.test( navigator.userAgent.toLowerCase() )
不过,我更倾向于使用功能检测,而不是浏览器检测,尽管从技术层面来说,这并不能检测 DnD 下载是否可行。
迭代 1 的问题
1) 由于我们目前已启用网页上拖放功能,以便在文件夹之间移动/复制文件,因此我们需要一种方法来区分拖放下载和网页上拖放。从技术层面来说,我们无法将这两项操作结合使用。我们无法预测用户是想将文件移至 Box.net 账号中的其他文件夹,还是想将其拖动到桌面。这两项操作完全不同。此外,没有简单的方法可以检测光标是否位于浏览器窗口之外。您可以使用 window.onmouseout(IE)和 document.onmouseout(其他浏览器)将 mouseout 事件附加到文档,并检查 e.relatedTarget.nodeName == "HTML"
(e 是 mouseout 事件或 window.event,以其中可用者为准)。但由于事件冒泡,这很难做到。当您悬停在图片或图层上时,此事件可能会随机触发,尤其是在 Box.net 等复杂的 Web 应用中。
2) 我们希望用户明确执行某项操作,以防止他们误将内容拖动到桌面。Box 文件夹的编辑者可能会上传可执行文件,该文件会在下载它的任何人的计算机上执行不良操作。我们希望用户能够确切知道文件何时会下载到桌面设备。
迭代 2
我们决定尝试使用“Ctrl + 拖动”(按住 Windows 上的 Ctrl 键拖动文件)。此操作与用户在 Windows 桌面上复制文件的方式一致。 此外,为了防止用户误下载文件,还需要用户额外付出工作(但不是额外执行步骤)。
由于我们需要将 DnD 下载功能与页面上的 DnD 紧密集成,因此现在已废弃迭代 1 中的 jQuery 插件。我们使用了经过修改的 jQuery UI 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 键之外,我们还添加了一个小 Toaster 提示,该提示会在用户执行常规的页面拖动操作时显示。它会告知用户,如果在按住 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 来下载文件,但无法使用拖放功能。
如需更好地说明“实际网址”和“重定向网址”之间的区别,请参阅以下屏幕截图:
迭代 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;
}
}
这很有道理。在拖动开始时,它会立即向服务器发出 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 拖动到即时通讯客户端,以便直接将其传输给好友……这为提高工作效率提供了无限可能。
反思和未来改进
这仍然不太理想,因为同步调用可能会短暂锁定浏览器。HTML 5 Web Worker 也无济于事,因为 Web Worker 必须是异步的。似乎必须在附加事件监听器时执行 setData。
实际上,性能还是相当不错的。同步 Ajax (Sjax) 调用只会检索网址字符串,速度应该很快。不过,它在 HTTP 标头中会产生大量开销,这可能可以通过 WebSocket 来解决。不过,在这种技术的使用量增加之前,不值得使用 WebSocket 将每项小更新发送到客户端。
我还希望将来能向该 API 添加多文件下载功能。结合使用自定义复选框在界面上选择多个文件,效果会非常棒。此外,如果客户端生成的文件(例如根据提交表单的结果生成的文本文件)也能以这种方式下载,那就更棒了。
- 列拖放
- 重新排列列表
- 创建图片库
- 导出画布图片
参考
- 拖放规范