拖放文件上传

Avatar of Osvaldas Valutis
Osvaldas Valutis 发布

DigitalOcean 为您旅程的每个阶段提供云产品。 立即开始使用 200 美元的免费信用额度!

我正在开发一个名为 Readerrr 的 RSS 阅读器应用程序(编辑注:链接已删除,因为网站似乎已失效)。 我希望通过允许拖放文件上传以及传统的文件输入来丰富 Feed 导入体验。 有时候,拖放是选择文件更便捷的方式,不是吗?

查看演示

标记

此标记与拖放本身无关。 它只是一个正常且功能齐全的 <form>,尽管包含了一些用于潜在状态的额外 HTML 元素。

<form class="box" method="post" action="" enctype="multipart/form-data">
  <div class="box__input">
    <input class="box__file" type="file" name="files[]" id="file" data-multiple-caption="{count} files selected" multiple />
    <label for="file"><strong>Choose a file</strong><span class="box__dragndrop"> or drag it here</span>.</label>
    <button class="box__button" type="submit">Upload</button>
  </div>
  <div class="box__uploading">Uploading…</div>
  <div class="box__success">Done!</div>
  <div class="box__error">Error! <span></span>.</div>
</form>

在需要之前,我们将隐藏这些状态。

.box__dragndrop,
.box__uploading,
.box__success,
.box__error {
  display: none;
}

一些解释

  • 关于状态:.box__uploading 元素将在文件上传的 Ajax 过程中可见(其他元素仍将隐藏)。 然后,根据发生的情况显示 .box__success.box__error
  • input[type="file"]label 是表单的功能部分。 我在关于自定义文件输入的文章中介绍了如何将它们一起设置样式。 在那篇文章中,我还描述了 [data-multiple-caption] 属性的用途。 输入和标签也作为以标准方式选择文件的替代方案(或者如果拖放不受支持,则为唯一方式)。
  • 如果浏览器支持拖放文件上传功能,则会显示 .box__dragndrop

特性检测

我们不能 100% 依赖浏览器支持拖放。 我们应该提供一个回退方案。 因此:特性检测。 拖放文件上传依赖于许多不同的 JavaScript API,因此我们需要检查所有这些 API。

首先是拖放事件本身。 Modernizr 是一个您可以信赖的关于特性检测的库。 此测试来自那里。

var div = document.createElement('div');
return ('draggable' in div) || ('ondragstart' in div && 'ondrop' in div)

接下来,我们需要检查 FormData 接口,该接口用于形成所选文件(s)的程序化对象,以便可以通过 Ajax 将它们发送到服务器。

return 'FormData' in window;

最后,我们需要 DataTransfer 对象。 这个有点棘手,因为在用户第一次与拖放界面交互之前,没有万无一失的方法来检测对象的可访问性。 并非所有浏览器都公开此对象。

理想情况下,我们希望避免类似以下的 UX...

  • “将文件拖放到此处!”
  • [用户拖放文件]
  • “哦,抱歉,拖放不受支持。”

这里的技巧是在文档加载时立即检查 FileReader API 的可用性。 这背后的想法是,支持 FileReader 的浏览器也支持 DataTransfer

'FileReader' in window

将以上代码组合到自执行匿名函数中...

var isAdvancedUpload = function() {
  var div = document.createElement('div');
  return (('draggable' in div) || ('ondragstart' in div && 'ondrop' in div)) && 'FormData' in window && 'FileReader' in window;
}();

...将使我们能够进行有效的特性支持检测。

if (isAdvancedUpload) {
  // ...
}

通过此有效的特性检测,现在我们可以让用户知道他们可以将文件拖放到我们的表单中(或者不能)。 在支持的情况下,我们可以通过向其添加一个类来设置表单的样式。

var $form = $('.box');

if (isAdvancedUpload) {
  $form.addClass('has-advanced-upload');
}
.box.has-advanced-upload {
  background-color: white;
  outline: 2px dashed black;
  outline-offset: -10px;
}
.box.has-advanced-upload .box__dragndrop {
  display: inline;
}

如果拖放文件上传不受支持,则完全没有问题。 用户将能够通过传统的 input[type="file"] 上传文件!

