利用File,Drop&Drag,XHR2实现图片拖拽上传

一直想作下图片上传的功能,今天终于把这个心愿了了,在制做的过程当中也顺便把HTML5的File,Drop,Drag,URL,FileReader复习了下,多赢php

image

查看demohtml

查看源码,欢迎starreact

咱们先来回顾下文件上传有几种方式git

form表单

这个是HTML5出来以前广泛的文件上传方式,它是经过页面的form表单进行上传的,代码以下github

<form action='upload.php' enctype='multipart/form-data' method='POST' name='form' id='form'>
    <input type='file' name='image' multiple accept='image/png'/>
    <button id='upload' type='submit'>上传</button>
</form>
复制代码

注意下当中的几个属性:web

  • action:接受请求的URL
  • enctype:请求的编码类型,默认是application/x-www-form-urlencoded,文件上传时设置为multipart/form-data
  • method:请求的方法,文件上传时设置为POST
  • multiple:可让咱们一次选择多个文件
  • accept:设置上传的文件类型

另外将咱们的input标签的type设置为file,点击以后就能够打开系统的文件管理器,单击上传按钮就能够把咱们选择的文件发送到服务器了ajax

FormData & XHR2

除了使用form表单来提交数据外,咱们还能够本身构建表单数据进行提交,其中FormData用来建立表单数据,是属于HTML5的东西,XHR2用来发送请求到服务器json

FormData对象API:跨域

  • append
  • delete
  • set
  • get
  • getAll
  • has
  • keys
  • entries
  • values
  • forEach

我在demo中是这样用的数组

const formData = new FormData()
files.forEach((file, index) => {
  formData.append(`img${index+1}`, file)
})
复制代码

XHR2是用来发送请求的,ajax的实现就是靠它,正是由于XHR2的出现才使得经过ajax上传文件变成可能,XHR2相对于XHR有如下特色:

  • 能够设置timeout
  • 可使用FormData对象管理数据
  • 能够上传二进制文件
  • 能够跨域
  • 能够获取数据传输的进度信息

关于XHR2的使用,下面给出一个demo,更详细的用法你们能够去MDN,传送门

const formData = new FormData()
const xhr = new XMLHttpRequest()
xhr.timeout = 3000
xhr.open('POST', 'upload')
xhr.upload.onprogress = event => {
  if (event.lengthComputable) {
    const percent = event.loaded / event.total
    console.log(percent)
  }
}
xhr.onload = () => {
  if (xhr.status === 200 && xhr.readyState === 4) {
    alert('文件上传成功')
  } else {
    alert('文件上传失败')
  }
}
xhr.send(formData)
复制代码

Fetch

终于说到Fetch了,Fetch是一种新的HTTP请求方式,替代了以前的XHR2,也是我我的比较喜欢的一种,由于它配合Promise,Async/Await写起代码来简直不要太爽了,关于它的使用你们可MDN

var form = new FormData(),
    url = 'http://.......', //服务器上传地址
    file = files[0];
form.append('file', file);
 
fetch(url, {
    method: 'POST',
    body: form
}).then(function(response) {
    if (response.status >= 200 && response.status < 300) {
        return response;
    } 
    else {
        var error = new Error(response.statusText);
        error.response = response;
        throw error;
    }
}).then(function(resp) {
    return resp.json();
}).then(function(respData) {
    console.log('文件上传成功', respData);
}).catch(function(e) {
    console.log('文件上传失败', e);
});

复制代码

回到正题,咱们先来分析下我所作的DEMO有几个核心功能,而后针对每一个功能去具体讲解如何实现的,整个demo是基于react写的

我把功能划分了一下:

  • 选择文件上传
  • 图片缩略图
  • 图片删除
  • 拖拽文件上传
  • 拖拽文件删除
  • 图片预览

选择文件上传

个人思路是这样子的:点击一个input,弹出一个文件选择器,咱们能够选取多张图片,选择完成后,会触发input标签的change事件,咱们能够从input的元素files属性里拿到咱们选择的图片数据,而后把它添加到一个全局的数组里面

这里注意一点就是:选择的图片数据保存在input的files属性里面,files属性的值是一个相似数组的FileList对象,所以咱们不能直接使用Array的实例方法

