一文搞懂前端文件上传中的File、FormData、Blob、Base64、ArrayBuffer

一、前置知识

1.什么是 multipart/form-data?

multipart/form-data 最初由 **《RFC 1867: Form-based File Upload in HTML》[1]**文档提出。

Since file-upload is a feature that will benefit many applications, this proposes an extension to HTML to allow information providers to express file upload requests uniformly, and a MIME compatible representation for file upload responses.

由于文件上传功能将使许多应用程序受益,因此建议对 HTML 进行扩展,以允许信息提供者统一表达文件上传请求,并提供文件上传响应的 MIME 兼容表示。

总结就是原先的规范不满足啦,需要扩充规范了

2.文件上传为什么要用 multipart/form-data?

The encoding type application/x-www-form-urlencoded is inefficient for sending large quantities of binary data or text containing non-ASCII characters. Thus, a new media type,multipart/form-data, is proposed as a way of efficiently sending the values associated with a filled-out form from client to server.

1867 文档中也写了为什么要新增一个类型,而不使用旧有的application/x-www-form-urlencoded:因为此类型不适合用于传输大型二进制数据或者包含非 ASCII 字符的数据。平常我们使用这个类型都是把表单数据使用 url 编码后传送给后端,二进制文件当然没办法一起编码进去了。所以multipart/form-data就诞生了,专门用于有效的传输文件。

也许你有疑问?那可以用 application/json吗?

其实我认为,无论你用什么都可以传,只不过会要综合考虑一些因素的话,multipart/form-data更好。例如我们知道了文件是以二进制的形式存在,application/json 是以文本形式进行传输,那么某种意义上我们确实可以将文件转成例如文本形式的 Base64 形式。但是呢,你转成这样的形式,后端也需要按照你这样传输的形式,做特殊的解析。并且文本在传输过程中是相比二进制效率低的,那么对于我们动辄几十 M 几百 M 的文件来说是速度是更慢的。

二、File

  • File 继承自 Blob,File 对象通常由用户通过文件输入控件选择,或者通过拖放操作生成。表示用户在文件输入控件中选择的文件。它是 Blob 的子类,除了 Blob 的所有属性和方法外,还包含文件名、最后修改时间等文件特有的信息

  • 可以通过 <input type="file"> 元素或 Drag and Drop API 获取 File 对象。

    1
    2
    3
    4
    5
    const fileInput = document.querySelector('input[type="file"]')
    fileInput.addEventListener('change', (event) => {
    const file = event.target.files[0]
    console.log(file)
    })

image-20241209164848375

三、FormData

  • FormData 对象用于构建一组键值对,表示表单数据。它通常用于通过 XMLHttpRequestfetch API 发送表单数据,包括文件上传

  • 你可以使用 append 方法将文件添加到 FormData 对象中。

    1
    2
    3
    4
    5
    6
    7
    8
    const formData = new FormData()
    formData.append('file', file)
    // 通过formData.get()的方式获取File
    console.log(formData.get('file'))
    fetch('/upload', {
    method: 'POST',
    body: formData,
    })

    image-20241209164907256FormData.append() 方法用于向 FormData 对象中添加一个键值对。

    1
    formData.append(name, value, filename)
    1. name(字符串):要添加的键的名称。
    2. value(字符串或 Blob 对象):要添加的值。如果是文件,可以是 BlobFile 对象。
    3. filename(可选,字符串):当 valueBlobFile 对象时,可以指定文件名。如果未指定,默认使用 BlobFile 对象的文件名。

    也就是说除了文件上传,使用 formData.append()可以添加普通的表单数据

    1
    2
    3
    4
    const formData = new FormData();
    formData.append('username', 'john_doe');
    formData.append('email', 'john@example.com');
    console.log(formData.get('username'))

    image-20241209164919174

