一直想作下图片上传的功能,今天终于把这个心愿了了,在制做的过程当中也顺便把HTML5的File,Drop,Drag,URL,FileReader复习了下,多赢php
查看demohtml
查看源码,欢迎starreact
咱们先来回顾下文件上传有几种方式git
这个是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
另外将咱们的input标签的type设置为file,点击以后就能够打开系统的文件管理器,单击上传按钮就能够把咱们选择的文件发送到服务器了ajax
除了使用form表单来提交数据外,咱们还能够本身构建表单数据进行提交,其中FormData用来建立表单数据,是属于HTML5的东西,XHR2用来发送请求到服务器json
FormData对象API:跨域
我在demo中是这样用的数组
const formData = new FormData()
files.forEach((file, index) => {
formData.append(`img${index+1}`, file)
})
复制代码
XHR2是用来发送请求的,ajax的实现就是靠它,正是由于XHR2的出现才使得经过ajax上传文件变成可能,XHR2相对于XHR有如下特色:
关于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是一种新的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,我这里引用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的属性,函数,事件这里列举下
介绍完了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事件,因此咱们的操做都必须写在其回调函数里面
使用了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事件,这三个事件相似鼠标的移动事件mousestart,mousemove,mouseend
当某个元素被拖动到放置目标上,会依次触发:
这三个事件都是在放置目标上触发的。当元素进入放置目标时会触发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对象用来在拖动的过程当中从被拖动元素向放置目标传递数据,这个对象有两个方法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是dataTransfer的两个属性,用来肯定个被拖动的元素以及做为放置目标的元素可以接收什么操做
dropEffect必须搭配effectAllowed才有效果,咱们必须在dragstart中设置这两个属性的值
effectAllowed的取值以下:
dropEffect的取值以下:
除了图片,连接,文本以外的元素默认是不能够拖动的,咱们须要添加draggable属性就可让这个元素变得能够拖动
dataTransfer除了上述的方法和属性,还有如下方法和属性:
拖拽文件上传的实现思路:咱们在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
})
}
复制代码
图片显示以后,给外层容器绑定一个点击事件,单击让预览图片隐藏
大家的打赏是我写做的动力