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

Tính năng Kéo và thả (DnD) là một trong nhiều tính năng tuyệt vời của HTML 5 và đượ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 sang máy tính. Đây là một tính năng cực kỳ tiện lợi, nhưng không được nhiều người biết đến cho đến khi Ryan Seddon đăng một bài viết về những khám phá của anh về kỹ thuật đảo ngược trên tính năng mới này.

Tại Box.net, chúng tôi rất vui mừng khi các tính 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 rất vui được 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 sang máy tính để tải xuống và lưu tệp.

Tôi muốn chia sẻ cách tôi đã trải qua một số 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 tính năng hỗ trợ API Kéo và thả

Việc đầu tiên cần làm là kiểm tra để đảm bảo trình duyệt của bạn hỗ trợ đầy đủ tính năng kéo và thả HTML5. Một cách dễ dàng để làm điều đó là 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

Trước 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" để liên kết các tệp. Quy trình này sử dụng thuộc tính dữ liệu tuỳ chỉnh của HTML5. Trong data-downloadurl, bạn cần thêm loại MIME của tệp, tên tệp đích (tên tệp mong muốn của tệp đã tải xuống) và URL tải xuống của tệp. Do đó, nội dung này được thêm vào mẫu HTML:

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

sẽ tạo ra 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 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 để phát hiện một số tính năng của trình duyệt. Các dòng đượ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 việc này là vì nếu không phát hiện trước trình duyệt, việc thực hiện addEventListener() cho một phần tử HTML trong IE sẽ tạo ra lỗi JavaScript vì IE sử dụng phương thức attachEvent() riêng. e.dataTransfer chưa được xác định trong IE (tính đến thời điểm hiện tại), 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 Clipboard. 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. Sau khi thực hiện tất cả các kiểm thử nêu trên, tính năng này chỉ hoạt động trên Chrome. Bạn có thể cho rằng tôi chỉ cần làm như sau:

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

Tuy nhiên, tôi thích tính năng 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 việc tải xuống DnD sẽ hoạt động.

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

1) Vì hiện tại chúng ta đã bật tính năng DnD trên trang để di chuyển/sao chép tệp giữa các thư mục, nên chúng ta cần có cách để phân biệt tính năng Tải xuống bằng DnD và DnD trên trang. Về mặt kỹ thuật, chúng ta không thể kết hợp hai thao tác này. Chúng tôi không thể dự đoán liệu 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àn hình nền. Hai thao tác 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 liệu con trỏ có nằm bên ngoài cửa sổ trình duyệt hay không. 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 mouseout vào tài liệu và kiểm tra xem e.relatedTarget.nodeName == "HTML" (e là sự kiện mouseout hoặc window.event, tuỳ theo sự kiện nào có sẵn) hay không. Tuy nhiên, việc này khá khó khăn do sự kiện nổi. Sự kiện có thể kích hoạt ngẫu nhiên khi bạn di chuột qua 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 ta muốn người dùng làm rõ một việc gì đó để ngăn họ kéo nội dung ra máy tính để bàn do nhầm lẫn. Có thể người chỉnh sửa thư mục Box có thể tải một tệp thực thi lên, tệp này sẽ thực hiện một hành động không mong muốn trên máy tính của bất kỳ ai tải tệp đó xuống. Chúng ta muốn người dùng biết chính xác thời điểm tệp được tải xuống máy tính.

Lặp lại 2

Chúng tôi quyết định thử nghiệm với tổ hợp phím Ctrl + kéo (kéo tệp khi nhấn phím Ctrl trên Windows). Thao tác này nhất quán với những việc mọi người có thể làm trên máy tính Windows để sao chép tệp. Phương thức này cũng yêu cầu người dùng phải làm thêm việc (nhưng không phải thêm 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ị bỏ vì chúng ta cần tích hợp chặt chẽ tính năng Tải xuống DnD 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 của giao diện người dùng jQuery. Bên trong sự kiện mousedown 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 ta cũng thêm một chú giải công cụ nhỏ, xuất hiện khi người dùng thực hiện thao tác kéo thông thường trên trang. Thông báo này cho người dùng biết rằng họ có thể tải tệp xuống nếu kéo biểu tượng tệp vào màn hình nền trong khi giữ phím Ctrl.

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

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

Khi truy cập vào "URL tải xuống" (ví dụ: https://www.box.net/box_download_file?file_id=f_60466690) của một mục, URL này sẽ trả về mã trạng thái "302 Found" (Đã 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. Vấn đề là mã này hết hạn vài phút một lần, vì vậy, việc đặt mã này vào đầu ra HTML là không thực tế. Lỗi 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 kết quả HTML được tạo vài phút trước.

Tính năng Tải xuống bằng DnD chỉ hoạt động trên các URL thực tế trỏ trực tiếp đến một tài nguyên. Nếu có liên quan đến lệnh chuyển hướng, thì hiện tại, lệnh này chưa đủ thông minh để tuân theo chuỗi (và không bao giờ được tuân theo chuỗi vì lý do 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 cho phép bạn tải tệp xuống khi nhập tệp đó vào thanh địa chỉ của trình duyệt, nhưng đường liên kết này sẽ không hoạt động với tính năng DnD.

Để minh hoạ rõ hơn sự khác biệt giữa "URL thực" 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ử Ajax.

Chúng ta đã sửa đổi một chút mã trong lần lặp lại trước và có được kết quả 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 rất hợp lý. Khi bắt đầu kéo, thao tác này 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 hoạt động.

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

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

Và nó hoạt động tốt cho đến khi tôi rút phích cắm kết nối mạng. Vì 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 không phản hồi), trình duyệt sẽ không làm tan băng như thể trình duyệt đã gặp sự cố.

Bạn nên làm như sau để đảm bảo an toàn hơn:

$.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 nền trong khi giữ phím Ctrl. Nếu bạn chưa có tài khoản, bạ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ể sáng tạo và làm được nhiều việc. Khi kéo một hình ảnh vào hộp thoại máy in Windows, hình ảnh đó sẽ được in ngay lập tức. Bạn có thể sao chép một bài hát từ Box vào ổ đĩa của điện thoại di động, kéo một tệp từ Box vào ứng dụng nhắn tin tức thì để chuyển tệp đó trực tiếp cho bạn bè… Điều này mở ra vô vàn khả năng để tăng năng suất của bạn.

kéo tệp đến máy in
Kéo 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 nhắn tin nhanh.

Ý kiến và những điểm cải tiến trong tương lai

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

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 và sẽ khá nhanh. Phương thức này có chi phí hao tổn lớn trong tiêu đề HTTP, điều này có thể được giải quyết bằng WebSocket. Tuy nhiên, cho đến khi chúng ta thấy việc sử dụng nhiều hơn loại công nghệ này, bạn không nên sử dụng WebSockets để gửi mọi nội dung cập nhật nhỏ đến ứng dụng.

Tôi cũng hy vọng rằng khả năng tải nhiều tệp xuống sẽ được thêm vào API trong tương lai. Khi 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, điều này sẽ rất tuyệt vời. Hơn nữa, sẽ càng tuyệt vời hơn nếu bạn có thể tải các tệp do ứng dụng tạo xuống theo cách này, 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.

  • Kéo và thả cột
  • 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