核心代码以下:

handleChange = event => {
    event.preventDefault()
    const { files } = this.state
    Array.prototype.forEach.call(this.fileInput.files, file => {
        const src = URL.createObjectURL(file)
        file.src = src
        this.setState({
            files: [...files, file]
        })
    this.fileInput.value = ''
    })
}
复制代码

其中我经过Array.prototype.forEach.call来调用数组的forEach方法,代码最后有一行this.fileInput.value = '' 是为了解决不能上传同一张图片,由于input内部会去检查咱们上传的文件是否和上一次同样,若是同样是不会触发onchange事件的

图片缩略图

如何实现上传一张图片以后把它显示出来呢?这里我查阅了相关资料,一种是经过FileReader,另一种是经过URL,我们分别来说解下

FileReader

什么是FileReader,我这里引用MDN中的一段话

The FileReader object lets web applications asynchronously read the contents of files (or raw data buffers) stored on the user's computer, using File or Blob objects to specify the file or data to read.

File objects may be obtained from a FileList object returned as a result of a user selecting files using the element, from a drag and drop operation's DataTransfer object, or from the mozGetAsFile() API on an HTMLCanvasElement.

大体意思就是FileReader能够异步读取电脑上的文件内容,咱们可使用File或者Blob对象来指定读取的文件

File对象能够来自input元素选择文件后返回的FileList对象,也能够来自使用Drag和Drop操做后的DataTransfer对象,或者是使用HTMLCanvasElement调用mozGetAsFile()返回的结果

关于FileReader的属性,函数,事件这里列举下

  • 属性
    • error: 表示读取文件时发生的错误,只读
    • readState: 0-表示尚未加载数据,1-正在加载数据,2-已完成所有的读取请求,只读
    • result: 文件的内容,只有在文件读取完成以后才有值,数据的格式取决于调用的方法
  • 函数
    • abort: 中断读取操做,返回时readyState变为2
    • readAsArrayBuffer: 读取文件内容,返回格式ArrayBuffer
    • readAsBinaryString: 读取文件内容,返回格式二进制
    • readAsDataURL: 读取文件内容,返回格式data:URL
    • readAsText: 读取文件内容,返回格式字符串
  • 事件
    • onabort: 读取被中断时触发
    • onerror: 读取发生错误时触发
    • onload: 读取完成时触发
    • onloadstart: 读取开始时触发
    • onloadend: 读取结束时触发,成功或者失败
    • onprogress: 读取进度改变时触发

介绍完了FileReader的API以后,咱们想一下如何实现文件上传后显示图片的缩略图

思路其实也特别简单,就是文件上传以后,咱们获取到上传的文件对象,而后建立一个FileReader去读取咱们上传的文件,读取成功以后咱们的文件内容会保存在FileReader中的result中,而后咱们建立一个img元素去显示咱们读取的文件内容就能够了

核心代码以下:

handleChange = event => {
    event.preventDefault()
    const { files } = this.state
    Array.prototype.forEach.call(this.fileInput.files, file => {
      const reader = new FileReader()
      reader.readAsDataURL(file)
      reader.onload = event => {
        file.src = reader.result
        this.setState({
          files: [...files, file]
        })
        this.fileInput.value = ''
      }
    })
  }
复制代码

其中由于img元素是能够直接显示base64编码的图片的,因此咱们在读取文件的时候调用的是readAsDataURL,文件读取成功后,fileReader中的result的值就是data:URL格式的文件内容,咱们能够直接将它赋值给img元素的src属性,文件读取成功会触发onload事件,因此咱们的操做都必须写在其回调函数里面

URL

使用了FileReader来读取文件不知道你有没疑问,个人文件就在我本地,我什么还要读取它,转成一个base64那么长的字符串,不能直接提供一个地址给img元素的src属性吗?答案是能够的,借住URL对象咱们能够实现,这也是我推荐用的方式

先来了解下URL对象,MDN上是这样介绍它的

The URL interface represents an object providing static methods used for creating object URLs.

When using a user agent where no constructor has been implemented yet, it is possible to access such an object using the Window.URL properties (prefixed with Webkit-based browser as Window.webkitURL).

URL接口用来建立URL对象,该接口提供了一些静态方法

