前端文件的上传和下载

简介

前端的上传下载平时不会经常用到,就算用到可能也是前人已经写好的模块或者是第三方库,引入就可以使用了。但是笔者觉得作为前端开发,文件的上传和下载还是非常有必要了解清楚的。

本文主要讲述前端文件上传和下载,这里可能会涉及到前端的一些二进制,例如ArrayBuffer、TypedArray、DataView、Blob、File、Base64、FileReader等,如果对这些不清楚的话可以先看看笔者写的前端二进制一次性搞清楚文章。

下载

对于下载,方式有很多种,而且每种方式都有各自的特点。

超链接下载

使用超链接来完成我们前端的下载是最常见的一种方式。其主要利用了window.URL.createObjectURL(blob)方法生成 blob url,然后将blob url赋值给超链接的href属性,然后模拟点击超链接进行下载。

简单的理解一下就是将一个fileBlob类型的对象转为UTF-16的字符串,并保存在当前操作的document下,存储在内存中。

生成blob url使用的方法是URL.createObjectURL(file/blob)。清除方式只有页面unload()事件或者使用URL.revokeObjectURL(objectURL)手动清除 。

download属性用来给下载的文件命名的,但是需要注意download属性的兼容性。

下面我将模拟后端返回ArrayBufferBlob对象来实现超链接方式下载。

后端返回ArrayBuffer

function aDownload1() {
  // 模拟后端返回 ArrayBuffer
  const str = "hello randy!";
  let ab = new ArrayBuffer(str.length);
  let ia = new Uint8Array(ab);
  for (let i = 0; i < str.length; i++) {
    ia[i] = str.charCodeAt(i);
  }

  const a = document.createElement("a");
  // 设置文件名为test
  a.download = "test";
  // 将 ArrayBuffer 转成blob
  const blob = new Blob([ia], { type: "text/plain" });
  // 生成blob url。这里可以使用Blob对象或者File对象
  a.href = window.URL.createObjectURL(blob);
  a.style.display = "none";
  document.body.appendChild(a);
  a.click();
  // 释放内存
  window.URL.revokeObjectURL(a.href);
  // 移除a元素
  document.body.removeChild(a);
}
复制代码

后端返回Blob

function aDownload2() {
  // 模拟后端返回 Blob
  const blob = new Blob(["hello", "randy"], { type: "text/plain" });

  const a = document.createElement("a");
  // 设置文件名为test
  a.download = "test";
  // 直接生成blob url。这里可以使用Blob对象或者File对象
  a.href = window.URL.createObjectURL(blob);
  a.style.display = "none";
  document.body.appendChild(a);
  a.click();
  // 释放内存
  window.URL.revokeObjectURL(a.href);
  // 移除a元素
  document.body.removeChild(a);
}
复制代码

showSaveFilePicker API 下载

showSaveFilePicker 是一个新的api,调用该方法后会显示允许用户选择保存路径的文件选择器。

const FileSystemFileHandle = Window.showSaveFilePicker(options);
复制代码

showSaveFilePicker 方法支持一个对象类型的可选参数,可包含以下属性:

  1. excludeAcceptAllOption:布尔类型,默认值为 false。默认情况下,选择器应包含一个不应用任何文件类型过滤器的选项(由下面的 types 选项启用)。将此选项设置为 true 意味着 types 选项不可用。

  2. types:数组类型,表示允许保存的文件类型列表。数组中的每一项是包含以下属性的配置对象:

  • description(可选):用于描述允许保存文件类型类别。
  • accept:是一个对象,该对象的 keyMIME 类型,值是文件扩展名列表。
  1. suggestedName 建议的文件名。
async function download3(blob, filename) {
  try {
    const handle = await window.showSaveFilePicker({
      suggestedName: filename,
      types: [
        {
          description: "text file",
          accept: {
            "text/plain": [".txt"],
          },
        },
        {
          description: "jpeg file",
          accept: {
            "image/jpeg": [".jpeg"],
          },
        },
      ],
    });
    const writable = await handle.createWritable();
    await writable.write(blob);
    await writable.close();
    return handle;
  } catch (err) {
    console.error(err.name, err.message);
  }
}

function showSaveFilePickerDownload() {
  // 模拟blob文件
  const blob = new Blob(["hello", "randy"], { type: "text/plain" });

  download3(blob, "test.txt");
}
复制代码

当你点击下载后会出现如下选择界面,会出现你建议的文件名和文件类型选择。

WX20220311-143509.png

相比 a 标签下载 的方式,showSaveFilePicker API 允许你选择文件的下载目录、选择文件的保存格式和更改存储的文件名称。不过可惜的是该 API 目前的兼容性还不是很好。

FileSaver 下载

FileSaver.js是在客户端保存文件的解决方案,非常适合在客户端上生成文件的 Web 应用程序。

兼容性如下

对于FileSaver.js 我们主要需要记住他的这个saveAs方法。

FileSaver.saveAs(
 Blob/File/Url, 
 optional DOMString filename, 
 optional Object { autoBom }
)
复制代码

saveAs 方法支持 3 个参数:

第 1 个参数表示它支持 Blob/File/Url 三种类型。

第 2 个参数表示文件名(可选)。

而第 3 个参数表示配置对象(可选)。如果你需要 FlieSaver.js 自动提供 Unicode 文本编码提示字节顺序标记,则需要设置 { autoBom: true}。请注意,只有当blob类型的charset=utf-8设置时,才能执行此操作。

