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

Введение

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

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

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

Проверьте поддержку API перетаскивания

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

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

Итерация 2

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

Плагин jQuery в итерации 1 сейчас заброшен, потому что нам нужно тесно интегрировать загрузку DnD со страницей 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-адресом перенаправления», см. снимки экрана:

302 URL-адрес перенаправления
302 URL-адрес перенаправления
Фактический 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;
}
}

Это имеет смысл. При перетаскивании он немедленно отправляет 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-клиент, чтобы передать его прямо другу… Это открывает безграничные возможности для повышения вашей производительности.

перекинуть файл на принтер
Перетаскивание файла на принтер.
Перетаскивание файла в IM-клиент
Перетаскивание файла в IM-клиент.

Мысли и будущие улучшения

Это все еще далеко не идеально, поскольку синхронный вызов может на короткое время заблокировать браузер. Веб-воркер HTML 5 также не помогает, поскольку веб-воркер должен быть асинхронным. Похоже, что setData необходимо выполнить в момент подключения прослушивателя событий.

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

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

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

Ссылки