文件上传是一个很基础的内容,有很多的应用场景,但是前端各种库和框架实在是太便利了,根本不用了解到用原生的是怎么实现的,一遇到问题就各种懵逼,最近刚好经历了几种文件上传的需求,就以此来作为开年的第一篇分享
1. 表单上传
在AJAX还不流行的年代,表单上传文件是基本操作。表单上传文件很简单,有两个需要重点关注的属性:
1.1 enctype
属性用于设定form表单提交的时候数据编码方式,一共有三种参数选择:
application/x-www-form-urlencoded
发送前编码所有字符multipart/form-data
不对字符进行编码text/plain
空格转换为+
,但是不会对字符进行编码
如果想要使用文件上传,必须指定为第二个属性值:enctype=multipart/form-data
1.2 multiple
对于选择文件的时候如果想对文件进行多选,那么必须要设置<input type="file" multiple="multiple">
一个比较完整代码片段
<form action="http://localhost:3000/upload" method="POST" enctype="multipart/form-data">
<input type="file" name="file" multiple="multiple">
<input type="submit" value="submit"/>
</form>
2. AJAX上传
如果要实现页面不刷新的文件上传,有两种常用的方案:
<iframe>
表单提交方案- AJAX方案
第一种方案在页面中嵌套一个<iframe>
,将表单放置于<iframe>
中,此时完成表单提交不会发生全局页面刷新。但是这个方案,随着AJAX的逐渐完善以及前后端分离和单页面应用的普及,轮为了很不常规的替代方案。
2.1 基本内容
实现AJAX上传,首先需要对XHR有所了解(如有不了解的可以参照MDN的学习文档AJAX开始)
XHR在发送数据的时候可以接受一个html5的新对象FormData
,可以通过将包含文件的表单/活着将文件放到FormData
中传递到后端接口,
html:
<form id="fileForm">
<input type="file" name="file" multiple="multiple" onchange="changeFileChoose(event)">
<input type="button" onclick="upload();" value="submit"/>
</form>
js:
let formData = new FormData(document.getElementById('fileForm'));
let xhr = new XMLHttpRequest();
xhr.open('POST', 'http://localhost:3000/upload');
xhr.setRequestHeader('Content-Type', 'multipart/form-data');
xhr.send(formData);
如果表单中每个文件想单独发送请求(发送多次请求),可以获取表单中文件信息并构建多个表单对象上传
formData.getAll('file').filter(file => {
return file.name
}).forEach((file, index) => {
let separateFormData = new FormData();
separateFormData.set('file', file);
xhr.send(separateFormData)
})
PS:在传递到时候注意设置请求头信息Content-type: multiple/form-data
来支持文件上传操作
2.2 上传进度
将上传过程的上传进度告诉用户是一个很好的用户交互行为,一方面避免用户多次重复上传,另一方面也是对用户操作对反馈,告诉用户系统正在处理他的操作。
监听文件上传进度,个人认为要么前端轮询获取后端的文件写入情况,要么前端有支持上传进度获取对事件,其实确实AJAX上传过程中提供了相关对象,获取到文件的网络传输情况的,所以在对上传结果要求并非十分严格的情况下,通过前端监听反馈进度已经足够了
上传进度的监听需要使用xhr.upload
对象的事件,利用监听xhr.upload.onprogress
来实现上传进度的监听
xhr.upload.onprogress = ev => {
console.log(`upload loaded: ${ev.loaded}, total: ${ev.total}`);
progress = ev.loaded * 100 / ev.total;
}
onprogress
事件的event
对象中包含前端已经传输的数据信息ev.loaded
以及文件的总尺寸信息ev.total
,利用这些信息就可以在页面中显示文件上传进度
2.3 取消上传
AJAX自身提供了取消操作,通过利用xhr.abort()
方法来取消掉整个xhr
的请求,当然如果仅仅想取消文件上传而不是取消整个AJAX过程,也可以使用xhr.upload.abort()
单独的取消掉AJAX过程中的文件上传
2.4 选择图片并上传预览
<input type="file">
的onchange
事件在选择文件发生变更的时候会触发,利用事件中的event
对象的event.target.files
,可以获取到当前选择的文件集合,遍历该集合,根据file.type
来判断文件类型,并利用window.URL.createObjectURL(file)
可以拿到转换过后的base64图片地址,最后再给图片img.src
设置路径从而实现选择回显(图片可以使用createElement('img')
并body.appendChild()
,也可以使用new Image()
和canvas
的dragImage()
方法来实现绘制)
/**
* 验证图片类型
* @param {*} type 文件类型
*/
function validateImage(type) {
return ['image/jpeg', 'image/png', 'image/jpg'].includes(type);
}
if (validateImage(file.type)) {
let image = document.createElement('img');
// URL.createObjectURL可以接受File, Blob, MediaSource对象
image.style.height = '100px';
image.style.width = '100px';
image.src = window.URL.createObjectURL(file);
document.body.appendChild(image);
}
PS:由于图片加载对浏览器来说是异步的过程,如果要对图片进行相关操作,请在img.onload
操作以后执行
3. 拖拽上传
在了解AJAX上传的基础上,其实拖拽上传只需要知道如何获取到拖拽文件对象,就可以使用相同的方法进行上传了。
拖拽也是有一系列事件,具体拖拽相关事件,可以参见接下来的分享或者MDN Drag and Drop API
3.1 文件拖拽
文件拖拽上传的关键在于,可以通过event.dataTransfer
获取到拖拽信息。该对象存在的两个对象属性files
和items
,如果拖拽的内容是文件,那么可以遍历files
对象,就可以获得文件信息
html:
<div>
<p>拖拽上传</p>
<div id="fileArea" class="file_area">拖拽到此区域上传</div>
</div>
js:
let fileArea = document.querySelector('#fileArea')
fileArea.addEventListener('drop', ev => {
let files = ev.dataTransfer.files
for (let i = 0; i < files.length; i++) {
// 调用ajax相关内容
sendFile(files[i]);
}
// 防止浏览器直接打开文件
ev.preventDefault();
})
3.2 目录拖拽
突然某一天出现了目录拖拽的需求,以为和文件上传是同样可以通过files
来获取,结果发现不行。这个时候需要使用另一个属性对象items
,并利用File and Directory Entries API来处理items
。
首先利用item.webkitGetAsEntry()/item.getEntry()
获取到FileEntry
,之后使用entry.createReader()
获取到reader
对象,之后reader.readEntries
读取信息并递归分别处理文件和文件夹,如果是文件通过entry.file()
的方式获取文件信息
js:
fileArea.addEventListener('drop', ev => {
for (let i = 0; i < ev.dataTransfer.items.length; i++) {
// 获取entry对象
let entry = ev.dataTransfer.items[i].webkitGetAsEntry()
if (entry) {
scanFiles(entry, sendFile)
}
}
// 防止浏览器直接打开文件
ev.preventDefault();
})
function scanFiles (entry, callback) { // 浏览文件结构
// 如果是文件目录,那么继续循环获取到目录下的文件
if (entry.isDirectory) {
let directoryReader = entry.createReader();
directoryReader.readEntries(entries => {
entries.forEach(entry => {
scanFiles(entry, callback);
})
}, err => {
console.log(err, err.message);
})
}
// 如果是文件,安么添加到最后的文件数据集中
if (entry.isFile) {
i++
entry.file(file => {
callback(file, i);
}, err => {
console.log(err, err.message);
})
}
}
PS:
- 这里尤其要注意
entry.file()
方法,想要获取到文件信息只能在回调函数中获取- 由于浏览器安全性问题,本地是不能直接访问文件系统的,所以,如果以上的例子不在服务端运行,会报错
DOMException
(这个问题花费了我N个小时),可以全局安装一个http-server来运行上面的代码
4. 总结
编程真的是一件很好玩的事情,最近看算法的基础,觉得真的很有意思,前端编程也一样,如果仅仅停留在使用组件上,真的很没意思,有时间可以多多看看各种原生的事件和方法,深入研究一下框架相当有意思。超级感谢MDN啊,基本上可以获取到所有想要的信息
完整DEMO的:https://github.com/PatrickLh/file-upload