文件流操作
在软件开发中,我们会看到各种形形色色的文件/资源(pdf/word/音频/视频
),其实它们归根到底就是不同数据格式的以满足自身规则的情况下展示。说的更浅显易懂点,它们都是数据,并且最终都会以二进制形式展示。
Blob & ArrayBuffer
在前端处理二进制数据时,有两个对象是绕不开的。
- Blob 对象(
Binary Large Object
)对象是一种可以在 JavaScript
中存储大量二进制数据的对象。可以通过构造函数创建 Blob
对象,或者通过其他 API
(如 FormData 对象)生成。
- ArrayBuffer 是
JavaScript
中的另一种对象类型,它们可以存储二进制数据。ArrayBuffers
通常用于较低级别的操作,如直接操作和处理二进制数据。
区分
互相转换
文件分片 & Hash 计算
无论是分片上传
和分片下载
最核心的点就是需要对文件资源进行分片处理。其中最主要的API 就是 Blob: slice()。
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 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85
|
async function sliceFile(file: File, chunkSizeStr = '10MB') {
const convertToBytes = (sizeStr: string): number => { const symbols = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
const regex = /^(\d+(?:\.\d+)?)\s*([a-zA-Z]+)$/ const matches = sizeStr.trim().match(regex)
if (!matches) { throw new Error('Invalid format. Expected format like "10MB" or "5.5 GB"') }
const value = parseFloat(matches[1]) const inputUnit = matches[2].toLowerCase()
const index = symbols.findIndex( (unit) => unit.toLowerCase() === inputUnit || unit.toLowerCase() === inputUnit + 'b', )
if (index === -1) { throw new Error(`Unsupported unit: ${matches[2]}`) }
return value * 1024 ** index }
return new Promise<{ chunkList: Blob[]; fileHash: string }>((resolve, reject) => { let currentChunk = 0 let fileHash = '' const chunkSize = convertToBytes(chunkSizeStr) const chunks = file && Math.ceil(file.size / chunkSize) const fileReader = new FileReader() const chunkList: Blob[] = [] const spark = new SparkMD5.ArrayBuffer() const blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice
fileReader.onload = (e: ProgressEvent) => { const chunk = (e.target as FileReader).result as ArrayBuffer spark.append(chunk) currentChunk++ if (currentChunk < chunks) { loadNextChunk() } else { fileHash = spark.end() resolve({ chunkList, fileHash }) } }
fileReader.onerror = function (e) { console.warn('读取文件出错', e) reject(e) }
function loadNextChunk() { const start = currentChunk * chunkSize const end = start + chunkSize >= file.size ? file.size : start + chunkSize
const chunk = blobSlice.call(file, start, end) chunkList.push(chunk) fileReader.readAsArrayBuffer(chunk) }
loadNextChunk() }) }
|
分片上传
这里以axios
上传为例,我们将每个分片同时上传,并通过onUploadProgress
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
| async function uploadFile( file: File, chunkSizeStr: string, onProgress: (percentage: number) => void, ) { const { chunkList, fileHash } = await sliceFile(file, chunkSizeStr) const totalChunks = chunkList.length const uploadedBytes: number[] = new Array(totalChunks).fill(0) const uploadPromises = Array.from({ length: totalChunks }, (_, index) => { const formData = new FormData() formData.append('file', chunkList[index]) formData.append('chunkIndex', index.toString()) formData.append('totalChunks', totalChunks.toString()) formData.append('fileHash', fileHash)
return axios.post('/api/upload', formData, { onUploadProgress: (progressEvent: AxiosProgressEvent) => { const currentChunkIndex = index uploadedBytes[currentChunkIndex] = progressEvent.loaded const totalUploaded = uploadedBytes.reduce((sum, bytes) => sum + bytes, 0) const progress = (totalUploaded / file.size) * 100 onProgress(progress) }, }) }) await Promise.all(uploadPromises)
onProgress(100) }
|
分片下载
实现客户端分片下载的基本解决方案如下:
- 服务器端将大文件切割成多个分片,并为每个分片生成唯一标识符。
- 客户端发送请求以获取分片列表并开始下载第一个分片。
- 在下载过程中,客户端基于分片列表发起并发请求以下载其他分片,并逐渐拼接和合并下载的数据。
- 当所有分片下载完成后,客户端将下载的数据合并为一个完整的文件。
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 56 57
| async function downloadable() { try { const response = await fetch('/download', { method: 'GET', headers: { 'Content-Type': 'application/json', }, })
const data = await response.json() const totalSize = data.totalSize const totalChunks = data.totalChunks
let downloadedChunks = 0 const chunks: Blob[] = []
for (let chunkNumber = 0; chunkNumber < totalChunks; chunkNumber++) { try { const chunkResponse = await fetch(`/download/${chunkNumber}`, { method: 'GET', })
const chunk = await chunkResponse.blob() downloadedChunks++ chunks.push(chunk)
if (downloadedChunks === totalChunks) { const mergedBlob = new Blob(chunks)
const downloadUrl = window.URL.createObjectURL(mergedBlob)
const link = document.createElement('a') link.href = downloadUrl link.setAttribute('download', 'file.txt')
link.click()
window.URL.revokeObjectURL(downloadUrl) } } catch (chunkError) { console.error(`Chunk ${chunkNumber} download failed:`, chunkError) } } } catch (error) { console.error('文件下载失败:', error) } }
|
我们先使用 Blob
对象创建一个总对象 URL,用于生成下载连接。然后创建一个标签,并将 href
属性设置为刚创建的对象 URL。继续设置标签的属性以下载文件名,这样在点击时可以自动下载文件。
断点续传
在前端,可以使用localStorage
或sessionStorage
存储已上传分片的信息,包括已上传的分片索引和分片大小。
每次上传前,检查本地存储
中是否存在已上传分片信息。如果存在,则从断点处继续上传。
在后端,可以使用临时文件夹
或数据库记录已接收的分片信息,包括已上传的分片索引和分片大小。
上传完成前,保存上传状态,以便在上传中断时能够恢复上传进度。