关于文件上传下载我所知道的全部内容

1,015 阅读6分钟

文件上传是一个很基础的内容,有很多的应用场景,但是前端各种库和框架实在是太便利了,根本不用了解到用原生的是怎么实现的,一遇到问题就各种懵逼,最近刚好经历了几种文件上传的需求,就以此来作为开年的第一篇分享

1. 表单上传

在AJAX还不流行的年代,表单上传文件是基本操作。表单上传文件很简单,有两个需要重点关注的属性:

1.1 enctype

属性用于设定form表单提交的时候数据编码方式,一共有三种参数选择:

  1. application/x-www-form-urlencoded 发送前编码所有字符
  2. multipart/form-data 不对字符进行编码
  3. 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上传

如果要实现页面不刷新的文件上传,有两种常用的方案:

  1. <iframe>表单提交方案
  2. 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()canvasdragImage()方法来实现绘制)

/**
 * 验证图片类型
 * @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获取到拖拽信息。该对象存在的两个对象属性filesitems,如果拖拽的内容是文件,那么可以遍历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:

  1. 这里尤其要注意entry.file()方法,想要获取到文件信息只能在回调函数中获取
  2. 由于浏览器安全性问题,本地是不能直接访问文件系统的,所以,如果以上的例子不在服务端运行,会报错DOMException(这个问题花费了我N个小时),可以全局安装一个http-server来运行上面的代码

4. 总结

编程真的是一件很好玩的事情,最近看算法的基础,觉得真的很有意思,前端编程也一样,如果仅仅停留在使用组件上,真的很没意思,有时间可以多多看看各种原生的事件和方法,深入研究一下框架相当有意思。超级感谢MDN啊,基本上可以获取到所有想要的信息

完整DEMO的:github.com/PatrickLh/f…

5. 参考

MDN XMLHttpRequest

MDN File and Directory Entries API

MDN HTML Drag and Drop API