個案研究 - 在 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 未偵測到瀏覽器,將 addEventListener() 套用到 IE 的 HTML 元素,將會產生 JavaScript 錯誤,因為 IE 使用其專屬的 includeEvent() 方法。e.dataTransfer 尚未在 IE 中定義(e.dataTransfer 尚未定義),e.dataTransfer.buildor 會在 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) 由於我們目前已啟用網頁內 DnD,可移動/複製不同資料夾之間的檔案,因此需要區分鄧白氏環球編碼和網頁內 DnD。嚴格來說,我們無法結合這兩種動作我們無法預測使用者是否要將檔案移至 Box.net 帳戶中的其他資料夾,或是將其拖曳至桌面。這兩個操作完全不同。 此外,目前沒有簡單的方法可以偵測遊標是否在瀏覽器視窗外。 您可使用 window.onmouseout (IE) 和 document.onmouseout (其他瀏覽器) 將 mouseout 事件附加到文件,並檢查 e.relatedTarget.nodeName == "HTML" (e is mouseout 事件或 window.event.event,如果有的話) 是否存在。但由於事件會爆炸,這並不容易。位於圖片或圖層上時,這個事件可能會隨機觸發,尤其是在 Box.net 等複雜的網頁應用程式中。

2) 我們希望使用者明確採取特定行動,避免使用者誤將內容拖曳至桌面。Box 資料夾的編輯者有可能可以上傳可執行檔,讓該檔案在下載者電腦上執行不必要的操作。我們希望使用者得知檔案下載至桌面的時間。

疊代作業 2

我們決定試用 Ctrl + 拖曳功能 (按下 Windows Ctrl 鍵時拖曳檔案)。 這個動作是確定使用者可在 Windows 桌面執行複製檔案的動作。 也需要使用者執行額外作業 (但不需要採取額外步驟),以免檔案誤遭下載。

由於我們需要將 DnD Download 與網頁內 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 鍵以外,我們也新增了小浮動式工具提示,在使用者執行一般網頁內拖曳時會出現。告知使用者如果按住 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;
}
}

這很合理。拖曳時,系統會立即向伺服器發出 Ajax 呼叫,擷取檔案的最新下載網址。但行不通。

結果證明該 API 必須是同步呼叫 (或是像 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 用戶端,以便直接傳輸給好友... 這樣可以帶來無限可能提升工作效率。

將檔案拖曳到印表機
將檔案拖曳到印表機上。
將檔案拖曳至即時訊息用戶端
將檔案拖曳到即時訊息用戶端。

想法與未來改善

不過,這個方法仍然不太理想,因為同步呼叫可能會導致瀏覽器短暫鎖定。HTML 5 Web Worker 也都幫不上忙,因為網路工作站必須非同步。似乎在附加事件監聽器時必須完成 setData。

實際上,此效能相當可觀。同步 Ajax (Sjax) 呼叫只會擷取網址字串,處理速度應該相當快。它會在 HTTP 標頭中伴隨大型負荷,可透過 WebSocket 處理。然而,在我們觀察到這類技術的更多使用率前,並不值得使用 WebSocket 將每一次更新傳送至用戶端。

我也希望日後 API 還會加入多檔案下載功能。若與自訂核取方塊搭配使用,以在使用者介面上選取多個檔案,就非常值得期待。此外,如果用戶端產生的檔案 (例如表單提交結果產生的文字檔案) 可以透過這種方式下載,效果會更佳。

  • 第 d 欄
  • 重新排列清單
  • 建立圖片庫
  • 匯出畫布圖片

參考資料