Практический пример — загрузка с помощью перетаскивания в Chrome

Дэвид Тонг
David Tong

Введение

Drag and Drop (DnD) — одна из многих замечательных функций HTML 5, которая поддерживается в Firefox 3.5, Safari, Chrome и IE. Недавно Google выпустила новую функцию , которая позволяет пользователям Google Chrome перетаскивать файлы из браузера на рабочий стол. Это чрезвычайно удобная функция, но она не была широко известна, пока Райан Седдон не опубликовал статью об открытиях своей обратной разработки этой новой функции.

В Box.net мы очень рады тому, как эти новые возможности позволяют нам улучшить наше решение по управлению облачным контентом, а также внести больший вклад в сообщество разработчиков. Я рад сообщить, что DnD Download был интегрирован в наш продукт. Теперь пользователи Box могут перетаскивать файлы напрямую из браузера Chrome на свой рабочий стол, чтобы загрузить и сохранить файл.

Я хотел бы рассказать, как я прошел несколько итераций в процессе разработки этой новой функции.

Проверьте поддержку Drag and Drop API

Первое, что нужно сделать, это проверить, что ваш браузер полностью поддерживает HTML5 drag and drop. Простой способ сделать это — использовать библиотеку Modernizr для проверки определенной функции:

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

Итерация 1

Сначала я попробовал подход, который Седдон нашел в Gmail. Я добавил новый атрибут под названием 'data-downloadurl' для привязки ссылок файлов. Этот процесс использует пользовательские атрибуты данных HTML5. В data-downloadurl вам нужно включить MIME-тип файла, имя файла назначения (желаемое имя файла для загружаемого файла) и URL загрузки файла. Таким образом, это добавляется в шаблон 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, созданного фон Шоршем, который основан на статье Седдона, я добавил плагин jQuery, который выполняет небольшое обнаружение функций браузера. Выделены строки, которые я добавил в версию фон Шорша:

(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);

Причина, по которой я это сделал, заключается в том, что без предварительного обнаружения браузера выполнение addEventListener() к элементу HTML в IE приведет к ошибке JavaScript, поскольку IE использует собственный метод attachEvent(). e.dataTransfer не определен в IE (на данный момент), e.dataTransfer.constructor возвращает DataTransfer в Firefox (Mozilla), в то время как браузеры 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 Download и DnD на странице. Технически мы не можем объединить эти два действия. Мы не можем предсказать, захочет ли пользователь переместить файл в другую папку в учетной записи Box.net или перетащить его на свой рабочий стол. Эти два действия совершенно разные. Более того, нет простого способа определить, находится ли курсор за пределами окна браузера. Вы можете использовать window.onmouseout (IE) и document.onmouseout (другие браузеры), чтобы прикрепить событие mouseout к документу и проверить, e.relatedTarget.nodeName == "HTML" (e — это событие mouseout или window.event, в зависимости от того, что доступно). Но это довольно сложно из-за всплытия событий. Событие может срабатывать случайным образом, когда вы находитесь над изображением или слоем, особенно в таком сложном веб-приложении, как Box.net.

2) Мы хотим, чтобы пользователь явно сделал что-то, чтобы предотвратить перетаскивание чего-либо на рабочий стол по ошибке. Потенциально редактор папки Box может загрузить исполняемый файл, который делает что-то нежелательное на компьютере того, кто его скачает. Мы хотим, чтобы пользователь точно знал, когда файл будет загружен на рабочий стол.

Итерация 2

Мы решили поэкспериментировать с control + drag (перетаскивание файла при нажатой клавише Windows Ctrl). Это действие соответствует тому, что люди могут делать на рабочем столе Windows, чтобы дублировать файл. Оно также требует дополнительной работы (но не дополнительного шага) от пользователя, чтобы предотвратить загрузку файлов по ошибке.

Плагин jQuery в итерации 1 теперь заброшен, потому что нам нужно тесно интегрировать DnD Download с DnD на странице. Для тех, кому интересно, мы используем модифицированную версию плагина Draggable jQuery UI. Внутри события 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 не раскрывает постоянные URL-адреса для прямого доступа к статическим файлам. Это не уникально для Box.net. Любой онлайн-сервис хранения не должен раскрывать постоянные URL-адреса без дополнительного уровня безопасности для проверки того, является ли файл общедоступным и запрашивается ли предполагаемая загрузка пользователем с соответствующими разрешениями.

При переходе по "URL-адресу загрузки" (например, https://www.box.net/box_download_file?file_id=f_60466690 ) элемента возвращается код статуса "302 Found" и происходит перенаправление на случайный URL-адрес (например, https://www.box.net/dl/6045?a=1f1207a084&m=168299,11211&t=2&b=aca15820d924e3b ), который является временным "фактическим URL-адресом" файла. Проблема в том, что он истекает каждые несколько минут, и поэтому размещение его в HTML-выводе непрактично. Он может вернуть "404", когда пользователь попытается загрузить файл по ссылке в HTML-выводе, сгенерированном несколько минут назад.

DnD Download работает только с реальными URL-адресами, которые указывают напрямую на ресурс. Если задействовано перенаправление, в настоящее время оно недостаточно умно, чтобы следовать цепочке (и оно никогда не должно следовать цепочке из-за безопасности). Поэтому, хотя ссылка https://www.box.net/box_download_file?file_id=f_60466690 выше позволит вам загрузить файл, если вы введете его в адресную строку браузера, она не будет работать с DnD.

Чтобы лучше проиллюстрировать разницу между «фактическим URL» и «URL-адресом перенаправления», посмотрите на скриншоты:

URL-адрес перенаправления 302
URL-адрес перенаправления 302
Фактический URL-адрес
Фактический URL-адрес

Итерация 3

Давайте попробуем «Аякс».

Мы немного изменили код в предыдущей итерации и получили следующее:

// 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-вызов на сервер, чтобы получить последний URL загрузки файла. Однако это не работает.

Оказывается, это должен быть синхронный вызов (или, как я люблю его называть, Sjax). Похоже, setData должен быть выполнен в то время, когда подключается прослушиватель событий. Согласно API jQuery, выделенные строки становятся:

$.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 тоже не помогает, так как Web Worker должен быть асинхронным. Похоже, setData должен быть выполнен в то время, когда присоединяется прослушиватель событий.

На самом деле производительность вполне приемлемая. Синхронный вызов Ajax (Sjax) просто извлекает строку URL, что должно быть довольно быстро. Он действительно имеет большие накладные расходы в заголовке HTTP, которые, возможно, можно решить с помощью WebSockets. Однако, пока мы не увидим большего использования такого рода технологий, не стоит использовать WebSockets для отправки каждого небольшого обновления клиенту.

Я также надеюсь, что в будущем в API будет добавлена ​​возможность загрузки нескольких файлов. В сочетании с пользовательскими флажками для выбора нескольких файлов в пользовательском интерфейсе это было бы невероятно. Кроме того, было бы еще лучше, если бы файлы, сгенерированные клиентом, например текстовые файлы, сгенерированные из результата отправки формы, можно было загружать таким образом.

  • Столбец dnd
  • Переставить список
  • Создание галереи изображений
  • Экспорт изображения холста

Ссылки