先引用掘金上的一个总结,将前端会遇到的文件相关的知识点间的关系串联了起来。
前端技术提供了一些高效的解决方案:文件流操作和切片下载与上传。
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