跳转到内容

前端文件上传与相关操作

刀刀

4/8/2025

0 字

0 分钟

    学习明灯
  • UP主:三十的前端课,视频地址:前往学习

前置知识

上传方案

  • 通过二进制的 blob 传输,如通过 formData 传输
  • 通过转为 base64 字符串上传,不过字符串体积会很大

相关对象

files

当用户选择一个或多个文件后,可以通过 <input type="file"> 元素标签获取文件数据,它返回一个 FileList 对象,其中包含用户选择的文件,可以获取文件类型、名称、大小等信息,前端用于做其他处理。

js
const fileInput = document.getElementById("fileInput");

fileInput.addEventListener("change", (event) => {
  const selectedFiles = event.target.files;

  // 遍历选中的文件
  for (let i = 0; i < selectedFiles.length; i++) {
    const file = selectedFiles[i];
    console.log("文件名:", file.name);
    console.log("文件类型:", file.type);
    console.log("文件大小:", file.size, "bytes");

    if (file.type !== "video/mp4") {
      alert("只能上传mp4文件");
    }
  }
});

其中,file 打印结果如下所示:

pCHx6Zn.png

拓展

file 对象本质是 File 类的实例化。这是另外一种创建文件的方案,但一般不会这么做,因为不能让用户自己选择,也无法写入磁盘。

js
console.log(new File(["content"], "a.txt")); // 第一个参数必须是一个数组,第二个参数是文件名

最终打印结果如下所示:

pCHxoL9.png

⚠️ 注意

  1. 获取到的对象不能直接给后端,因为这个是前端的对象,后端不认识,需要使用 formData 转换
  2. file 对象是 blob 对象的一个子类,他本质也是一个 blob

blob

Blob(Binary Large Object)是一种数据对象,用于表示二进制数据。在前端上传文件给后端时,通常会将文件数据转换为 Blob 对象来进行处理和传输。

可以使用 Blob 构造函数创建一个 Blob 对象,它接受一个参数 blobParts,其中包含要构建 Blob 的数据。blobParts 可以是一个数组,每个元素代表一个部分数据,也可以是一个包含数据的字符串。

file 对象放进 new Blob() 方法内就能转换为 blob 对象。

blob 对象可以使用 slice 进行切割操作,放进 new File() 方法内就能转换为 file 对象。把切割后的 blob 放进去就能转为新的 file 对象。步骤如下:

  1. 获取文件并转为 blob 对象
  2. 通过 slice() 方法截取
  3. 通过 new File() 把截取后的 blob 转为 files 对象
js
const fileInput = document.getElementById("fileInput");

fileInput.addEventListener("change", () => {
  const selectedFile = fileInput.files[0];

  // 将文件转换为 Blob 对象
  const blob = new Blob([selectedFile], { type: selectedFile.type });

  // 执行其他操作,例如将 Blob 对象上传到服务器
});
vue
<input type="file" @change="change" />

<script>
const change = (e) => {
  let file = e.target.files[0];
  const blob = new Blob([file]);
  const sliceBlob = new Blob([file]).slice(0, 5000); // 切割0到5000位
  const sliceFile = new File([sliceBlob], "text.png");
  console.log(sliceFile);
};
</script>

最后可以把切割好的 sliceFile 调用接口传给后端。

⚠️ 注意

后端直接给前端前端也不认识,需要转换。

formData

FormData 是一个用于构建和处理表单数据的 JavaScript 对象。它可以方便地将表单数据格式化为一个可以通过 XMLHttpRequestFetch API 发送到服务器的数据体,用于和后端传输的对象。

使用 FormData 可以轻松地处理包括文本字段、文件和二进制数据在内的表单数据。搭载 files 对象传递给后端。本质作用是把前端的 file 对象转为 blob 传递给后端。

js
const change = (e) => {
  const formData = new FormData();
  formData.append("user", "content");
  formData.append("file", e.target.file[0]);
  axios.post("/xx", formData);
};

最终查看接口,传递的参数是二进制文件,请求头也是相应的 form-data 类型。

⚠️ 注意

使用 FormData 对象可以方便地处理带有文件和二进制数据的表单,但对于处理只包含文本数据的简单表单,也可以直接将数据格式化为 JSON 对象或查询字符串发送到服务器。具体的处理方式取决于服务器端的要求和你选择使用的前端框架或库。

fileReader

FileReader 是一个用于读取文件内容的 JavaScript 对象。它提供了一些方法,可以异步读取文件数据并以不同的方式进行处理,例如文本、二进制数据、 base64 或数据 URL

js
const fileInput = document.getElementById("fileInput");

fileInput.addEventListener("change", (event) => {
  const selectedFile = event.target.files[0];

  const reader = new FileReader();

  reader.addEventListener("load", (event) => {
    // 读取完成后的回调函数
    const fileData = event.target.result;
    console.log(fileData);
    // 在这里可以对文件数据进行进一步处理
  });

  // 将文件读取为文本
  // reader.readAsText(selectedFile);

  // 将文件读取为二进制数据
  // reader.readAsArrayBuffer(selectedFile);

  // 将文件读取为数据 URL
  // reader.readAsDataURL(selectedFile);

  // 将文件读取为指定长度的字符串
  // reader.readAsBinaryString(selectedFile);
});
方法操作
readAsText()将文件读取为文本。
readAsArrayBuffer()将文件读取为二进制数据。
readAsDataURL()将文件读取为数据 URL。
readAsBinaryString()将文件读取为指定长度的字符串。