四、Blob

  • Blob (Binary Large Object)对象表示一个不可变的、原始数据的类文件对象。它可以包含文本、二进制数据或其他类型的数据。Blob 对象通常用于处理文件或数据的片段

  • 你可以使用 new Blob() 创建一个 Blob 对象,并指定数据和类型。

    1
    2
    const blob = new Blob(['Hello, world!'], { type: 'text/plain' })
    console.log(blob)
    1
    new Blob(array, options)
    1. array(数组):一个包含 ArrayBufferArrayBufferViewBlobDOMString 等对象的数组,这些对象将会被组合成新的 Blob 对象的数据部分。

    2. options(可选,对象):一个包含属性的对象,用于指定 Blob 的配置选项。

      • type(字符串):表示 Blob 对象的 MIME 类型。默认值是空字符串。

        常见的 MIME 类型有:

        文本类型

        • text/plain:纯文本文件。
        • text/html:HTML 文件。
        • text/css:CSS 文件。
        • text/javascript:JavaScript 文件。

        图像类型

        • image/jpeg:JPEG 图像文件。
        • image/png:PNG 图像文件。
        • image/gif:GIF 图像文件。
        • image/svg+xml:SVG 图像文件。

        音频类型

        • audio/mpeg:MP3 音频文件。
        • audio/wav:WAV 音频文件。
        • audio/ogg:OGG 音频文件。

        视频类型

        • video/mp4:MP4 视频文件。
        • video/webm:WebM 视频文件。
        • video/ogg:OGG 视频文件。

        应用程序类型

        • application/json:JSON 数据文件。
        • application/xml:XML 数据文件。
        • application/pdf:PDF 文件。
        • application/zip:ZIP 压缩文件。
        • application/octet-stream:二进制数据文件(默认值)。
      • endings(字符串):表示换行符的结尾类型,可以是 "transparent""native"。默认值是 "transparent"

        创建一个简单的文本 Blob

        1
        2
        const blob = new Blob(['Hello, world!'], { type: 'text/plain' });
        console.log(blob);

        image-20241209164929959

创建一个包含二进制数据的 Blob

1
2
3
4
5
const arrayBuffer = new ArrayBuffer(8)
const view = new Uint8Array(arrayBuffer)
view[0] = 255
const blob = new Blob([view], { type: 'application/octet-stream' })
console.log(blob)

image-20241209164939501

