在軟件開發(fā)中,我們會看到各種形形色色的文件/資源(pdf/word/音頻/視頻),其實它們歸根到底就是不同數(shù)據(jù)格式的以滿足自身規(guī)則的情況下展示。說的更淺顯易懂點,它們都是數(shù)據(jù),并且最終都會以二進制形式展示。也就是說,我們的各種操作都是在處理數(shù)據(jù)。那么處理文件也是如此。
在前端開發(fā)中,文件流操作允許我們通過數(shù)據(jù)流來處理文件,執(zhí)行諸如讀取、寫入和刪除文件的操作。
在前端開發(fā)中,文件可以作為數(shù)據(jù)流來處理。數(shù)據(jù)流是從一個源到另一個目的地傳輸?shù)臄?shù)據(jù)序列。
在前端處理二進制數(shù)據(jù)時,有兩個對象是繞不開的。
FileReader 是一個前端瀏覽器 API,允許我們異步讀取文件內(nèi)容并將其轉(zhuǎn)換為可用的數(shù)據(jù)格式,如文本或二進制數(shù)據(jù)。
它提供了如 readAsText()[4] 和 readAsArrayBuffer()[5] 等方法,可以根據(jù)我們的需要進行選擇。
圖片
下面,我們來用一個例子來簡單說明一下FileReader的使用方式。
import { ChangeEvent, useState } from 'react';function FileInput() { // 讀取文件內(nèi)容到 ArrayBuffer function readFileToArrayBuffer(file: File): Promise<ArrayBuffer> { return new Promise((resolve, reject) => { const reader = new FileReader(); // 注冊文件讀取完成后的回調(diào)函數(shù) reader.onload = function (event) { const arrayBuffer = event.target?.result as ArrayBuffer; resolve(arrayBuffer); }; // 讀取文件內(nèi)容到 ArrayBuffer reader.readAsArrayBuffer(file); // 處理文件讀取錯誤 reader.onerror = function (error) { reject(error); }; }); } // 處理文件選擇事件 function handleFileChange(event: ChangeEvent<HTMLInputElement>) { const file = event.target.files?.[0]; // 獲取選擇的文件 if (file) { readFileToArrayBuffer(file) .then((arrayBuffer) => { // 此處已經(jīng)能拿到文件的`arrayBuffer`信息,也就是Blob數(shù)據(jù) }) .catch((error) => { console.error('文件讀取失敗:', error); }); } } return ( <div> <input type="file" notallow={handleFileChange} /> </div> );}export default FileInput;在上面的代碼中,我創(chuàng)建了一個名為 FileInput 的函數(shù)組件。該組件有一個文件選擇框。當用戶選擇一個文件時,文件內(nèi)容會使用 FileReader[6] 讀取到 ArrayBuffer。然后在對應的回調(diào)中就可以處理對應的Blob信息了。
當然,我們這里是利用FileReader的readAsArrayBuffer將文件內(nèi)容轉(zhuǎn)換成(ArrayBuffer)。這樣我們可以更好的進行分片處理(這個后面會講)。其實,我們還可以使用例如readAsDataURL()將資源變成一個url,然后在頁面中顯示。
具體的顯示方法取決于文件類型。例如,可以將文本文件直接顯示在文本框或區(qū)域中,圖片文件使用 img 標簽顯示,音頻和視頻文件使用 audio 或 video 標簽顯示。通過在前端頁面上顯示文件流,可以在線預覽和查看文件內(nèi)容。
const reader = new FileReader();reader.onload = function(event) { // 讀取操作成功完成時執(zhí)行的代碼 const result = event.target.result; console.log('文件內(nèi)容:', result);};const file = ...; // 獲取的文件對象reader.readAsArrayBuffer(file); // 或者使用其他讀取方法當調(diào)用 readAsArrayBuffer, readAsDataURL 或 readAsText 方法時,F(xiàn)ileReader 會開始讀取文件。當讀取操作成功完成后,onload 事件會被觸發(fā),并且 FileReader 對象的 result 屬性包含了讀取到的數(shù)據(jù)。
FileReader 觸發(fā)的事件按以下順序發(fā)生:
下面的示例代碼展示了如何在讀取文件時顯示讀取進度:
document.getElementById('fileInput').addEventListener('change', function(event) { const file = event.target.files[0]; const reader = new FileReader(); // 進度事件 reader.onprogress = function(e) { if (e.lengthComputable) { const percentLoaded = (e.loaded / e.total) * 100; document.getElementById('progressBar').value = percentLoaded; } }; // 定義 onload 事件處理程序 reader.onload = function(e) { const content = e.target.result; document.getElementById('fileContent').textContent = content; }; // 讀取文件為文本 reader.readAsText(file); });其實呢,無論是分片上傳和分片下載最核心的點就是需要對文件資源進行分片處理。
并且有很多現(xiàn)成的庫或者框架都會為我們來實現(xiàn)該部分,但是呢本著探索知識的本質(zhì),我們還是對其內(nèi)部比較核心的部分做一次講解。
在前端范圍內(nèi),我們使用JavaScript中的File API[7]獲取文件對象,并使用Blob.prototype.slice()[8]方法將文件切成多個分片,從而實現(xiàn)分片上傳。
讓我們將第一節(jié)中的代碼在稍加改造。
/** * 將文件讀取為 ArrayBuffer 并分片 * @param file 要讀取的文件 * @returns 返回包含分片 Blob 數(shù)組的 Promise */function readFileToArrayBuffer(file: File): Promise<{ chunkList: Blob[] }> { return new Promise((resolve, reject) => { let currentChunk = 0; // 當前分片的索引 const chunkSize = 1024 * 1024; // 設置分片大小為 1MB const chunks = Math.ceil(file.size / chunkSize); // 計算總分片數(shù) const fileReader = new FileReader(); // 創(chuàng)建 FileReader 對象 const chunkList: Blob[] = []; // 存儲分片的數(shù)組 // 文件讀取完成后的回調(diào)函數(shù) fileReader.onload = function (e) { currentChunk++; // 增加當前分片索引 // 如果還有分片需要讀取,繼續(xù)讀取下一個分片 if (currentChunk < chunks) { loadNextChunk(); } else { // 所有分片讀取完成,resolve Promise 并返回分片數(shù)組 resolve({ chunkList }); } }; // 文件讀取出錯時的回調(diào)函數(shù) fileReader.onerror = function (e) { console.warn('讀取文件出錯', e); reject(e); // reject Promise 并傳遞錯誤信息 }; // 讀取下一個分片的函數(shù) function loadNextChunk() { const start = currentChunk * chunkSize; // 當前分片的起始字節(jié) const end = start + chunkSize >= file.size ? file.size : start + chunkSize; // 當前分片的結(jié)束字節(jié) const chunk = file.slice(start, end); // 切割文件得到當前分片 chunkList.push(chunk); // 將當前分片添加到分片數(shù)組中 fileReader.readAsArrayBuffer(chunk); // 讀取當前分片為 ArrayBuffer } // 開始讀取第一個分片 loadNextChunk(); });}?
當然,在進行文件上傳時,有時候需要用到md5加密等。計算文件的md5是為了檢查上傳到服務器的文件是否與用戶所傳的文件一致,由于行文限制,這里我們不做介紹。(其實在分片完成,就可以執(zhí)行加密處理)
然后,我們就可以在readFileToArrayBuffer的調(diào)用處,獲取到對應文件的分片信息。
function handleFileChange(event: ChangeEvent<HTMLInputElement>) { const file = event.target.files?.[0]; // 獲取選擇的文件 if (file) { readFileToArrayBuffer(file) .then(({ chunkList }) => { for (let i = 0; i < chunkList.length; i++) { const chunk = chunkList[i]; console.log('chunk', chunk); } }) .catch((error) => { console.error('文件讀取失敗:', error); }); } }然后,我們就可以在for循環(huán)中執(zhí)行后續(xù)的操作了。
大文件上傳可能會很慢、效率低并且不可靠,但有一些解決方案可以改善上傳過程的性能和穩(wěn)定性。
傳統(tǒng)上傳方法的問題 | 分片上傳的優(yōu)點 |
大文件上傳耗時長,容易導致超時。 | 將大文件拆分成較小的分片,更快更可靠地上傳。 |
占用服務器和網(wǎng)絡帶寬資源,可能影響其他用戶的訪問速度。 | 監(jiān)控并顯示上傳進度,提高用戶體驗。 |
如果上傳中斷,需要重新上傳整個文件,效率低下。 | 充分利用瀏覽器的并發(fā)上傳能力,減輕服務器負載。 |
難以顯示和控制上傳進度。 | 實現(xiàn)斷點續(xù)傳功能,避免重新上傳已上傳的分片。 |
在前一節(jié)中,我們不是已經(jīng)能夠獲取到chunklist信息了嗎。此時,我們就可以在for循環(huán)中執(zhí)行上傳操作。
而實現(xiàn)前端分片上傳的主要步驟如下
下面,我們主要講講前端范圍的邏輯實現(xiàn)。
readFileToArrayBuffer(file) .then(async ({ chunkList }) => { for (let i = 0; i < chunkList.length; i++) { const chunk = chunkList[i]; await upChunk(chunk, i); } }) .catch((error) => { console.error('文件讀取失敗:', error); });我們將chunk上傳的邏輯,封裝成一個函數(shù)upChunk,其主要的邏輯如下:
/** * 異步上傳文件分片 * * @param chunk - 當前需要上傳的文件分片 (Blob 對象) * @param index - 當前文件分片的索引 */const upChunk = async (chunk: Blob, index: number) => { const formData = new FormData(); // 上傳的唯一標識符,用于區(qū)分不同的文件上傳,前后端約定的值 formData.append('uploadId', 'front789'); formData.append('partIndex', index.toString()); formData.append('partFile', chunk); try { // 發(fā)送 POST 請求上傳當前分片 await axios.post('上傳地址', formData, { headers: { 'Content-Type': 'multipart/form-data' }, onUploadProgress: (progressEvent) => { // 檢查進度事件的總大小是否存在 if (progressEvent.total) { // 計算已上傳的百分比 const percentCompleted = Math.round((progressEvent.loaded / progressEvent.total) * 100); // 在這里添加更新進度條的邏輯 } }, }); } catch (error) { // 如果上傳失敗,打印錯誤信息 console.error(`Chunk ${index + 1} upload failed:`, error); } // 打印分片上傳完成的信息 console.log(`上傳分片 ${index}完成`);};當我們把所有的chunklist都上傳成功后,后端服務會將上傳的分片組裝成完整的文件。
我們使用了axios_onUploadProgress[10]來處理文件上傳進度問題,然后我們可以在特定的位置改變一下state的值,這樣就可以實時顯示文檔上傳進度了。
文件分片下載是一種通過將大文件拆分成較小的片段(分片)并同時下載它們來提高文件下載效率的技術。
問題/技術 | 傳統(tǒng)文件下載 | 文件分片下載 |
長時間等待 | 用戶可能需要等待很長時間才能開始使用大文件 | 只需下載第一個分片,客戶端就可以開始使用文件 |
網(wǎng)絡擁堵 | 如果網(wǎng)絡帶寬被大文件下載占用,其他用戶可能會遇到下載速度慢的問題 | 可以使用多個并行請求來下載分片,充分利用帶寬并提高整體下載速度 |
難以恢復下載 | 如果網(wǎng)絡故障或用戶中斷,整個文件必須重新下載 | 如果下載被中斷,只需重新下載未完成的分片,而不是整個文件 |
下載效率 | 下載速度較慢,特別是在網(wǎng)絡不穩(wěn)定或速度較慢的情況下 | 通過將大文件拆分成較小的片段并同時下載,提高文件下載效率 |
并行下載 | 不支持 | 支持,可以使用多個并行請求來下載分片 |
下載管理 | 整個文件作為一個整體進行下載 | 每個分片可以單獨管理和下載,提供更好的靈活性 |
實現(xiàn)客戶端分片下載的基本解決方案如下:
async function downloadable() { try { // 發(fā)送文件下載請求,獲取文件的總大小和總分片數(shù) const response = await fetch('/download', { method: 'GET', headers: { 'Content-Type': 'application/json', }, }); // 解析響應數(shù)據(jù) 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); // 創(chuàng)建對象 URL 以生成下載鏈接 const downloadUrl = window.URL.createObjectURL(mergedBlob); // 創(chuàng)建一個 <a> 元素并設置屬性 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 對象創(chuàng)建一個總對象 URL,用于生成下載連接。然后創(chuàng)建一個標簽,并將 href 屬性設置為剛創(chuàng)建的對象 URL。繼續(xù)設置標簽的屬性以下載文件名,這樣在點擊時可以自動下載文件。
在前端,可以使用localStorage或sessionStorage存儲已上傳分片的信息,包括已上傳的分片索引和分片大小。
每次上傳前,檢查本地存儲中是否存在已上傳分片信息。如果存在,則從斷點處繼續(xù)上傳。
在后端,可以使用臨時文件夾或數(shù)據(jù)庫記錄已接收的分片信息,包括已上傳的分片索引和分片大小。
上傳完成前,保存上傳狀態(tài),以便在上傳中斷時能夠恢復上傳進度。
import axios from 'axios';import React, { useState, useEffect, ChangeEvent } from 'react';function FileUp() { const [file, setFile] = useState(null); // 本地上傳的文件 const [uploadedChunks, setUploadedChunks] = useState([]); // 已上傳的分片列表 const [uploading, setUploading] = useState(false); // 上傳是否進行中 function handleFileChange(event: ChangeEvent<HTMLInputElement>) { setFile(event.target.files?.[0]); } // 處理文件選擇事件 async function upload() { if (!file) { alert('請選擇要上傳的文件!'); return; } const chunkSize = 1024 * 1024; // 1MB const totalChunks = Math.ceil(file.size / chunkSize); let start = 0; let end = Math.min(chunkSize, file.size); setUploading(true); 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 upChunk(chunk, i); 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'); } const upChunk = async (chunk: Blob, index: number) => { const formData = new FormData(); // 這應該是一個隨機值,用于標識當前上傳的文件,這是和后端做約定的值 formData.append('uploadId', 'front789'); formData.append('partIndex', index.toString()); formData.append('partFile', chunk); try { return await axios.post(`https://Front789/api/uploadChunk`, formData, { headers: { 'Content-Type': 'multipart/form-data' }, }); } catch (error) { console.error(`Chunk ${index + 1} upload failed:`, error); } console.log(`上傳分片 ${index}完成`); }; useEffect(() => { const storedUploadedChunks = localStorage.getItem('uploadedChunks'); if (storedUploadedChunks) { setUploadedChunks(JSON.parse(storedUploadedChunks)); } }, []); return ( <div> <input type="file" onChange={handleFileChange} /> <button onClick={upload} disabled={uploading}> {uploading ? `上傳中..` : '上傳'} </button> </div> );}export default FileUp;該FileUp函數(shù)組件使用React的useState鉤子創(chuàng)建uploadedChunks狀態(tài)來保存已上傳的分片索引數(shù)組。
當用戶選擇要上傳的文件時,handleFileChange()函數(shù)會更file狀態(tài)。
upChunk()函數(shù)將分片發(fā)送到服務器并返回一個Promise對象來處理響應。
upload()函數(shù)通過獲取總分片數(shù)并將uploading狀態(tài)設置為true來禁用上傳按鈕,從斷點處繼續(xù)上傳。它遍歷所有分片并檢查分片索引是否已包含在uploadedChunks數(shù)組中。如果沒有,該函數(shù)會上傳分片并將已上傳的分片索引添加到uploadedChunks數(shù)組中。然后使用localStorage保存已上傳的分片信息。最后,上傳完成后,函數(shù)會將uploading狀態(tài)設置為false并清除本地存儲中的分片信息。
在上傳大文件時,需要考慮服務器的處理能力和存儲空間,以及安全問題。同時,避免并發(fā)上傳相同文件以確保續(xù)傳的準確性。可以使用唯一的文件標識符或用戶會話標識符來區(qū)分。
本文鏈接:http://m.www897cc.com/showinfo-26-98194-0.html聊聊大文件分片上傳和分片下載
聲明:本網(wǎng)頁內(nèi)容旨在傳播知識,若有侵權等問題請及時與本網(wǎng)聯(lián)系,我們將在第一時間刪除處理。郵件:2376512515@qq.com