Nghiên cứu điển hình – Tải xuống bằng tính năng kéo và thả trong Chrome

Giới thiệu

Kéo và thả (DnD) là một trong nhiều tính năng tuyệt vời của HTML 5 và nó được hỗ trợ trong Firefox 3.5, Safari, Chrome và IE. Gần đây, Google đã ra mắt một tính năng mới cho phép người dùng Google Chrome kéo và thả tệp từ trình duyệt vào máy tính. Đây là một tính năng cực kỳ tiện lợi, nhưng tính năng này chưa được nhiều người biết đến cho đến khi Ryan Seddon đăng một bài viết về những phát hiện kỹ thuật đảo ngược của ông đối với tính năng mới này.

Tại Box.net, chúng tôi rất vui mừng về cách những chức năng mới này giúp chúng tôi cải thiện giải pháp quản lý nội dung trên đám mây, cũng như đóng góp nhiều hơn cho cộng đồng nhà phát triển. Tôi vui mừng thông báo rằng DnD Download đã được tích hợp vào sản phẩm của chúng tôi. Giờ đây, người dùng Box có thể kéo tệp trực tiếp từ trình duyệt Chrome vào máy tính để tải xuống và lưu tệp.

Tôi muốn chia sẻ trải nghiệm của tôi qua nhiều lần lặp lại trong quá trình phát triển tính năng mới này.

Kiểm tra xem có hỗ trợ API kéo và thả hay không

Điều đầu tiên cần làm là kiểm tra xem trình duyệt của bạn có hỗ trợ đầy đủ tính năng kéo và thả HTML5 hay không. Bạn có thể dễ dàng thực hiện việc đó bằng cách sử dụng thư viện có tên Modernizr để kiểm tra một tính năng nhất định:

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

Lặp lại 1

Lần đầu tiên tôi thử phương pháp mà Seddon tìm thấy trong Gmail. Tôi đã thêm một thuộc tính mới có tên là "data-downloadurl" vào đường liên kết neo của các tệp. Quá trình này sử dụng các thuộc tính dữ liệu tuỳ chỉnh của HTML5. Trong data-downloadurl, bạn cần đưa vào loại MIME của tệp, tên tệp đích (tên mong muốn của tệp đã tải xuống) và URL tải xuống của tệp đó. Do đó, đoạn mã này được thêm vào mẫu HTML:

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

thao tác này sẽ tạo ra một kết quả như sau:

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

Dựa trên một plugin jQuery mà von Schorsch đã tạo (dựa trên bài viết của Seddon), tôi đã thêm một trình bổ trợ jQuery giúp phát hiện tính năng của trình duyệt. Nội dung được đánh dấu là những dòng mà tôi đã thêm vào phiên bản của 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);

Lý do tôi làm điều này là vì nếu không có phát hiện trình duyệt trước đó, việc thực hiện addEventListener() vào một phần tử HTML trong IE sẽ tạo ra lỗi JavaScript vì IE sử dụng phương thức composeEvent() riêng. e.dataTransfer chưa được xác định trong IE (kể từ bây giờ), e.dataTransfer.constructor trả về DataTransfer trong Firefox (Mozilla), trong khi trình duyệt Webkit (Chrome và Safari) triển khai hàm khởi tạo Bảng nhớ tạm. Trong Safari, e.dataTransfer.setData('DownloadURL','http://www.box.net') trả về giá trị false và Chrome trả về giá trị true cho câu lệnh này. Khi thực hiện tất cả các thử nghiệm nêu trên, tính năng này sẽ chỉ hoạt động với Chrome. Bạn có thể cho rằng tôi chỉ cần làm như sau:

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

Nhưng tôi thích phát hiện tính năng hơn là phát hiện trình duyệt, mặc dù về mặt kỹ thuật, tính năng này không phát hiện được rằng tính năng tải DnD xuống sẽ hoạt động.

Vấn đề của vòng lặp 1