关于浏览器支持的说明: Microsoft Edge 存在一个阻止拖放工作的错误。 听起来他们已经意识到了这个问题,并希望修复它。(更新:链接到错误已删除,因为链接已停止工作。现在 Edge 是 Chromium,大概不再是问题了。)

拖放

我们开始了,这里才是重点。

这部分处理在不同状态下(例如用户将文件拖到表单上时)向表单添加和删除类。 然后,在文件被放置时捕获这些文件。

if (isAdvancedUpload) {

  var droppedFiles = false;

  $form.on('drag dragstart dragend dragover dragenter dragleave drop', function(e) {
    e.preventDefault();
    e.stopPropagation();
  })
  .on('dragover dragenter', function() {
    $form.addClass('is-dragover');
  })
  .on('dragleave dragend drop', function() {
    $form.removeClass('is-dragover');
  })
  .on('drop', function(e) {
    droppedFiles = e.originalEvent.dataTransfer.files;
  });

}
  • e.preventDefault()e.stopPropagation() 可防止跨浏览器为分配的事件出现任何意外行为。
  • e.originalEvent.dataTransfer.files 返回已放置的文件列表。 很快您将看到如何使用数据将这些文件发送到服务器。

根据需要添加和删除 .is-dragover 使我们能够直观地指示用户何时可以安全地放置文件。

.box.is-dragover {
  background-color: grey;
}

以传统方式选择文件

有时,拖放文件并不是选择要上传文件非常便捷的方式。 特别是当用户在小型屏幕的电脑前时。 因此,最好让用户选择他们喜欢的上传方式。 文件输入和标签用于实现这一点。 我描述过的样式设置方式使我们能够保持 UI 的一致性。

Ajax 上传

没有跨浏览器的方法可以在没有 Ajax 的情况下上传拖放的文件。 一些浏览器(IE 和 Firefox)不允许设置文件输入的值,然后才能以通常的方式将其提交到服务器。

不起作用

$form.find('input[type="file"]').prop('files', droppedFiles);

相反,当表单提交时,我们将使用 Ajax。

$form.on('submit', function(e) {
  if ($form.hasClass('is-uploading')) return false;

  $form.addClass('is-uploading').removeClass('is-error');

  if (isAdvancedUpload) {
    // ajax for modern browsers
  } else {
    // ajax for legacy browsers
  }
});

.is-uploading 类具有双重作用:它可以防止表单重复提交(return false),并帮助指示用户提交正在进行中。

.box.is-uploading .box__input {
  visibility: none;
}
.box.is-uploading .box__uploading {
  display: block;
}

现代浏览器的 Ajax

如果这是一个没有文件上传的表单,我们就不需要两种不同的 Ajax 技术。 不幸的是,IE 9 及以下版本不支持通过 XMLHttpRequest 上传文件。

为了区分哪种 Ajax 方法有效,我们可以使用现有的 isAdvancedUpload 测试,因为支持我之前编写的内容的浏览器也支持通过XMLHttpRequest 上传文件。 这是在 IE 10+ 上有效的代码。

if (isAdvancedUpload) {
  e.preventDefault();

  var ajaxData = new FormData($form.get(0));

  if (droppedFiles) {
    $.each( droppedFiles, function(i, file) {
      ajaxData.append( $input.attr('name'), file );
    });
  }

  $.ajax({
    url: $form.attr('action'),
    type: $form.attr('method'),
    data: ajaxData,
    dataType: 'json',
    cache: false,
    contentType: false,
    processData: false,
    complete: function() {
      $form.removeClass('is-uploading');
    },
    success: function(data) {
      $form.addClass( data.success == true ? 'is-success' : 'is-error' );
      if (!data.success) $errorMsg.text(data.error);
    },
    error: function() {
      // Log the error, show an alert, whatever works for you
    }
  });
}
  • FormData($form.get(0)) 收集来自所有表单输入的数据。
  • $.each() 循环遍历拖放的文件。 ajaxData.append() 将它们添加到数据堆栈中,该堆栈将通过 Ajax 提交。
  • data.successdata.error 是服务器将返回的 JSON 格式的响应。 以下是 PHP 中的示例。
<?php
  // ...
  die(json_encode([ 'success'=> $is_success, 'error'=> $error_msg]));
?>

旧版浏览器的 Ajax

