文件上传下载

406 阅读6分钟

前言

在日常工作中,文件上传和下载,是很常用的知识点,不同的上传下载方式适用的场景也是不同的。下面我们针对上传和下载从以下几点出发总结: 1)文件上传和下载的方式有哪些? 2)不同方式的优缺点是什么?适用场景有哪些?

一、文件上传

1. 传统表单上传

早期的文件上传方法是使用HTML表单元素,通过表单提交将文件发送到服务器。这是最早的文件上传方式,仍然广泛使用

1.1 优劣势

  • 优点
    • 是传统的、标准的方法,几乎所有浏览器都支持
    • 无需额外的JavaScript代码
  • 缺点
    • 用户体验有限,无法自定义上传界面
    • 不适合使用高级功能,比如实时进度显示、断点续传等
    • 页面刷新后需要重新选择文件
    • 不能批量处理

1.2 适用场景

简单的文件上传场景,不需要额外的交互和功能。

1.3 代码示例

<form action="/upload" method="POST" enctype="multipart/form-data">
  <input type="file" name="file" id="uploadFile" />
  <input type="submit" value="点击上传文件" />
</form>

1.3.1 单文件上传

<input type="file" name="file" />
const upload = () => {
    // 获取上传的input元素
    const uploadFileEle = document.querySelector("#uploadFile1");
    // 获取文件
    const files = uploadFileEle.files;
    // 获取文件名
    const fieldName = files[0].name;
    // 创建一个formData对象实例
    let formData = new FormData();
    formData.append(fieldName, files[0]);
    // 进行请求
    // axios.post(url, formData)
}

1.3.2 多文件上传

对于多文件上传,我们只需要在input元素里面添加multiple属性,表示支持多文件上传。

<input type="file" name="file" id="uploadFile2" multiple />
const upload = () => {
    // 获取上传的input元素
    const uploadFileEle = document.querySelector("#uploadFile2");
    // 获取文件
    const files = uploadFileEle.files;
    // 创建一个formData对象实例
    let formData = new FormData();
    Object.values(files).forEach((file, i) => {
      formData.append('file' + i, file);
    });
    // 进行请求
    // axios.post(url, formData)
}

1.3.3 文件夹上传

对于文件夹上传,我们只需要稍微改一下。在input元素里面添加webkitdirectory属性,表示是文件夹上传。

需要注意的是这个属性有一定的兼容性要求

<input id="uploadFile3" type="file" webkitdirectory />
const upload = () => {
  // 获取上传的input元素
  const uploadFileEle = document.querySelector("#uploadFile3");
  // 获取文件
  const files = uploadFileEle.files;
  let formData = new FormData();
  Object.values(files).forEach((file, i) => {
    formData.append('file' + i, file);
  });
  // 进行请求
  // axios.post(url, formData)
}

以文件夹方式上传的话,在选择文件夹后悔有个小提示,如下图所示:

image.png

并且我们可以在File对象里面通过webkitRelativePath属性看到该文件的相对路径。

1.3.4 jszip压缩上传

压缩上传就是将文件压缩成压缩包,然后再上传到服务端。压缩还是使用我们前面介绍的jszip库。

下面我用文件夹上传的方式举例。

<input id="uploadFile4" type="file" webkitdirectory />
function generateZipFile(
  zipName,
  files,
  options = { type: "blob", compression: "DEFLATE" }
) {
  return new Promise((resolve, reject) => {
    // 创建 JSZip 对象
    const zip = new JSZip();
    Object.values(files).forEach((file, i) => {
      // 循环遍历 把文件添加到前面创建的 JSZip 对象中
      zip.file("file" + i, file);
    });
    // 生成 JSZip 文件
    zip.generateAsync(options).then(function (blob) {
      zipName = zipName || Date.now() + ".zip";
      const zipFile = new File([blob], zipName, {
        type: "application/zip",
      });
      resolve(zipFile);
    });
  });
}

