Skip to content

前端文件上传与相关操作

作者:Yuan Tang
更新于:5 个月前
字数统计:3k 字
阅读时长:10 分钟

前置知识

上传方案

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

相关对象

files

当用户选择一个或多个文件后,可以使用 JavaScript 来获取 <input type="file"> 元素的文件数据。该元素有一个 files 属性,它返回一个 FileList 对象,其中包含用户选择的文件。

以下是一个示例,演示如何使用 JavaScript 获取文件数据:

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')
  }
})

在这个例子中,我们获取具有 id="fileInput"<input type="file"> 元素,并使用 addEventListener 监听其 change 事件。当用户选择文件时,change 事件被触发,并且可以通过 event.target.files 获取到选中的文件列表。

然后,我们可以遍历选中的文件列表,访问每个文件的属性,例如文件名 (name)、文件类型(type)、文件大小 (size) 等。

一旦获取到文件数据,你可以使用 XMLHttpRequest、Fetch API 或其他网络请求库将文件数据发送给后端。具体的上传方式取决于后端的接收方式和你选择使用的前端框架或库。

注意

  1. 获取到的对象不能直接给后端,因为这个是前端的对象。
  2. file 对象是 blob 对象的一个子类。

blob

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

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

以下是一个示例,演示如何将文件转换为 Blob 对象:

js
const fileInput = document.getElementById('fileInput')

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

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

  // 执行其他操作,例如将 Blob 对象上传到服务器
})

在这个例子中,我们假设有一个 <input type="file"> 元素用于选择文件,监听其 change 事件。当用户选择文件后,我们使用 fileInput.files[0] 获取到用户选择的文件对象,并将其传入 Blob 构造函数。

构造函数的第二个参数是一个可选的配置对象,我们可以在这里设置 Blob 对象的 MIME 类型。在这个例子中,我们将 MIME 类型设置为用户选择文件的类型,以确保 Blob 对象与原始文件具有相同的类型信息。

通过将文件转换为 Blob 对象,你可以对文件数据进行处理、传输或上传到服务器。具体的上传方式取决于后端的要求和你选择使用的前端框架或库。

注意

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

formData

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

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

以下是一个示例,演示如何使用 FormData 来构建表单数据:

js
const form = document.getElementById('myForm')
const formData = new FormData(form)

// 添加额外的字段
formData.append('username', 'John')
formData.append('age', 30)

// 发送表单数据到服务器
fetch('/submit', {
  method: 'POST',
  body: formData,
})
  .then((response) => {
    // 处理响应
  })
  .catch((error) => {
    // 处理错误
  })

在这个例子中,我们假设有一个 id 为 “myForm” 的表单元素。通过创建 FormData 对象时传入这个表单元素,可以将表单中的输入字段和文件添加到 FormData 对象中。

你还可以使用 append() 方法向 FormData 对象添加额外的字段。每个字段都由键值对(key-value pairs)表示,其中键是字段的名称,值是字段的值。

一旦构建了 FormData 对象,你可以使用 XMLHttpRequest 或 Fetch API 将其发送到服务器。在这个示例中,我们使用 Fetch API 发送 POST 请求,并将 formData 对象作为请求的主体(body)。服务器将接收到这些表单数据,并进行相应的处理。

注意

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

fileReader

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

以下是 FileReader 的常见用法示例:

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);
})

在这个例子中,我们假设有一个 <input type="file"> 元素用于选择文件,通过监听其 change 事件,可以在用户选择文件后触发。

change 事件处理程序中,我们使用 event.target.files[0] 获取用户选择的文件对象,并创建一个 FileReader 对象。

然后,我们可以使用 FileReader 对象的不同方法来读取文件的内容。在示例中,我们展示了四种常见的方式:

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

每个方法都是异步的,当操作完成时,会触发 load 事件,并将读取的文件数据存储在 event.target.result 中。

你可以根据需要选择适合的文件读取方法,并在 load 事件处理程序中对读取的文件数据进行进一步处理。例如,可以将文本显示在页面上,将二进制数据发送到服务器,或将数据 URL 用作图片的 src。

注意

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

files对象

下面有一个基础的获取文件代码示例:

vue
<input type="file" @change="change" />

<script>
function change(e) {
  const file = e.target.files[0]
  console.log(file) // 包含文件类型、大小、名称等

  // 判断文件大小
  if (file.size > 1024 * 24 * 24)
    return

  // 判断文件类型
  if (file.type !== 'video/mp4')
    return
}
</script>

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

图片信息

拓展:

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

js
console.log(new File(['content'], 'a.txt'))

最终打印结果如下所示:

图片信息

blob对象

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

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

步骤如下:

  1. 获取文件并转为 blob 对象
  2. 通过 slice() 方法截取
  3. 通过 new File() 把截取后的 blob 转为 files 对象

代码如下:

vue
<input type="file" @change="change" />

<script>
function change(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')
  console.log(sliceFile)
}
</script>

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

fileReader对象

通过 new FileReader 定义一个方法,使用 readAsDateURL 读取文件信息。

注意:

该操作非同步操作,因此需要在他的 onload 事件中等待其解读完。

js
const fr = new FileReader()
fr.readAsDateURL(file)
fr.onload = function () {
  console.log(fr.result)
}

文件缩略图的实现原理就是通过截取 base64 ,再使用 readAsDateURL 把内容显示在页面上(文本预览也是该操作)。

formData对象

本质作用是把前端的 file 对象转为 blob 传递给后端。

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

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

转换关系

转换关系如下:

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

一图流

图片信息

上传方式

单多文件上传

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

步骤如下:

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

代码如下所示:

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

}

// 点击提交按钮
async function onSubmit() {
  const _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) // 百分比

function onChange(e) {
  fileObj.value = e.target.files[0]
}

async function onSubmit() {
  const size = 2 * 1024 * 1024
  let current = 0
  const fileSize = fileObj.value.size
  while (current < fileSize) {
    const 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) // 百分比

function onChange(e) {
  fileObj.value = e.target.files[0]
}

async function onSubmit() {
  const size = 2 * 1024 * 1024
  let current = 0
  const fileSize = fileObj.value.size

  localStorage.setItem(fileObj.value.name, current)

  while (current < fileSize) {
    const 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>

Contributors

Yuan Tang