先引用掘金上的一個總結,將前端會遇到的文件相關的知識點間的關係串聯了起來。
前端技術提供了一些高效的解決方案:文件流操作和切片下載與上傳。
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