当咱们的环境没有实现URL的构造函数时,咱们能够经过Window.URL(Window.webkitURL)来返回一个URL实例

说白了这个URL其实就是一个工具类,用来处理咱们的url的,如获取host,pathname,hash等参数,咱们用到的倒不是这个,咱们这里用到的是URL中的两个静态方法createObjectURL和revokeObjectURL

createObjectURL传入一个File或者Blob对象,返回一个DOMString,这个字符串能够用来展现咱们的内容

revokeObjectURL用来销毁经过createObjectURL建立的DOMString

具体该怎么作呢?直接看代码

handleChange = event => {
    event.preventDefault()
    const { files } = this.state
    Array.prototype.forEach.call(this.fileInput.files, file => {
        const src = URL.createObjectURL(file)
        file.src = src
        this.setState({
            files: [...files, file]
        })
        this.fileInput.value = ''
    })
  }
复制代码

代码就一句话:const src = URL.createObjectURL(file),返回的src直接能够赋值给img的src属性,给FileReader不知道方便了多少

createObjectURL返回的字符串长这个样子的:blob:http://localhost:3000/81e8eaa9-3041-4c93-bd16-913f578ece42

关于URL其它的一些属性和方法在这次demo中暂时用不到就不列举出来了,感兴趣的同窗能够取MDN了解下,传送门

图片删除

图片删除这个功能就很简单了,点击图片上方的删除按钮,传入对应的index到删除方法,而后根据index在全局的files对象中找到对应的file过滤掉,返回一个新的数组

核心代码以下:

handleDelete = event => {
    event.preventDefault()
    event.stopPropagation()
    const { target: { dataset: { index } } } = event
    const { files } = this.state
    
    const newFiles = files.filter((file, index2) => {
      if (index2 === +index) {
        URL.revokeObjectURL(file.src)
        return false
      }
      return true
    })
    this.setState({
      files: newFiles
    })
}
复制代码

这里记住如下删除图片的同时,调用URL.revokeObjectURL方法删除对应的URL实例,节省内存,固然你不这样作也没什么问题

拖拽文件上传 & 拖拽文件删除

把拖拽文件上传和拖拽文件删除放在一块儿说是由于它们两个功能都须要用到HTML5提供的Drag和Drop API,我们先来学习下这两个API

拖放事件

关于拖放事件有些是在被拖动元素上触发的,而有些则是在放置目标上触发的

当咱们拖动某个元素时,会依次触发:

  • dragstart
  • drag
  • dragend

这三个事件都是在被拖动元素上触发的。当拖动开始时会先触发dragstart事件,而后在拖动的过程当中会持续触发drag事件,当拖动中止时(不管被拖动元素是否放到了有效的放置目标)都会触发dragend事件,这三个事件相似鼠标的移动事件mousestart,mousemove,mouseend

当某个元素被拖动到放置目标上,会依次触发:

  • dragenter
  • dragover
  • dragleave 或 drop

这三个事件都是在放置目标上触发的。当元素进入放置目标时会触发dragenter事件,当元素在放置目标上移动时会持续触发dragover事件,当元素移出放置目标时会触发dragleave事件,当元素被放到了放置目标中会触发drop事件而不是dragleave事件,这几个事件(除drop)也相似鼠标的移动事件mouseenter,mouseover,mouseleave

阻止默认行为。虽然全部的元素都支持drop事件,可是这些元素默认是不容许放置的,这个时候当咱们在放置目标上松开鼠标是不会触发drop事件的,咱们能够经过event.preventDefault()来阻止默认的行为,以下:

droptarget.ondragenter = event => {
    event.preventDefault()
}
droptarget.ondragover = event => {
    event.preventDefault()
}
复制代码

另外在一些浏览器中,当咱们移动图片到放置目标上,松开的时候会打开这张图片,若是移动的是超连接,则会打开这个页面。咱们有时候须要阻止这种默认的行为,能够这样作

droptarget.ondrop = event => {
    event.preventDefault()
}
复制代码

dataTransfer对象

dataTransfer对象用来在拖动的过程当中从被拖动元素向放置目标传递数据,这个对象有两个方法setData和getData

setData有两个参数,第一个是MIME类型,第二个则是咱们要保存的值