这主要针对 IE 9 及以下版本。 我们不需要收集拖放的文件,因为在这种情况下(isAdvancedUpload = false),浏览器不支持拖放文件上传,并且表单仅依赖于 input[type="file"]

奇怪的是,将表单定位到动态插入的iframe 上可以解决问题。

if (isAdvancedUpload) {
  // ...
} else {
  var iframeName  = 'uploadiframe' + new Date().getTime();
    $iframe   = $('<iframe name="' + iframeName + '" style="display: none;"></iframe>');

  $('body').append($iframe);
  $form.attr('target', iframeName);

  $iframe.one('load', function() {
    var data = JSON.parse($iframe.contents().find('body' ).text());
    $form
      .removeClass('is-uploading')
      .addClass(data.success == true ? 'is-success' : 'is-error')
      .removeAttr('target');
    if (!data.success) $errorMsg.text(data.error);
    $form.removeAttr('target');
    $iframe.remove();
  });
}

自动提交

如果您有一个仅包含拖放区域或文件输入的简单表单,则避免要求用户按下按钮可能会方便用户。 相反,您可以在文件放置/选择时自动提交表单,方法是触发 submit 事件。

// ...

.on('drop', function(e) { // when drag & drop is supported
  droppedFiles = e.originalEvent.dataTransfer.files;
  $form.trigger('submit');
});

// ...

$input.on('change', function(e) { // when drag & drop is NOT supported
  $form.trigger('submit');
});

如果拖放区域设计得很好(用户很清楚该怎么做),您可能会考虑隐藏提交按钮(更少的 UI 可能会更好)。 但是,在隐藏此类控件时要小心。 如果由于某种原因 JavaScript 不可用,则按钮应该可见且可操作(渐进增强!)。 向 添加 .no-js 类名并在 JavaScript 中将其移除即可解决此问题。

<html class="no-js">
  <head>
    <!-- remove this if you use Modernizr -->
    <script>(function(e,t,n){var r=e.querySelectorAll("html")[0];r.className=r.className.replace(/(^|\s)no-js(\s|$)/,"$1js$2")})(document,window,0);</script>
  </head>
</html>
.box__button {
  display: none;
}
.no-js .box__button {
  display: block;
}

显示选定的文件

如果不进行自动提交,则应向用户指示他们是否已成功选择文件。

var $input    = $form.find('input[type="file"]'),
    $label    = $form.find('label'),
    showFiles = function(files) {
      $label.text(files.length > 1 ? ($input.attr('data-multiple-caption') || '').replace( '{count}', files.length ) : files[ 0 ].name);
    };

// ...

.on('drop', function(e) {
  droppedFiles = e.originalEvent.dataTransfer.files; // the files that were dropped
  showFiles( droppedFiles );
});

//...

$input.on('change', function(e) {
  showFiles(e.target.files);
});

当 JavaScript 不可用时

渐进增强是关于用户无论如何都应该能够完成网站上的主要任务的想法。文件上传也不例外。如果由于某种原因 JavaScript 不可用,则界面将如下所示

页面将在表单提交时刷新。我们用于指示提交结果的 JavaScript 无用。这意味着我们必须依靠服务器端解决方案。以下是它在演示页面中的外观和工作方式

<?php

  $upload_success = null;
  $upload_error = '';

  if (!empty($_FILES['files'])) {
    /*
      the code for file upload;
      $upload_success – becomes "true" or "false" if upload was unsuccessful;
      $upload_error – an error message of if upload was unsuccessful;
    */
  }

?>

以及对标记的一些调整

<form class="box" method="post" action="" enctype="multipart/form-data">

  <?php if ($upload_success === null): ?>

  <div class="box__input">
    <!-- ... -->
  </div>

  <?php endif; ?>

  <!-- ... -->

  <div class="box__success"<?php if( $upload_success === true ): ?> style="display: block;"<?php endif; ?>>Done!</div>
  <div class="box__error"<?php if( $upload_success === false ): ?> style="display: block;"<?php endif; ?>>Error! <span><?=$upload_error?></span>.</div>

</form>

就是这样!这篇文章本来可以更长,但我想这将帮助您在自己的项目中使用负责任的拖放文件上传功能。

查看演示以了解更多信息(查看源代码以查看不依赖 jQuery 的 JavaScript)

查看演示