五、Base64

  • Base64 是一种将二进制数据编码为 ASCII 字符串的方法。它通常用于在 URL、JSON 或 XML 中嵌入二进制数据。

  • 你可以使用 FileReader 对象将文件转换为 Base64 编码字符串。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="UTF-8" />
    <title>文件上传并转换为 Base64 链接</title>
    </head>
    <body>
    <input type="file" />
    <a id="downloadLink" href="#" target="_blank">点击这里打开文件</a>
    <script>
    const fileInput = document.querySelector('input[type="file"]')
    const downloadLink = document.getElementById('downloadLink')

    fileInput.addEventListener('change', (event) => {
    const file = event.target.files[0]
    const reader = new FileReader()

    reader.onload = () => {
    const base64String = reader.result
    console.log(base64String)
    downloadLink.href = base64String // 将 Base64 字符串设置为链接的 href
    downloadLink.download = file.name // 设置下载文件的默认名称
    downloadLink.textContent = `点击这里打开 ${file.name}` // 更新链接文本
    }

    reader.readAsDataURL(file)
    })
    </script>
    </body>
    </html>

    当文件是 txt 文本时

    image-20241209164950088

    当文件是图片时

    image-20241209164957297

    当文件是 excel 时

    image-20241209165006677

    FileReader 是一个内置的 JavaScript 对象,用于读取 BlobFile 对象的内容

    • onloadFileReader 对象的一个事件处理函数,当读取操作完成时会被触发。

    • onload 事件处理函数中,reader.result 包含了读取到的文件内容。

    • reader 在调用 readAsDataURL 方法时,会返回一个 Base64 编码的字符串,表示文件的内容。它是一个该文件的链接,可以直接将其给 a 标签的 href 属性进行文件下载,或者如果文件是图片给到 img 标签的 src 属性进行图片预览

    • reader.result 赋值给 base64String 变量,并输出到控制台。

      除了readAsDataURL,reader 还有其他的 API

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      <!DOCTYPE html>
      <html lang="en">
      <head>
      <meta charset="UTF-8" />
      <title>FileReader 示例</title>
      </head>
      <body>
      <input type="file" id="fileInput" />
      <script>
      const fileInput = document.getElementById('fileInput');

      fileInput.addEventListener('change', (event) => {
      const file = event.target.files[0];
      const reader = new FileReader();

      // 使用 readAsDataURL 读取文件
      reader.onload = () => {
      console.log('Data URL:', reader.result);
      };
      reader.readAsDataURL(file);

      // 使用 readAsArrayBuffer 读取文件
      reader.onload = () => {
      console.log('ArrayBuffer:', reader.result);
      };
      reader.readAsArrayBuffer(file);

      // 使用 readAsBinaryString 读取文件
      reader.onload = () => {
      console.log('Binary String:', reader.result);
      };
      reader.readAsBinaryString(file);

      // 使用 readAsText 读取文件
      reader.onload = () => {
      console.log('Text:', reader.result);
      };
      reader.readAsText(file, 'UTF-8');
      });
      </script>
      </body>
      </html>
      1. **readAsDataURL(file)**:
        • 将文件读取为 Base64 编码的 Data URL 字符串。
      2. **readAsArrayBuffer(file)**:
        • 将文件读取为 ArrayBuffer 对象,适用于处理二进制数据。
      3. **readAsBinaryString(file)**:
        • 将文件读取为二进制字符串(不推荐使用,因为 ArrayBuffer 更加现代和高效)。
      4. **readAsText(file, [encoding])**:
        • 将文件读取为文本字符串,第二个参数可选,用于指定文本编码(默认是 UTF-8)。

      有一个跟**readAsDataURL(file)**很像的 API 叫URL.createObjectURL

      URL.createObjectURLFileReader.readAsDataURL 都可以用于将文件转换为可以在网页中使用的 URL,但它们的工作方式和用途有所不同。

      URL.createObjectURL 方法创建一个包含文件数据的临时 URL。这个 URL 可以直接用于 <img><a> 等 HTML 元素。这个 URL 是浏览器内存中的一个引用,不会将文件数据编码为 Base64。

      特点:

      • 更高效,因为它不需要将文件数据编码为 Base64。
      • 适用于大文件,因为它不会占用额外的内存。
      • 创建的 URL 是临时的,需要手动调用URL.revokeObjectURL 释放内存

      FileReader.readAsDataURL 方法将文件读取为 Base64 编码的 Data URL。这个 Data URL 可以直接用于 <img><a> 等 HTML 元素。这个方法会将文件数据编码为 Base64,因此会占用更多的内存。

      优点:

      • 生成的 URL 可以在任何地方使用,不依赖于浏览器的内存管理。
      • 适用于需要将文件数据嵌入到 HTML 或 JSON 中的场景。
      • FileReader.readAsDataURL 创建的 Data URL 是永久的,不需要手动释放

六、ArrayBuffer

  • ArrayBuffer 对象表示一个通用的、固定长度的原始二进制数据缓冲区。它可以用来处理文件的二进制数据。

  • 你可以使用 FileReader 对象将文件读取为 ArrayBuffer

    1
    2
    3
    4
    5
    6
    const reader = new FileReader()
    reader.onload = () => {
    const arrayBuffer = reader.result
    console.log(arrayBuffer)
    }
    reader.readAsArrayBuffer(file)

七、总结

对于浏览器的文件上传,一般都是构建出 File/Blob 对象,再将其封装进 FormData 对象进行上传。而对于文件下载

