提示
参考:
在真实的开发中,往往需要后端配合,后续有时间写一个完整的案例看看。
文件切片上传/下载后端接口
如果使用切片上传,大致有一下几个接口:
- 上传文件接口(暂时存储前端上传的切片)
- 检查文件接口(告诉前端当前文件目前上传的状态:已上传切片个数、位置等)
- 合并文件分片接口(合并操作前端/后端都可以)
# 一、前端文件流
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、下载进度条
通过监听每个切片的下载进度来计算整体下载进度,并实时更新进度条的显示
略
# 四、多文件上传
渡一