在真实的开发中,往往需要后端配合,后续有时间写一个完整的案例看看。

文件切片上传/下载后端接口

  如果使用切片上传,大致有一下几个接口:

  • 上传文件接口(暂时存储前端上传的切片)
  • 检查文件接口(告诉前端当前文件目前上传的状态:已上传切片个数、位置等)
  • 合并文件分片接口(合并操作前端/后端都可以)

# 一、前端文件流

  Blob 对象 和 ArrayBuffer 是处理二进制数据的重要工具。而 FileReader 则是读取文件内容的的关键组件。通过这些技术,我们可以方便的在前端页面上进行操作或者文件展示。

文件流示意图 ✍

# 1、数据进制转换

  在前端处理文件时,经常需要处理二进制数据。Blob(Binary Large Object)对象是用来表示二进制数据的一个接口,可以存储大量的二进制数据。

提示

  要注意的是:Blob 对象是包含有只读原始数据的类文件对象

简单来说,Blob 对象就是一个不可修改的二进制文件

我们可以使用 ArrayBuffer 将 Blob 中的二进制数据转换为目标进制数据

ArrayBuffer
import React, { useState } from 'react'

function FileInput() {
  const [fileContent, setFileContent] = useState('')

  // 读取文件内容到ArrayBuffer
  function readFileToArrayBuffer(file) {
    return new Promise((resolve, reject) => {
      const reader = new FileReader()

      // 注册文件读取完成后的回调函数
      reader.onload = function (event) {
        const arrayBuffer = event.target.result
        resolve(arrayBuffer)
      }

      // 读取文件内容到ArrayBuffer
      reader.readAsArrayBuffer(file)
    })
  }

  // 将ArrayBuffer转为十六进制字符串
  function arrayBufferToHexString(arrayBuffer) {
    const uint8Array = new Uint8Array(arrayBuffer)
    let hexString = ''
    for (let i = 0; i < uint8Array.length; i++) {
      const hex = uint8Array[i].toString(16).padStart(2, '0')
      hexString += hex
    }
    return hexString
  }

  // 处理文件选择事件
  function handleFileChange(event) {
    const file = event.target.files[0] // 获取选中的文件

    if (file) {
      readFileToArrayBuffer(file)
        .then((arrayBuffer) => {
          const hexString = arrayBufferToHexString(arrayBuffer)
          setFileContent(hexString)
        })
        .catch((error) => {
          console.error('文件读取失败:', error)
        })
    } else {
      setFileContent('请选择一个文件')
    }
  }

  return (
    <div>
      <input type="file" onChange={handleFileChange} />
      <div>
        <h4>文件内容:</h4>
        <pre>{fileContent}</pre>
      </div>
    </div>
  )
}

export default FileInput

# 2、数据格式转换

  FileReader 是前端浏览器提供的一个 API,用于读取文件内容。通过 FileReader,我们可以通过异步方式读取文件,并将文件内容转换为可用的数据形式

FileReader
const reader = new FileReader()

reader.readAsDataURL(fileList[0]) // Base64
reader.readAsText(fileList[0]) // 文本
reader.readAsBinaryString(fileList[0]) // 二进制

# 二、文件切片上传

切片上传的优点
  • 防止大文件上请求超时
  • 上传中断不需要重新上传整个文件
  • 可以对上传进度的显示和控制

# 1、切片上传

前端:

  使用 JavaScript 的 File API 获取文件对象,并使用 Blob.prototype.slice() 方法将文件切割为多个切片

后端:

  在后端服务器上接收切片并保存到临时存储中,等待后续合并

const uploadChunk = (chunk) => {
  // 创建FormData对象
  const formData = new FormData()
  formData.append('file', chunk)

  // 发送切片到服务器
  fetch('上传接口xxxx', {
    method: 'POST',
    body: formData
  })
    .then((response) => response.json())
    .then((data) => {
      console.log(data)
      // 处理响应结果
    })
    .catch((error) => {
      console.error(error)
      // 处理错误
    })
}