若后端返回的数据是 Base64 格式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
// 下载文件
const handleDownload = async (index, row) => {
try {
// 调用下载附件的接口,传递文件名处理器
const { data } = await downloadAttachments({
fileNameHandler: row.filenameHandler || row.appendHandler,
})

// 将 Base64 编码的文件内容解码为二进制字符串。atob 代表 "ASCII to Binary。window.atob() 只能解码 Base64 编码的字符串。window.btoa() 是 window.atob() 的逆函数,用于将二进制数据字符串编码
const binaryStr = window.atob(data.fileContent)

// 创建一个新的 ArrayBuffer,长度为二进制字符串的长度
const ab = new ArrayBuffer(binaryStr.length)

// 创建一个新的 Uint8Array 视图,指向 ArrayBuffer
const ia = new Uint8Array(ab)

// 将二进制字符串的每个字符的 Unicode 编码值存储到 Uint8Array 中
for (let i = 0; i < binaryStr.length; i++) {
ia[i] = binaryStr.charCodeAt(i)
}

// 创建一个新的 Blob 对象,包含 Uint8Array 的数据
const blob = new Blob([ia])

// 获取下载文件的名称
const fileName = row.filename

// 创建一个隐藏的 <a> 元素,用于触发下载
const alink = document.createElement('a')
alink.download = fileName
alink.style.display = 'none'

// 创建一个指向 Blob 对象的 URL,并将其设置为 <a> 元素的 href 属性
alink.href = URL.createObjectURL(blob)

// 将 <a> 元素添加到文档中
document.body.appendChild(alink)

// 触发 <a> 元素的点击事件,开始下载文件
alink.click()

// 释放 URL 对象
URL.revokeObjectURL(alink.href)

// 从文档中移除 <a> 元素
document.body.removeChild(alink)

// 显示下载成功的消息
ElMessage.success('下载成功')
} catch {
// 显示下载失败的消息
ElMessage.error('下载失败')
}
}

虽然后端返回的 Base64 可以直接作为 a 标签的 href 属性进行下载,但在实际开发中却并不经常这样做,主要是因为通用性问题:BlobURL.createObjectURL 支持所有类型的文件,而 Base64 编码的 Data URL 可能在某些浏览器或文件类型上存在兼容性问题

若后端返回的是 File,则可以直接用它创建一个下载链接

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 假设 downloadFile 是一个返回 File 对象的函数
const handleDownload = async () => {
try {
const file = await downloadFile() // 获取 File 对象

// 创建一个隐藏的 <a> 元素,用于触发下载
const alink = document.createElement('a')
alink.download = file.name // 设置下载文件的名称
alink.style.display = 'none'

// 创建一个指向 File 对象的 URL,并将其设置为 <a> 元素的 href 属性
alink.href = URL.createObjectURL(file)

// 将 <a> 元素添加到文档中
document.body.appendChild(alink)

// 触发 <a> 元素的点击事件,开始下载文件
alink.click()

// 释放 URL 对象
URL.revokeObjectURL(alink.href)

// 从文档中移除 <a> 元素
document.body.removeChild(alink)

console.log('下载成功')
} catch (error) {
console.error('下载失败', error)
}
}

若后端返回的是二进制数据(例如 ArrayBufferBlob),可以先将其转化为 Blob 对象,再创建下载链接

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 假设 downloadBinaryData 是一个返回 ArrayBuffer 或 Blob 的函数
const handleDownload = async () => {
try {
const response = await downloadBinaryData() // 获取二进制数据
const blob = new Blob([response], { type: 'application/octet-stream' }) // 创建 Blob 对象

// 创建一个隐藏的 <a> 元素,用于触发下载
const alink = document.createElement('a')
alink.download = 'downloaded_file' // 设置下载文件的名称
alink.style.display = 'none'

// 创建一个指向 Blob 对象的 URL,并将其设置为 <a> 元素的 href 属性
alink.href = URL.createObjectURL(blob)

// 将 <a> 元素添加到文档中
document.body.appendChild(alink)

// 触发 <a> 元素的点击事件,开始下载文件
alink.click()

// 释放 URL 对象
URL.revokeObjectURL(alink.href)

// 从文档中移除 <a> 元素
document.body.removeChild(alink)

console.log('下载成功')
} catch (error) {
console.error('下载失败', error)
}
}
  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2023-2025 congtianfeng
  • 访问人数: | 浏览次数: