前端文件相关总结
先引用掘金上的一个总结,将前端会遇到的文件相关的知识点间的关系串联了起来。
前端技术提供了一些高效的解决方案:文件流操作和切片下载与上传。
1. 文件基本操作
1.1 数据流和文件处理的基本概念
数据流是指连续的数据序列
,可以从一个源传输到另一个目的地。在前端开发中,文件可以被看作数据流的一种形式,可以通过数据流的方式进行处理。 文件处理涉及读取和写入文件的操作,包括读取文件的内容、写入数据到文件,以及对文件进行删除、重命名等操作。
1.2. Blob 对象和 ArrayBuffer:处理二进制数据
在前端处理文件时,经常需要处理二进制数据。Blob
(Binary Large Object)对象是用来表示二进制数据的一个接口,可以存储大量的二进制数据。Blob 对象可以通过构造函数进行创建,也可以通过其他 API 生成,例如通过 FormData 对象获取上传的文件。 而 ArrayBuffer 是 JavaScript 中的一个对象类型,用于表示一个通用的、固定长度的二进制数据缓冲区。我们可以通过 ArrayBuffer
来操作和处理文件的二进制数据。
示例代码:
1 import React, { useState } from 'react'; 2 3 function FileInput() { 4 const [fileContent, setFileContent] = useState(''); 5 6 // 读取文件内容到ArrayBuffer 7 function readFileToArrayBuffer(file) { 8 return new Promise((resolve, reject) => { 9 const reader = new FileReader(); 10 11 // 注册文件读取完成后的回调函数 12 reader.onload = function(event) { 13 const arrayBuffer = event.target.result; 14 resolve(arrayBuffer); 15 }; 16 17 // 读取文件内容到ArrayBuffer 18 reader.readAsArrayBuffer(file); 19 }); 20 } 21 22 // 将ArrayBuffer转为十六进制字符串 23 function arrayBufferToHexString(arrayBuffer) { 24 const uint8Array = new Uint8Array(arrayBuffer); 25 let hexString = ''; 26 for (let i = 0; i < uint8Array.length; i++) { 27 const hex = uint8Array[i].toString(16).padStart(2, '0'); 28 hexString += hex; 29 } 30 return hexString; 31 } 32 33 // 处理文件选择事件 34 function handleFileChange(event) { 35 const file = event.target.files[0]; // 获取选中的文件 36 37 if (file) { 38 readFileToArrayBuffer(file) 39 .then(arrayBuffer => { 40 const hexString = arrayBufferToHexString(arrayBuffer); 41 setFileContent(hexString); 42 }) 43 .catch(error => { 44 console.error('文件读取失败:', error); 45 }); 46 } else { 47 setFileContent('请选择一个文件'); 48 } 49 } 50 51 return ( 52 <div> 53 <input type="file" onChange={handleFileChange} /> 54 <div> 55 <h4>文件内容:</h4> 56 <pre>{fileContent}</pre> 57 </div> 58 </div> 59 ); 60 } 61 62 export default FileInput;
1.3 使用 FileReader 进行文件读取
FileReader
是前端浏览器提供的一个 API,用于读取文件内容。通过 FileReader,我们可以通过异步方式读取文件,并将文件内容转换为可用的数据形式,比如文本数据或二进制数据。 FileReader 提供了一些读取文件的方法,例如 readAsText()、readAsArrayBuffer()
等,可以根据需要选择合适的方法来读取文件内容。
1.4 将文件流展示在前端页面中
一旦我们成功地读取了文件的内容,就可以将文件流展示在前端页面上。具体的展示方式取决于文件的类型。例如,对于文本文件,可以直接将其内容显示在页面的文本框或区域中;对于图片文件,可以使用 <img>
标签展示图片;对于音视频文件,可以使用 <video>
或 <audio>
标签来播放。 通过将文件流展示在前端页面上,我们可以实现在线预览和查看文件内容的功能。
好的,这一部分就基本介绍完毕,总结一下。前端文件操作流是处理大型文件
的一种常见方式,他可以通过数据流的方式对文件进行操作。Blob
对象 和 ArrayBuffer
是处理二进制数据的重要工具。而FileReader
则是读取文件内容的的关键组件。通过这些技术,我们可以方便的在前端页面上进行操作或者文件展示。
2. 文件切片
流程:
A(开始) --> B{选择文件}
B -- 用户选择文件 --> C[切割文件为多个切片]
C --> D{上传切片}
D -- 上传完成 --> E[合并切片为完整文件]
E -- 文件合并完成 --> F(上传成功)
D -- 上传中断 --> G{保存上传进度}
G -- 上传恢复 --> D
G -- 取消上传 --> H(上传取消)
传统文件整体上传下载的弊端:
等待较长、网络占用、续传困难。
切片上传下载好处:
- 快速启动:客户端可以快速开始下载,因为只需要下载第一个切片即可。
- 并发下载:通过使用多个并发请求下载切片,可以充分利用带宽,并提高整体下载速度。
- 断点续传:如果下载中断,客户端只需要重新下载中断的切片,而不需要重新下载整个文件。
2.1 切片上传-借助FormData
1 const [selectedFile, setSelectedFile] = useState(null); 2 const [progress, setProgress] = useState(0); 3 // 处理文件选择事件 4 function handleFileChange(event) { 5 setSelectedFile(event.target.files[0]); 6 } 7 8 // 处理文件上传事件 9 function handleFileUpload() { 10 if (selectedFile) { 11 // 计算切片数量和每个切片的大小 12 const fileSize = selectedFile.size; 13 const chunkSize = 1024 * 1024; // 设置切片大小为1MB 14 const totalChunks = Math.ceil(fileSize / chunkSize); 15 16 // 创建FormData对象,并添加文件信息 17 const formData = new FormData(); 18 formData.append('file', selectedFile); 19 formData.append('totalChunks', totalChunks); 20 21 // 循环上传切片 22 for (let chunkNumber = 0; chunkNumber < totalChunks; chunkNumber++) { 23 const start = chunkNumber * chunkSize; 24 const end = Math.min(start + chunkSize, fileSize); 25 const chunk = selectedFile.slice(start, end); 26 formData.append(`chunk-${chunkNumber}`, chunk, selectedFile.name); 27 } 28 29 // 发起文件上传请求 30 axios.post('/upload', formData, { 31 onUploadProgress: progressEvent => { 32 const progress = Math.round((progressEvent.loaded / progressEvent.total) * 100); 33 setProgress(progress); 34 } 35 }) 36 .then(response => { 37 console.log('文件上传成功:', response.data); 38 }) 39 .catch(error => { 40 console.error('文件上传失败:', error); 41 }); 42 } 43 }
2.2 切片下载
实现客户端切片下载的基本方案如下:
- 服务器端将大文件切割成多个切片,并为每个切片生成唯一的标识符。
- 客户端发送请求获取切片列表,同时开始下载第一个切片。
- 客户端在下载过程中,根据切片列表发起并发请求下载其他切片,并逐渐拼接合并下载的数据。
- 当所有切片都下载完成后,客户端借助blob将下载的数据合并为完整的文件。
1 function downloadFile() { 2 // 发起文件下载请求 3 fetch('/download', { 4 method: 'GET', 5 headers: { 6 'Content-Type': 'application/json', 7 }, 8 }) 9 .then(response => response.json()) 10 .then(data => { 11 const totalSize = data.totalSize; 12 const totalChunks = data.totalChunks; 13 14 let downloadedChunks = 0; 15 let chunks = []; 16 17 // 下载每个切片 18 for (let chunkNumber = 0; chunkNumber < totalChunks; chunkNumber++) { 19 fetch(`/download/${chunkNumber}`, { 20 method: 'GET', 21 }) 22 .then(response => response.blob()) 23 .then(chunk => { 24 downloadedChunks++; 25 chunks.push(chunk); 26 27 // 当所有切片都下载完成时 28 if (downloadedChunks === totalChunks) { 29 // 合并切片 30 const mergedBlob = new Blob(chunks); 31 32 // 创建对象 URL,生成下载链接 33 const downloadUrl = window.URL.createObjectURL(mergedBlob); 34 35 // 创建 <a> 元素并设置属性 36 const link = document.createElement('a'); 37 link.href = downloadUrl; 38 link.setAttribute('download', 'file.txt'); 39 40 // 模拟点击下载 41 link.click(); 42 43 // 释放资源 44 window.URL.revokeObjectURL(downloadUrl); 45 } 46 }); 47 } 48 }) 49 .catch(error => { 50 console.error('文件下载失败:', error); 51 }); 52 }
2.3 显示下载进度和完成状态
为了显示下载进度和完成状态,可以在客户端实现以下功能:
- 显示进度条:客户端可以通过监听每个切片的下载进度来计算整体下载进度,并实时更新进度条的显示。
- 显示完成状态:当所有切片都下载完成后,客户端可以显示下载完成的状态,例如显示一个完成的图标或者文本。
这里我们可以继续接着切片上传代码示例
里的继续写。
- 当用户点击下载按钮时,通过
handleFileDownload
函数处理文件下载事件。 - 在
handleFileDownload
函数中,使用axios
库发起文件下载请求,并设置responseType: 'blob'
表示返回二进制数据。 - 通过监听
onDownloadProgress
属性获取下载进度,并更新进度条的显示。 - 下载完成后,创建一个临时的 URL 对象用于下载,并通过动态创建
<a>
元素模拟点击下载。
1 function handleFileDownload() { 2 axios.get('/download', { 3 responseType: 'blob', 4 onDownloadProgress: progressEvent => { 5 const progress = Math.round((progressEvent.loaded / progressEvent.total) * 100); 6 setProgress(progress); 7 } 8 }) 9 .then(response => { 10 // 创建一个临时的URL对象用于下载 11 const url = window.URL.createObjectURL(new Blob([response.data])); 12 const link = document.createElement('a'); 13 link.href = url; 14 link.setAttribute('download', 'file.txt'); 15 document.body.appendChild(link); 16 link.click(); 17 document.body.removeChild(link); 18 }) 19 .catch(error => { 20 console.error('文件下载失败:', error); 21 }); 22 } 23 24 25 <button onClick={handleFileDownload}>下载文件</button> 26 <div>进度:{progress}%</div>
3. 大文件上传下载
3.1 大文件切片上传
- 使用 JavaScript 的 `File API` 获取文件对象,并使用 `Blob.prototype.slice()` 方法将文件切割为多个切片。
- 使用 FormData 对象将切片数据通过 AJAX 或 Fetch API 发送到服务器。
- 在后端服务器上接收切片并保存到临时存储中,等待后续合并。
- 在客户端通过监听上传进度事件,在进度条或提示中展示上传进度。 代码示例
1 const [file, setFile] = useState(null); //用来存放我本地上传的文件 2 3 const chunkSize = 1024 * 1024; // 1MB 切片大小 4 5 const upload = () => { 6 if (!file) { 7 alert("请选择要上传的文件!"); 8 return; 9 } 10 11 const chunkSize = 1024 * 1024; // 1MB 12 13 let start = 0; 14 let end = Math.min(chunkSize, file.size); 15 16 while (start < file.size) { 17 const chunk = file.slice(start, end); 18 19 // 创建FormData对象 20 const formData = new FormData(); 21 formData.append('file', chunk); 22 23 // 发送切片到服务器 24 fetch('上传接口xxxx', { 25 method: 'POST', 26 body: formData 27 }) 28 .then(response => response.json()) 29 .then(data => { 30 console.log(data); 31 // 处理响应结果 32 }) 33 .catch(error => { 34 console.error(error); 35 // 处理错误 36 }); 37 38 start = end; 39 end = Math.min(start + chunkSize, file.size); 40 } 41 }; 42 43 return ( 44 <div> 45 <input type="file" onChange={handleFileChange} /> 46 <button onClick={upload}>上传</button> 47 </div> 48 ); 49 }
在上面的代码中,创建了一个名为Upload
的函数组件。它使用了 React 的useState
钩子来管理选中的文件。
通过onChange
事件监听文件输入框的变化,并在handleFileChange
函数中获取选择的文件,并更新file
状态。
点击“上传”按钮时,调用upload
函数。它与之前的示例代码类似,将文件切割为多个大小相等的切片,并使用FormData
对象和fetch
函数发送切片数据到服务器。
3.2 实现断点续传的技术:记录和恢复上传状态
- 在前端,可以使用
localStorage
或sessionStorage
来存储已上传的切片信息,包括已上传的切片索引、切片大小等。 - 每次上传前,先检查本地存储中是否存在已上传的切片信息,若存在,则从断点处继续上传。
- 在后端,可以使用一个临时文件夹或数据库来记录已接收到的切片信息,包括已上传的切片索引、切片大小等。
- 在上传完成前,保存上传状态,以便在上传中断后能够恢复上传进度
1 import React, { useState, useRef, useEffect } from 'react'; 2 3 function Upload() { 4 const [file, setFile] = useState(null); 5 const [uploadedChunks, setUploadedChunks] = useState([]); 6 const [uploading, setUploading] = useState(false); 7 const uploadRequestRef = useRef(); 8 9 const handleFileChange = (event) => { 10 const selectedFile = event.target.files[0]; 11 setFile(selectedFile); 12 }; 13 14 const uploadChunk = (chunk) => { 15 // 创建FormData对象 16 const formData = new FormData(); 17 formData.append('file', chunk); 18 19 // 发送切片到服务器 20 return fetch('your-upload-url', { 21 method: 'POST', 22 body: formData 23 }) 24 .then(response => response.json()) 25 .then(data => { 26 console.log(data); 27 // 处理响应结果 28 return data; 29 }); 30 }; 31 32 const upload = async () => { 33 if (!file) { 34 alert("请选择要上传的文件!"); 35 return; 36 } 37 38 const chunkSize = 1024 * 1024; // 1MB 39 const totalChunks = Math.ceil(file.size / chunkSize); 40 41 let start = 0; 42 let end = Math.min(chunkSize, file.size); 43 44 setUploading(true); 45 46 for (let i = 0; i < totalChunks; i++) { 47 const chunk = file.slice(start, end); 48 const uploadedChunkIndex = uploadedChunks.indexOf(i); 49 50 if (uploadedChunkIndex === -1) { 51 try { 52 const response = await uploadChunk(chunk); 53 setUploadedChunks((prevChunks) => [...prevChunks, i]); 54 55 // 保存已上传的切片信息到本地存储 56 localStorage.setItem('uploadedChunks', JSON.stringify(uploadedChunks)); 57 } catch (error) { 58 console.error(error); 59 // 处理错误 60 } 61 } 62 63 start = end; 64 end = Math.min(start + chunkSize, file.size); 65 } 66 67 setUploading(false); 68 69 // 上传完毕,清除本地存储的切片信息 70 localStorage.removeItem('uploadedChunks'); 71 }; 72 73 useEffect(() => { 74 const storedUploadedChunks = localStorage.getItem('uploadedChunks'); 75 76 if (storedUploadedChunks) { 77 setUploadedChunks(JSON.parse(storedUploadedChunks)); 78 } 79 }, []); 80 81 return ( 82 <div> 83 <input type="file" onChange={handleFileChange} /> 84 <button onClick={upload} disabled={uploading}> 85 {uploading ? '上传中...' : '上传'} 86 </button> 87 </div> 88 ); 89 }
首先,使用useState
钩子创建了一个uploadedChunks
状态来保存已上传的切片索引数组。初始值为空数组。
然后,我们使用useRef
钩子创建了一个uploadRequestRef
引用,用于存储当前的上传请求。
在handleFileChange
函数中,我们更新了file
状态以选择要上传的文件。
在uploadChunk
函数中,我们发送切片到服务器,并返回一个Promise
对象来处理响应结果。
在upload
函数中,我们添加了断点续传的逻辑。首先,我们获取切片的总数,并设置uploading
状态为true
来禁用上传按钮。
然后,我们使用for
循环遍历所有切片。对于每个切片,我们检查uploadedChunks
数组中是否已经包含该索引,如果不包含,则进行上传操作。
在上传切片之后,我们将已上传的切片索引添加到uploadedChunks
数组,并使用localStorage
保存已上传的切片信息。
最后,在上传完毕后,我们将uploading
状态设为false
,并清除本地存储的切片信息。
本文参考掘金:https://juejin.cn/post/7255189826226602045