1) Vì chúng tôi hiện đã bật DnD trên trang để di chuyển/sao chép tệp giữa các thư mục, nên chúng tôi cần có cách để phân biệt DnD Tải xuống và DnD trên trang. Về mặt kỹ thuật, chúng tôi không thể kết hợp hai hành động này. Chúng tôi không thể dự đoán xem người dùng muốn di chuyển một tệp sang một thư mục khác trong tài khoản Box.net hay kéo tệp đó vào máy tính của họ. Hai hành động này hoàn toàn khác nhau. Hơn nữa, không có cách nào dễ dàng để phát hiện nếu con trỏ nằm ngoài cửa sổ trình duyệt. Bạn có thể sử dụng window.onMouseout (IE) và document.onMouseout (các trình duyệt khác) để đính kèm sự kiện di chuột vào tài liệu và kiểm tra xem e.relatedTarget.nodeName == "HTML" (e là sự kiện chuột ra hoặc window.event, tuỳ theo sự kiện nào có sẵn). Nhưng việc này khá khó do sự kiện bong bóng. Sự kiện này có thể kích hoạt ngẫu nhiên khi bạn ở trên một hình ảnh hoặc lớp, đặc biệt là trong một ứng dụng web phức tạp như Box.net.

2) Chúng tôi muốn người dùng thực hiện một việc gì đó rõ ràng để ngăn họ kéo nhầm nội dung nào đó ra máy tính. Có khả năng trình chỉnh sửa của thư mục Box có thể tải tệp thực thi lên máy tính của người tải tệp đó xuống. Chúng tôi muốn người dùng biết chính xác thời điểm một tệp sẽ được tải xuống máy tính.

Lặp lại 2

Chúng tôi quyết định thử nghiệm bằng tổ hợp phím Control + kéo (kéo tệp khi nhấn phím Ctrl của Windows). Thao tác này nhất quán với những gì mọi người có thể thực hiện trên máy tính Windows để sao chép một tệp. Ứng dụng này cũng yêu cầu người dùng thực hiện thêm thao tác (nhưng không phải là thêm một bước) để ngăn việc tải tệp xuống do nhầm lẫn.

Trình bổ trợ jQuery trong vòng lặp 1 hiện đã bị loại bỏ vì chúng ta cần tích hợp chặt chẽ DnD Download với DnD trên trang. Đối với những người quan tâm, chúng tôi sử dụng phiên bản sửa đổi của trình bổ trợ có thể kéo trên giao diện người dùng jQuery. Bên trong sự kiện di chuột xuống của một phần tử mục tiêu, chúng ta đặt mã sau:

// 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;
}
}

Ngoài việc bật phím Ctrl, chúng tôi cũng thêm một chú giải công cụ nhỏ về thông báo ngắn, xuất hiện khi người dùng thực hiện thao tác kéo trên trang thông thường. Phương thức này cho người dùng biết rằng có thể tải tệp xuống nếu biểu tượng tệp được kéo vào màn hình trong khi phím Ctrl đang được giữ.

Vấn đề lặp lại 2

Do những vấn đề về bảo mật, Box.net không hiển thị các URL cố định để truy cập trực tiếp vào các tệp tĩnh. Đây không phải là điều duy nhất đối với Box.net. Bất kỳ dịch vụ lưu trữ trực tuyến nào cũng đều không nên hiển thị các URL cố định mà không có thêm một lớp bảo mật để kiểm tra xem tệp có công khai hay không và liệu tệp tải xuống có mục đích do người dùng có quyền phù hợp yêu cầu hay không.

Khi truy cập "URL tải xuống" (ví dụ: https://www.box.net/box_download_file?file_id=f_60466690) của một mục, mục này sẽ trả về mã trạng thái "302 Đã tìm thấy" và chuyển hướng đến một URL ngẫu nhiên (ví dụ: https://www.box.net/dl/6045?a=1f1207a084&m=168299,11211&t=2&b=aca15820d924e3b), là "URL thực tế" tạm thời của tệp. Thách thức đặt ra là mã này sẽ hết hạn sau mỗi vài phút và vì vậy, việc đặt mã này trong đầu ra HTML là không thực tế. Mã này có thể trả về "404" khi người dùng cố gắng tải tệp xuống tại đường liên kết trong đầu ra HTML được tạo vài phút trước.

Tính năng Tải DnD xuống chỉ hoạt động trên những URL thực trỏ trực tiếp đến một tài nguyên. Nếu có liên quan đến hoạt động chuyển hướng, thì việc theo dõi chuỗi hiện tại chưa đủ thông minh (và không nên đi theo chuỗi do tính bảo mật). Do đó, mặc dù đường liên kết https://www.box.net/box_download_file?file_id=f_60466690 ở trên sẽ cho phép bạn tải tệp xuống khi nhập vào thanh vị trí của trình duyệt, nhưng nó sẽ không hoạt động với DnD.

Để minh hoạ rõ hơn sự khác biệt giữa "URL thực tế" và "URL chuyển hướng", hãy xem ảnh chụp màn hình:

URL chuyển hướng 302
URL chuyển hướng 302
URL thực
URL thực tế

Lặp lại 3

Hãy thử dùng Ajax.

Chúng tôi đã sửa đổi một chút đoạn mã trong vòng lặp trước và có kết quả như sau:

// 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;
}
}