async function uploadFile() {
  // 获取上传的input元素
  const uploadFileEle = document.querySelector("#uploadFile4");
  // 获取文件
  const files = uploadFileEle.files;
  // 获取相对路径
  let webkitRelativePath = fileList[0].webkitRelativePath;
  // 获取文件夹的名字,用做zip包的名字
  let zipFileName = webkitRelativePath.split("/")[0] + ".zip";
  let zipFile = await generateZipFile(zipFileName, fileList);
  
  let formData = new FormData();
  formData.append('zipfile', zipFile);
  // 进行请求
  // axios.post(url, formData)
  
}

1.3.5 拖拽上传

要实现拖拽上传的功能,我们需要先了解与拖拽相关的事件。比如 drag、dragend、dragenter、dragover 或 drop 事件等。

  • dragenter:当拖拽元素或选中的文本到一个可释放目标时触发;
  • dragover:当元素或选中的文本被拖到一个可释放目标上时触发(每100毫秒触发一次);
  • dragleave:当拖拽元素或选中的文本离开一个可释放目标时触发;
  • drop:当元素或选中的文本在可释放目标上被释放时触发。

关于拖拽事件大家可以查看mdn 官方文档

拖拽上传的核心是通过 DataTransfer 对象的 files 属性来获取文件列表,然后在利用FormData进行上传。

<!-- 这个id属性放在哪个盒子里面 哪个盒子的范围就是拖拽上传的范围 -->
<div class="box" id="drop">
    <h1>拖拽上传文件</h1>
    <input type="file" id="file-btn" onchange="selectFile(event)" accept=".doc,.docx" style="display: none;">
</div>
  // 拖拽上传获取对应文件
  let dropBox=document.querySelector('#drop');
  // 当文件在目标元素内移动时
  dropBox.addEventListener('dragover',function(e){
    // 阻止事件冒泡
    e.stopPropagation();
    // 阻止默认事件(与drop事件结合,阻止拖拽文件在浏览器打开的默认行为)
    e.preventDefault();
  })
  // 当拖拽文件在目标元素内松开时
  dropBox.addEventListener('drop',function(e){
    // 阻止事件冒泡
    e.stopPropagation();
    // 阻止默认事件(与dragover事件结合,阻止拖拽文件在浏览器打开的默认行为)
    e.preventDefault();
    // 获取拖拽上传的文件(files是个数组 此处默认限制只能上传一个)
    console.log('获取拖拽上传的文件---',e.dataTransfer.files[0]);
    // 第二次验证选择的文件类型是否正确
    if(e.dataTransfer.files[0].type == 'application/msword' || e.dataTransfer.files[0].type == 'application/vnd.openxmlformats-officedocument.wordprocessingml.document') {
      file = e.dataTransfer.files[0]
    } else {
      alert('请选择正确的文件类型')
    }
  })

1.3.6 复制粘贴上传

复制粘贴我们首先需要了解Clipboard对象

我们可以通过 navigator.clipboard 来获取 Clipboard 对象,然后通过navigator.clipboard.read()获取内容。但是对于不兼容的我们需要通过 e.clipboardData.items 来访问剪贴板中的内容。

下面的例子是获取剪切板里面的图片进行上传。

onst IMAGE_MIME_REGEX = /^image\/(jpe?g|gif|png)$/i;
const uploadAreaEle = document.querySelector("#uploadArea");

// 监听粘贴事件
uploadAreaEle.addEventListener("paste", async (e) => {
  e.preventDefault();
  const files = [];
  if (navigator.clipboard) {
    let clipboardItems = await navigator.clipboard.read();
    for (const clipboardItem of clipboardItems) {
      for (const type of clipboardItem.types) {
        if (IMAGE_MIME_REGEX.test(type)) {
          const blob = await clipboardItem.getType(type);
          files.push(blob);
        }
      }
    }
  } else {
    const items = e.clipboardData.items;
    for (let i = 0; i < items.length; i++) {
      if (IMAGE_MIME_REGEX.test(items[i].type)) {
        let file = items[i].getAsFile();
        files.push(file);
      }
    }
  }
  
  // 有了files我们就可以利用FormData进行上传啦
  let formData = new FormData();
  files.forEach((file, i) => {
    formData.append("file" + i, file);
  });
  // 进行请求
  // axios.post(url, formData)
});