每个方法都是异步的,当操作完成时,会触发 load 事件,并将读取的文件数据存储在 event.target.result 中。可以根据需要选择适合的文件读取方法,并在 load 事件处理程序中对读取的文件数据进行进一步处理。例如,可以将文本显示在页面上,将二进制数据发送到服务器,或将数据 URL 用作图片的 src

⚠️ 注意

读取大型文件或者使用错误的读取方法可能会影响性能或导致异常。确保选择正确的方法,并适当处理读取操作的成功或失败。

案例:缩略图

运用前面的前置知识,实现缩略图效果,具体思想步骤如下:

  1. 获取到用户选择的 file 文件
  2. file 转为 blob 格式,目的是为了通过 slice 实现裁剪
  3. 裁剪后通过 new File() 转为 file 对象
  4. 使用 fileReaderreadAsDataURL() 方法获取对应图片的 base64 格式,并赋值给 img 标签的 src 属性
js
fileChange = (e) => {
  const file = e.target.files[0];
  const blob = new Blob([file]);
  const sliceBlob = new Blob([file]).slice(0, 5000); // 切割0到5000位
  const sliceFile = new File([sliceBlob], "text.png");
  const reader = new FileReader();
  reader.readAsDataURL(sliceFile);
  reader.onload = (e) => {
    this.imgSrc = e.target.result;
  };
};
vue
<template>
  <input type="file" @change="fileChange" />
  <img :src="imgSrc" alt="" />
</template>

转换关系

转换关系如下:

  • files 对象前端可直接获取,通过 new Blob() 转为blobblob 也可以通过 new File() 转为 files 对象。filesblob 的子类
  • filesblob 都可用 fileReader 读取为文本和 base64
  • filesblob 都要 appendformdata 内,调用接口传输

关系图

上传方式

单多文件上传

多文件上传就是遍历文件数组,一个个声明 formData 对象,调接口传参。(即循环单文件上传)

步骤如下:

  1. 每次用户选择文件后把文件保存到一个数组内
  2. 点击提交时遍历数组依次 append 添加文件并各自提交

代码如下所示:

js
// 用户选择图片
const onChange = (e) => {
  if (e.target.files.length > 1) {
    imageList.value.concat(e.target.files);
  } else {
    imageList.value.push(e.target.files[0]);
  }
};

// 点击提交按钮
const onSubmit = async () => {
  let _formData = new FormData();
  imageList.value.forEach((item) => {
    _formData.append(item.name + "file", item);
    axios.post("/xx", _formData);
  });
};

切片上传

思路:

设置一个最大值,定义一个变量 current 初始值为 0,上传文件时通过使用 slice(current, current + 设置的允许最大上传的尺寸) 上传文件,上传完后 current 加上当前上传的文件大小即可。

实现步骤如下:

  1. 获取用户选择的文件
  2. 点击提交按钮后获取文件的大小,初始化设置一个变量,保存切片上传的大小,默认为 0
  3. while 循环,只要上传切片的大小没有超过文件总大小,就调用接口传递参数,参数使用 slice 截取大小并累加已上传的文件大小
  4. 通过 asyncawait 把请求变为同步任务,保证上一个切片上传成功再上传下一个切片

代码如下所示:

vue
<script setup>
const fileObj = ref({}); // 文件上传
const precent = ref(0); // 百分比

const onChange = (e) => {
  fileObj.value = e.target.files[0];
};

const onSubmit = async () => {
  let size = 2 * 1024 * 1024;
  let current = 0;
  let fileSize = fileObj.value.size;
  while (current < fileSize) {
    let formData = new FormData();
    formData.append(
      fileObj.value.name,
      fileObj.value.slice(current, current + size)
    );
    // 切片上传
    await axios.post("/upload", formData);
    // 累加文件大小
    current += size;
    // 计算百分比
    precent.value = Math.min((current / fileSize) * 100, 100);
  }
};
</script>

拓展

后端的做法是获取到切片的文件判断名称是否相同,相同则在后面做拼接的操作。

断点续传

思路:

当上传的时候中断了,例如 4mb,把中断的值本地存储,下次上传时从第 4mb 开始传起。

以上方代码示例为例:

vue
<script setup>
const fileObj = ref({}); // 文件上传
const precent = ref(0); // 百分比

const onChange = (e) => {
  fileObj.value = e.target.files[0];
};

const onSubmit = async () => {
  if (!fileObj.value)
    fileObj.value = JSON.parse(localStorage.getItem(fileObj.value.name)).file;

  let size = 2 * 1024 * 1024;
  let current = 0;
  let fileSize = fileObj.value.size;

  while (current < fileSize) {
    let formData = new FormData();
    formData.append(
      fileObj.value.name,
      fileObj.value.slice(current, current + size)
    );
    // 切片上传
    const res = await axios.post("/upload", formData);
    if (res.code === 200) {
      // 累加文件大小
      current += size;
      // 计算百分比
      precent.value = Math.min((current / fileSize) * 100, 100);
    } else {
      // 存储当前文件信息
      localStorage.setItem(
        fileObj.value.name,
        JSON.stringify({ file: fileObj.value, current })
      );
    }
  }
};
</script>

总体效果