Điều này hợp lý. Khi bắt đầu kéo, hệ thống sẽ ngay lập tức thực hiện lệnh gọi Ajax đến máy chủ để truy xuất URL tải xuống mới nhất của tệp. Tuy nhiên, cách này không hiệu quả.

Hoá ra đó cần là một lệnh gọi đồng bộ (hoặc như tôi thường gọi là Sjax). Có vẻ như setData phải được thực hiện tại thời điểm trình nghe sự kiện được đính kèm. Theo API của jQuery, các dòng được đánh dấu sẽ trở thành:

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

Nó hoạt động tốt cho đến khi tôi rút kết nối mạng ra. Do thực hiện lệnh gọi đồng bộ, nên trình duyệt sẽ bị treo cho đến khi lệnh gọi thành công. Nếu lệnh gọi Ajax không thành công (404 hoặc nếu lệnh gọi không phản hồi), thì trình duyệt sẽ không rã đông như thể đã gặp sự cố.

Sẽ an toàn hơn nhiều nếu bạn làm những việc như sau:

$.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
});

Để xem bản minh hoạ tính năng này, vui lòng tải một tệp tĩnh lên tài khoản Box.net. Kéo biểu tượng tệp ra màn hình trong khi giữ phím Ctrl. Nếu bạn chưa có tài khoản, chỉ mất chưa đến 30 giây để tạo tài khoản.

Với tính năng này, bạn có thể thoả sức sáng tạo và làm được nhiều thứ có thể thực hiện được. Khi bạn kéo hình ảnh vào hộp thoại máy in của Windows, hình ảnh đó sẽ được in ngay lập tức. Bạn có thể sao chép bài hát từ Box vào ổ đĩa của điện thoại di động, kéo tệp từ Box vào ứng dụng khách nhắn tin nhanh để chuyển trực tiếp đến bạn bè... Điều này mở ra vô số khả năng giúp bạn tăng năng suất.

chuyển tệp vào máy in
Kéo một tệp vào máy in.
Kéo tệp vào ứng dụng nhắn tin nhanh
Kéo một tệp vào ứng dụng tức thì.

Ý tưởng và cải tiến trong tương lai

Điều này vẫn chưa lý tưởng vì một lệnh gọi đồng bộ có thể khoá trình duyệt trong một thời gian ngắn. Web Worker HTML 5 cũng không trợ giúp được gì vì Web Worker phải đồng bộ. Có vẻ như setData phải được thực hiện tại thời điểm trình nghe sự kiện được đính kèm.

Trong thực tế, hiệu suất khá chấp nhận được. Lệnh gọi Ajax đồng bộ (Sjax) chỉ truy xuất một chuỗi URL khá nhanh. Vấn đề này có mức hao tổn lớn trong tiêu đề HTTP mà WebSocket có thể giải quyết. Tuy nhiên, cho đến khi chúng ta thấy nhiều mức độ sử dụng loại công nghệ này hơn, bạn không nên sử dụng WebSockets để gửi mọi bản cập nhật nhỏ cho máy khách.

Tôi cũng hy vọng rằng tính năng tải nhiều tệp xuống sẽ được thêm vào API trong tương lai. Thật tuyệt vời khi được kết hợp với các hộp đánh dấu tuỳ chỉnh để chọn nhiều tệp trên giao diện người dùng. Hơn nữa, sẽ tuyệt vời hơn nếu bạn có thể tải các tệp do ứng dụng tạo, chẳng hạn như tệp văn bản được tạo từ kết quả của một biểu mẫu đã gửi, theo cách này.

  • Cột dnd
  • Sắp xếp lại danh sách
  • Tạo thư viện hình ảnh
  • Xuất hình ảnh canvas

Tài liệu tham khảo