2. XMLHttpRequest

XMLHttpRequest是一种传统的、基于回调的方式,用于在前端发送HTTP请求。虽然它不是专门用于文件上传的API,但可以通过它来实现文件上传。

2.1 优劣势

  • 优点:
    • 支持自定义上传界面和进度显示
    • 可以实现高级功能,如断点续传、取消上传等
  • 缺点:
    • 使用回调函数处理请求状态,代码相对繁琐
    • 不支持Promise,需要手动处理异步操作

2.2 适用场景

适用于需要更高度自定义的文件上传场景,如实现自定义上传进度条、取消上传、上传队列等。

2.3 代码示例:

const fileInput = document.getElementById('fileInput');
const uploadButton = document.getElementById('uploadButton');

uploadButton.addEventListener('click', () => {
  const file = fileInput.files[0];
  
  // 创建一个 XMLHttpRequest对象
  const xhr = new XMLHttpRequest();
  // 设置请求方法为POST,目标URL为文件上传的服务器端地址
  xhr.open('POST', '/upload', true);
  xhr.onreadystatechange = function () {
    if (xhr.readyState === 4 && xhr.status === 200) {
      console.log('上传成功');
    }
  };
  // 使用 `FormData` 对象来构建表单数据,将文件附加到表单中
  const formData = new FormData();
  formData.append('file', file);
  // 通过 `xhr.send(formData)` 发送请求,上传文件
  xhr.send(formData);
});

3. Fetch API

Fetch API 是现代的、基于Promise的HTTP请求API,也可以用于文件上传。

3.1 优劣势

  • 优点:
    • 使用Promise,更清晰和易于理解
    • 支持自定义上传界面和进度显示
    • 可以实现高级功能,如断点续传、取消上传等
  • 缺点:
    • 在部分老版本浏览器中可能需要 polyfill

3.2 适用场景

Fetch API 是现代浏览器中首选的文件上传方式,适用于大多数文件上传场景。

3.3 代码示例

const fileInput = document.getElementById('fileInput');
const uploadButton = document.getElementById('uploadButton');

uploadButton.addEventListener('click', async () => {
  const file = fileInput.files[0];
  const formData = new FormData();
  formData.append('file', file);

  try {
    // 使用 fetch 函数发送POST请求,设置请求方法和请求体为 FormData 对象
    // 使用 await关键字处理异步操作,等待响应结果。
    const response = await fetch('/upload', {
      method: 'POST',
      body: formData,
    });

    if (response.ok) {
      console.log('上传成功');
    } else {
      console.error('上传失败');
    }
  } catch (error) {
    console.error('上传出错', error);
  }
});

4. FormData

FormData 是一种用于创建表单数据的API,可以用于构建文件上传请求的请求体。

4.1 优缺点

  • 优点:
    • 构建和发送请求非常简单。
    • 支持多文件上传,可以轻松添加多个文件。
    • 支持添加其他表单字段。
  • 缺点:
    • 不支持自定义上传界面和进度显示,需要额外的代码来实现。
    • 不支持高级功能,如断点续传、取消上传等。

4.2 适用场景

FormData适用于简单的文件上传场景,特别是需要同时上传多个文件或其他表单字段的情况。

代码示例:

const fileInput = document.getElementById('fileInput');
const uploadButton = document.getElementById('uploadButton');

uploadButton.addEventListener('click', async () => {
  const file = fileInput.files[0];
  const formData = new FormData();
  formData.append('file', file);

  try {
    const response = await fetch('/upload', {
      method: 'POST',
      body: formData,
    });

    if (response.ok) {
      console.log('上传成功');
    } else {
      console.error('上传失败');
    }
  } catch (error) {
    console.error('上传出错', error);
  }
});

5. 第三方上传库

除了原生的文件上传方法外,还有许多第三方上传库可供选择。这些库通常提供更多的功能和可定制性,以简化文件上传过程。

5.1 优劣势

  • 优点:

    • 提供了丰富的文件上传功能和用户界面。
    • 简化了文件上传的实现。
    • 支持拖放上传、文件预览等高级功能。
  • 缺点:

    • 需要额外的库和样式文件。
    • 可能会引入较大的依赖。

5.2 适用场景

第三方上传库适用于需要丰富功能和用户界面的文件上传场景,如图片上传、音视频上传等。

6 云存储服务

在某些情况下,文件上传可能会通过云存储服务进行,例如,Amazon S3、Google Cloud Storage 或 Azure Blob Storage。这些服务通常提供了强大的存储和文件管理功能,并通过API进行文件上传。

6.1 优缺点

  • 优点:
    • 适用于大规模文件存储和管理
    • 提高可用性和可扩展性
    • 可以使用云存储服务的高级功能,如文件版本控制、访问控制等
  • 缺点:
    • 需要配置和维护云存储服务
    • 可能会产生额外的费用

6.2 适用场景

云存储服务适用于需要大规模、可扩展的文件存储和管理场景,如媒体文件存储、备份等。

二、文件下载

1. 直接下载

适用场景:仅适用于浏览器无法识别的文件。如果是浏览器支持的文件格式,如html、jpg、png、pdf等,则不会触发文件下载,而是直接被浏览器解析并展示

1.1 location.href

示例代码:

window.location.href = url;

1.2 window.open

示例代码:

window.open(url);

2. a标签

  • 适用场景
    • 适用于单文件下载。如果下载多文件,点击过快就会重置掉前面的请求
    • get请求
    • 需要自定义文件名
  • 注意:有时候对于浏览器可识别的文件格式,我们还是需要直接下载的情况,可以声明一下文件的headerContent-Disposition信息,告诉浏览器,该链接为下载附件链接,并且可以声明文件名,Content-Disposition: attachment; filename="filename.xls"

2.1 文件地址已知

<a href="https://.../xxx.xlsx" download="test">下载文件</a>

2.2 文件地址异步获取

const download = (filename, url) => {
    let a = document.createElement('a'); 
    a.style = 'display: none'; // 创建一个隐藏的a标签
    a.download = filename; // 设置文件名
    a.href = url;
    document.body.appendChild(a);
    a.click(); // 触发a标签的click事件
    document.body.removeChild(a);
}

3. 文件流

如果需要使用post请求,且后端返回是一个文件流形式,那么前端需要自己将文件流转成链接,然后下载。

  • 适用场景
    • post请求
    • get请求
    • 多文件

3.1 请求的方式

注意:不可以使用JQuery,因为JQuery不支持blob类型

3.1.1 原生js写法

const req = new XMLHttpRequest();
req.open('POST', '/download/excel', true);
req.responseType = 'blob'; //如果不指定,下载后文件会打不开
req.setRequestHeader('Content-Type', 'application/json');
req.onload = function() {
    var content = req.getResponseHeader("Content-Disposition") ;
    // 文件名最好用后端返的Content-disposition
    // 需要后端设置 Access-Control-Expose-Headers: Content-disposition 使得浏览器将该字段暴露给前端
    var name = content && content.split(';')[1].split('filename=')[1];
    var fileName = decodeURIComponent(name)
    downloadFile(req.response,fileName)
};
req.send( JSON.stringify(params));

3.1.2 axios写法

axios({
  method: 'post',
  headers: {
    'Content-Type': 'application/json; charset=utf-8'
  },
  url: '/robot/strategyManagement/analysisExcel',
  responseType: 'blob', // 指定响应中包含的数据类型
  headers: { //如果需要权限下载的话,加在这里
        Authorization: '123456'
    }
  data: JSON.stringify(params),
}).then(function(res){
   var content = res.headers['content-disposition'];
   var name = content && content.split(';')[1].split('filename=')[1];
   var fileName = decodeURIComponent(name)
   downloadFile(res.data,fileName)
})

3.2 文件下载的方式

此方式需要与后端配合,当点击下载按钮时,请求接口,返回文件流。

3.2.1 通过URL.createObjectURL()下载文件

URL.createObjectURL() 静态方法会创建一个 DOMString,其中包含一个表示参数中给出的对象的URL。这个 URL 的生命周期和创建它的窗口中的 document 绑定。这个新的URL 对象表示指定的 File 对象或 Blob 对象。

createObjectURL()支持传入 File 对象、Blob 对象或者 MediaSource 对象(媒体资源)。

示例代码:

handleDownload(file) {
  service.get(`/download?fileId${file.id}`,{responseType: 'blob'})
  .then((blobContent)=>{
      let a = document.createElement('a')
      a.download = file.fileName
      a.style.display = 'none'
      let url = URL.createObjectURL(blobContent)
      a.href = url
      document.body.appendChild(a)
      a.click()
      URL.revokeObjectURL(url) // 销毁
      document.body.removeChild(a)
  }
},

注意:URL.createObjectURL()创建的对象使用完成后,即下载后可以通过URL.revokeObjectURL()移除该对象,释放内存。理论上讲dom销毁时,URL.createObjectURL()创建的对象也会随之销毁,我们可以不必手动销毁。但当页面有类似表格形式的文件列表时,下载完文件立即手动销毁对象无疑是最好的选择。

3.2.2 通过FileReader.readAsDataURL()下载文件

readAsDataURL 方法会读取指定的 Blob 或 File 对象。读取操作为异步操作,当读取完成时,可以从onload回调函数中通过实例对象的result属性获取data:URL格式的字符串(base64编码),此字符串即为读取文件的内容,可以放入a标签的href属性中。

示例代码:

handleDownload(file) {
  service.get(`/download?fileId${file.id}`,{responseType: 'blob'})
  .then((blobContent)=>{
      // 创建FileReader实例
      const reader = new FileReader()
      // 传入被读取的blob对象
      reader.readAsDataURL(blobContent)
      // 读取完成的回调事件
      reader.onload = (e) => {
        let a = document.createElement('a')
        a.download = file.fileName
        a.style.display = 'none'
        // 生成的base64编码
        let url = reader.result
        a.href = url
        document.body.appendChild(a)
        a.click()
        document.body.removeChild(a)
      }
  }
},

URL.createObjectURL()FileReader.readAsDataURL()的异同点

  • 相同点:都是通过将blob转成地址,再放入a标签实现下载
  • 区别点
    • 返回值
      • FileReader.readAsDataURL(blob)可以得到一段base64的字符串
      • URL.createObjectURL(blob)得到的是当前文件的一个内存url
    • 内存使用
      • FileReader.readAsDataURL(blob)得到一段超长的base64的字符串
      • URL.createObjectURL(blob)得到的是一个url地址
    • 内存清理
      • FileReader.readAsDataURL(blob)依照js垃圾回收机制自动从内存中清理
      • URL.createObjectURL(blob)存在于当前document内,清除方式只有upload()事件或者revokeObjectURL()手动清除
    • 执行方式
      • FileReader.readAsDataURL(blob)通过回调的方式f返回,异步执行;
      • URL.createObjectURL(blob) 直接返回,同步执行;
    • 多个文件
      • FileReader.readAsDataURL(blob)同时处理多个文件时,需要一个文件对应一个FileReader对象;
      • URL.createObjectURL(blob) 依次返回,没有影响;