event.dataTransfer.setData('text/plain', 'msg')
event.dataTransfer.setData('text/uri-list', 'http://baidu.com')
复制代码

getData只有一个参数,就是setData中咱们传的第一个参数

event.dataTransfer.getData('text/plain')
event.dataTransfer.setData('text/uri-list')
复制代码

setData咱们通常在dragstart中去使用,而getData只能在drop事件中去使用,这个务必记住

dropEffect & effectAllowed

dropEffect和effectAllowed是dataTransfer的两个属性,用来肯定个被拖动的元素以及做为放置目标的元素可以接收什么操做

dropEffect必须搭配effectAllowed才有效果,咱们必须在dragstart中设置这两个属性的值

effectAllowed的取值以下:

  • uninitialized
  • none
  • copy
  • link
  • move
  • copyLink
  • copyMove
  • linkMove
  • all

dropEffect的取值以下:

  • none
  • move
  • copy
  • link

draggable

除了图片,连接,文本以外的元素默认是不能够拖动的,咱们须要添加draggable属性就可让这个元素变得能够拖动

其它成员

dataTransfer除了上述的方法和属性,还有如下方法和属性:

  • addElement(element)
  • clearData(format)
  • setDragImage(element, x, y)
  • types

拖拽文件上传的实现思路:咱们在ondrop中拿到dataTransfer,文件就存放到其files属性中,而后使用FormData对象和XHR2把数据传递给服务器,核心代码以下:

handleDrop = event => {
    event.preventDefault()
    event.stopPropagation()
    const { files } = this.state
    Array.prototype.forEach.call(event.dataTransfer.files, file => {
      const src = URL.createObjectURL(file)
      file.src = src
      this.setState({
        files: [...files, file]
      })
      this.fileInput.value = ''
    })
}
handleUpload = event => {
    event.preventDefault()
    const { files } = this.state 
    if (files.length === 0) {
      this.fileInput.click()
      return
    }
    const formData = new FormData()
    files.forEach((file, index) => {
      formData.append(`img${index+1}`, file)
    })
    // xhr2上传文件 或者 fetch
    const xhr = new XMLHttpRequest()
    xhr.timeout = 3000
    xhr.open('POST', 'upload')
    xhr.upload.onprogress = event => {
      if (event.lengthComputable) {
        const percent = event.loaded / event.total
        console.log(percent)
      }
    }
    xhr.onload = () => {
      if (xhr.status === 200 && xhr.readyState === 4) {
        alert('文件上传成功')
      } else {
        alert('文件上传失败')
      }
    }
    // xhr.send(formData)
    alert('文件上传成功')
    this.setState({
      files: []
    })
    this.fileInput.value = ''
}
复制代码

拖拽文件删除的实现思路:在ondragstart中把拖动的文件的索引放到dataTransfer中,而后在ondrop中取出索引,根据索引值在全局的文件列表中进行删除,核心代码以下:

handleDustDrop = event => {
    event.preventDefault()
    const { dataTransfer } = event
    const index = dataTransfer.getData('text/plain')
    const { files } = this.state
    let deleteFile 
    const newFiles = files.filter((file, index2) => {
      if (index2 === +index) {
        deleteFile = file.name
        URL.revokeObjectURL(file.src)
        return false
      }
      return true
    })
    this.setState({
      files: newFiles,
      deleteFile
    })
    event.currentTarget.style.borderColor = '#cccccc'
}
复制代码

图片预览

图片预览的功能也很是简单,跟删除差很少,点击对应的图片传入index,而后从全局的files中找到对应的file,将其src属性的值赋值给一个预览的img元素的src属性便可

核心代码以下:

showPreview = event => {
    const { currentTarget: { dataset: { index } } } = event
    const { files } = this.state
    this.setState({
      preview: true,
      previewImg: {
        name: files[+index].name,
        src: files[+index].src
      }
    })
}
hidePreview = event => {
    this.setState({
      preview: false,
      previewImg: null
    })
}
复制代码

图片显示以后,给外层容器绑定一个点击事件,单击让预览图片隐藏

最后

查看demo

查看源码,欢迎star

大家的打赏是我写做的动力

微信
支付宝
相关文章
相关标签/搜索