提示
参考:
在真实的开发中,往往需要后端配合,后续有时间写一个完整的案例看看。
文件切片上传/下载后端接口
如果使用切片上传,大致有一下几个接口:
- 上传文件接口(暂时存储前端上传的切片)
- 检查文件接口(告诉前端当前文件目前上传的状态:已上传切片个数、位置等)
- 合并文件分片接口(合并操作前端/后端都可以)
# 一、前端文件流
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
Copied!
# 2、数据格式转换
FileReader 是前端浏览器提供的一个 API,用于读取文件内容。通过 FileReader,我们可以通过异步方式读取文件,并将文件内容转换为可用的数据形式
FileReader
const reader = new FileReader() reader.readAsDataURL(fileList[0]) // Base64 reader.readAsText(fileList[0]) // 文本 reader.readAsBinaryString(fileList[0]) // 二进制
Copied!
# 二、文件切片上传
切片上传的优点
- 防止大文件上请求超时
- 上传中断不需要重新上传整个文件
- 可以对上传进度的显示和控制
# 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) } }
Copied!
# 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') }
Copied!
# 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) }) }
Copied!
# 三、文件切片下载
# 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) }) }
Copied!
# 2、下载进度条
通过监听每个切片的下载进度来计算整体下载进度,并实时更新进度条的显示
略
# 四、多文件上传
渡一