安装

npm install file-saver --save
复制代码

blob数据源

const blob = new Blob(["Hello, world!"], {type: "text/plain;charset=utf-8"});
FileSaver.saveAs(blob, "helloworld.txt");
复制代码
const canvas = document.getElementById("my-canvas");
canvas.toBlob(function(blob) {
    saveAs(blob, "prettyimage.png");
});
复制代码

file数据源

const file = new File(["Hello, world!"], {type: "text/plain;charset=utf-8"});
FileSaver.saveAs(file, "helloworld.txt");
复制代码

网络链接数据源

FileSaver.saveAs("https://httpbin.org/image", "image.jpg");
复制代码

jszip 压缩下载

jszip可以让下载的文件转为zip格式。

jszip自己不具备下载功能,他只是提供了将文件压缩成zip包的功能,下载的话我们还是需要借助前面所说的FileSaver.js

安装

npm install jszip
复制代码

使用

// 创建 JSZip 对象
var zip = new JSZip();

// 把文件添加到前面创建的 JSZip 对象中,可以添加多个
zip.file("Hello.txt", "Hello World\n");

// 生成 JSZip 文件
zip.generateAsync({type:"blob"}).then(function(content) {
  // 这里需要用到上面说的 FileSaver.js
  FileSaver.saveAs(content, "example.zip");
});
复制代码

关于jszip更多的使用方法可以查看官方文档

上传

在上传这一块,不管使用何种方式,都是先获取到文件对象然后在利用表单FormData对象进行传输。

单文件上传

<input id="uploadFile1" type="file" accept="image/*" />
复制代码
const upload = () => {
  // 获取上传的input元素
  const uploadFileEle = document.querySelector("#uploadFile1");
  // 获取文件
  const files = uploadFileEle.files;
  let formData = new FormData();
  formData.append(fieldName, files[0]);
  // 进行请求
  // axios.post(url, formData)
}
复制代码

多文件上传

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

<input id="uploadFile2" type="file" accept="image/*" multiple />
复制代码

对于js,我们需要在表单里面循环添加我们的文件。

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

文件夹上传

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

该属性的兼容性如下,需要注意IE是完全不支持的。

<input id="uploadFile3" type="file" accept="image/*" webkitdirectory />
复制代码

对于js,我们需要在表单里面循环添加我们的文件。

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)
}
复制代码

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

WX20220314-133332.png

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

jszip 压缩上传

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

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

<input id="uploadFile4" type="file" accept="image/*" 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)
  
}
复制代码

拖拽上传

要实现拖拽上传的功能,我们需要先了解与拖拽相关的事件。比如 dragdragenddragenterdragoverdrop 事件等。

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

关于拖拽事件大家可以查看mdn 官方文档笔者在这里就不细说了。

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

核心代码

dropAreaEle.addEventListener("drop", handleDrop, false);

function handleDrop(e) {
  // 在dataTransfer对象上获取文件列表
  const files = e.dataTransfer.files;
  let formData = new FormData();
  Object.values(files).forEach((file, i) => {
    formData.append("file" + i, file);
  });
  // 进行请求
  // axios.post(url, formData)
}
复制代码

复制粘贴上传

对于复制粘贴我们首先需要了解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)
});
复制代码

关于FormData

前面的上传都涉及到了FormData,关于FormData很多小伙伴可能不太理解,笔者在这里详细讲解下关于FormData的相关api

FormData我们可以想像成js版的表单。功能和我们的html表单是类似的。

// 通过FormData构造函数创建一个空对象
const formdata = new FormData();
// 可以通过append()方法来追加数据
formdata.append("name","randy");
// 通过get方法对值进行读取
console.log(formdata.get("name"));//randy
// 通过set方法对值进行设置
formdata.set("name","demi");
console.log(formdata.get("name"));//demi
// 获取key为age的所有值,返回值为数组类型
formdata.getAll("age");
// 判断是否包含key为name的数据
console.log(formdata.has("name"));//true
// 删除key为name的值
formdata.delete("name");
复制代码

除了创建一个全新的formData,我们还可以基于一个现有表单进行初始化。

<form id="myForm">
  名称:<input type="text" name="name"  value="randy">
</form>
复制代码
// 根据id获得页面当中的form表单元素
const myForm = document.querySelector("#myForm");
// 将获得的表单元素作为参数,对formData进行初始化
const formdata=new FormData(myForm);
console.log(formdata.get("name"));// randy
复制代码

对于formData 类似Object,支持keysvaluesentries三种遍历方式

formData.keys();
formData.values();
formData.entries();
复制代码

更多细节可以自行查看mdn 官方文档

阿里 oss上传和下载

除了上面笔者介绍的在自己服务器上传下载,我们还可能会碰到第三方服务器的上传和下载,例如oss,这个我们也是需要了解的。对于我们前端来说着重看 oss node文档就可以了。

参考文章

文件下载,搞懂这9种场景就够了

文件上传,搞懂这8种场景就够了

后记

本文为笔者个人学习笔记,如有谬误,还请告知,万分感谢!如果本文对你有所帮助,还请点个关注点个赞~,您的支持是笔者不断更新的动力。

Guess you like

Origin juejin.im/post/7074869887759286280