const upload = () => {
  if (!file) {
    alert('请选择要上传的文件!')
    return
  }

  const chunkSize = 1024 * 1024 // 1MB

  let start = 0
  let end = Math.min(chunkSize, file.size)

  while (start < file.size) {
    const chunk = file.slice(start, end) // 切片 👈

    uploadChunk(chunk) // 切片上传

    start = end
    end = Math.min(start + chunkSize, file.size)
  }
}

# 2、断点续传 🎈

  可以使用 localStorage 或 sessionStorage 来存储已上传的切片信息,包括已上传的切片索引、切片大小等。

  每次上传前,先检查本地存储中是否存在已上传的切片信息,若存在,则从断点处继续上传。

const [uploadedChunks, setUploadedChunks] = useState([])

const upload = async () => {
  if (!file) {
    alert('请选择要上传的文件!')
    return
  }

  const chunkSize = 1024 * 1024 // 1MB

  let start = 0
  let end = Math.min(chunkSize, file.size)

  // while (start < file.size) {
  //   const chunk = file.slice(start, end) // 切片 👈

  //   uploadChunk(chunk) // 切片上传

  //   start = end
  //   end = Math.min(start + chunkSize, file.size)
  // }

  // ======

  setUploading(true)

  const totalChunks = Math.ceil(file.size / chunkSize)

  for (let i = 0; i < totalChunks; i++) {
    const chunk = file.slice(start, end) // 切片 👈

    const uploadedChunkIndex = uploadedChunks.indexOf(i)
    // 有切片就不用重复上传
    if (uploadedChunkIndex === -1) {
      try {
        const response = await uploadChunk(chunk) // 切片上传

        setUploadedChunks((prevChunks) => [...prevChunks, i]) // 记录切片索引
        localStorage.setItem('uploadedChunks', JSON.stringify(uploadedChunks))
      } catch (error) {
        console.error(error)
      }
    }

    start = end
    end = Math.min(start + chunkSize, file.size)
  }

  setUploading(false)
  localStorage.removeItem('uploadedChunks')
}

# 3、上传进度条

  直接使用 axios 现成的 API onUploadProgress

const [progress, setProgress] = useState(0)

function uploadFile() {
  axios
    .post('/upload', formData, {
      onUploadProgress: (progressEvent) => {
        const progress = Math.round((progressEvent.loaded / progressEvent.total) * 100)
        setProgress(progress)
      }
    })
    .then((response) => {
      console.log('文件上传成功:', response.data)
    })
    .catch((error) => {
      console.error('文件上传失败:', error)
    })
}

# 三、文件切片下载

# 1、切片下载

后端:

  服务器端将大文件切割成多个切片,并为每个切片生成唯一的标识符

前端:

  根据切片列表发起并发请求下载其他切片,并逐渐拼接合并下载的数据

function downloadFile() {
  // 发起文件下载请求
  fetch('/download', {
    method: 'GET',
    headers: {
      'Content-Type': 'application/json'
    }
  })
    .then((response) => response.json())
    .then((data) => {
      const totalSize = data.totalSize
      const totalChunks = data.totalChunks

      let downloadedChunks = 0
      let chunks = []

      // 下载每个切片
      for (let chunkNumber = 0; chunkNumber < totalChunks; chunkNumber++) {
        fetch(`/download/${chunkNumber}`, {
          method: 'GET'
        })
          .then((response) => response.blob())
          .then((chunk) => {
            downloadedChunks++
            chunks.push(chunk)

            // 当所有切片都下载完成时
            if (downloadedChunks === totalChunks) {
              // 合并切片
              const mergedBlob = new Blob(chunks)

              // 创建对象 URL,生成下载链接
              const downloadUrl = window.URL.createObjectURL(mergedBlob)

              // 创建 <a> 元素并设置属性
              const link = document.createElement('a')
              link.href = downloadUrl
              link.setAttribute('download', 'file.txt')

              // 模拟点击下载
              link.click()

              // 释放资源
              window.URL.revokeObjectURL(downloadUrl)
            }
          })
      }
    })
    .catch((error) => {
      console.error('文件下载失败:', error)
    })
}

# 2、下载进度条

  通过监听每个切片的下载进度来计算整体下载进度,并实时更新进度条的显示

# 四、多文件上传

渡一

更新于 : 8/7/2024, 2:16:31 PM