JavaScript實現(xiàn)下載超大文件的方法詳解
本文從前端方面出發(fā)實現(xiàn)瀏覽器下載大文件的功能。不考慮網(wǎng)絡(luò)異常、關(guān)閉網(wǎng)頁等原因造成傳輸中斷的情況。分片下載采用串行方式(并行下載需要對切片計算hash,比對hash,丟失重傳,合并chunks的時候需要按順序合并等,很麻煩。對傳輸速度有追求的,并且在帶寬允許的情況下可以做并行分片下載)。
測試發(fā)現(xiàn)存一兩個G左右數(shù)據(jù)到IndexedDB后,瀏覽器確實會內(nèi)存占用過高導(dǎo)致退出 (我測試使用的是chrome103版本瀏覽器)
實現(xiàn)步驟
- 使用分片下載: 將大文件分割成多個小塊進行下載,可以降低內(nèi)存占用和網(wǎng)絡(luò)傳輸中斷的風(fēng)險。這樣可以避免一次性下載整個大文件造成的性能問題。
- 斷點續(xù)傳: 實現(xiàn)斷點續(xù)傳功能,即在下載中途中斷后,可以從已下載的部分繼續(xù)下載,而不需要重新下載整個文件。
- 進度條顯示: 在頁面上展示下載進度,讓用戶清晰地看到文件下載的進度。如果一次全部下載可以從process中直接拿到參數(shù)計算得出(很精細),如果是分片下載,也是計算已下載的和總大小,只不過已下載的會成片成片的增加(不是很精細)。
- 取消下載和暫停下載功能: 提供取消下載和暫停下載的按鈕,讓用戶可以根據(jù)需要中止或暫停下載過程。
- 合并文件: 下載完成后,將所有分片文件合并成一個完整的文件。
以下是一個基本的前端大文件下載的實現(xiàn)示例:
可以在類里面增加注入一個回調(diào)函數(shù),用來更新外部的一些狀態(tài),示例中只展示下載完成后的回調(diào)
class FileDownloader { constructor({url, fileName, chunkSize = 2 * 1024 * 1024, cb}) { this.url = url; this.fileName = fileName; this.chunkSize = chunkSize; this.fileSize = 0; this.totalChunks = 0; this.currentChunk = 0; this.downloadedSize = 0; this.chunks = []; this.abortController = new AbortController(); this.paused = false; this.cb = cb } async getFileSize() { const response = await fetch(this.url, { signal: this.abortController.signal }); const contentLength = response.headers.get("content-length"); this.fileSize = parseInt(contentLength); this.totalChunks = Math.ceil(this.fileSize / this.chunkSize); } async downloadChunk(chunkIndex) { const start = chunkIndex * this.chunkSize; const end = Math.min(this.fileSize, (chunkIndex + 1) * this.chunkSize - 1); const response = await fetch(this.url, { headers: { Range: `bytes=${start}-${end}` }, signal: this.abortController.signal }); const blob = await response.blob(); this.chunks[chunkIndex] = blob; this.downloadedSize += blob.size; if (!this.paused && this.currentChunk < this.totalChunks - 1) { this.currentChunk++; this.downloadChunk(this.currentChunk); } else if (this.currentChunk === this.totalChunks - 1) { this.mergeChunks(); } } async startDownload() { if (this.chunks.length === 0) { await this.getFileSize(); } this.downloadChunk(this.currentChunk); } pauseDownload() { this.paused = true; } resumeDownload() { this.paused = false; this.downloadChunk(this.currentChunk); } cancelDownload() { this.abortController.abort(); this.reset(); } async mergeChunks() { const blob = new Blob(this.chunks, { type: "application/octet-stream" }); const url = window.URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = this.fileName; document.body.appendChild(a); a.click(); setTimeout(() => { this.cb && this.cb({ downState: 1 }) this.reset(); document.body.removeChild(a); window.URL.revokeObjectURL(url); }, 0); } reset() { this.chunks = []; this.fileName = ''; this.fileSize = 0; this.totalChunks = 0; this.currentChunk = 0; this.downloadedSize = 0; } } // 使用示例 const url = "https://example.com/largefile.zip"; const fileName = "largefile.zip"; const downloader = new FileDownloader({url, fileName, cb: this.updateData}); // 更新狀態(tài) updateData(res) { const {downState} = res this.downState = downState } // 開始下載 downloader.startDownload(); // 暫停下載 // downloader.pauseDownload(); // 繼續(xù)下載 // downloader.resumeDownload(); // 取消下載 // downloader.cancelDownload();
分片下載怎么實現(xiàn)斷點續(xù)傳?已下載的文件怎么存儲?
瀏覽器的安全策略禁止網(wǎng)頁(JS)直接訪問和操作用戶計算機上的文件系統(tǒng)。
在分片下載過程中,每個下載的文件塊(chunk)都需要在客戶端進行緩存或存儲,方便實現(xiàn)斷點續(xù)傳功能,同時也方便后續(xù)將這些文件塊合并成完整的文件。這些文件塊可以暫時保存在內(nèi)存中或者存儲在客戶端的本地存儲(如 IndexedDB、LocalStorage 等)中。
一般情況下,為了避免占用過多的內(nèi)存,推薦將文件塊暫時保存在客戶端的本地存儲中。這樣可以確保在下載大文件時不會因為內(nèi)存占用過多而導(dǎo)致性能問題。
在上面提供的示例代碼中,文件塊是暫時保存在一個數(shù)組中的,最終在mergeChunks()
方法中將這些文件塊合并成完整的文件。如果你希望將文件塊保存在本地存儲中,可以根據(jù)需要修改代碼,將文件塊保存到 IndexedDB 或 LocalStorage 中。
IndexedDB本地存儲
IndexedDB文檔:IndexedDB_API
無痕模式是瀏覽器提供的一種隱私保護功能,它會在用戶關(guān)閉瀏覽器窗口后自動清除所有的瀏覽數(shù)據(jù),包括 LocalStorage、IndexedDB 和其他存儲機制中的數(shù)據(jù)。
IndexedDB 數(shù)據(jù)實際上存儲在瀏覽器的文件系統(tǒng)中,是瀏覽器的隱私目錄之一,不同瀏覽器可能會有不同的存儲位置,普通用戶無法直接訪問和手動刪除這些文件,因為它們受到瀏覽器的安全限制??梢允褂?nbsp;deleteDatabase
方法來刪除整個數(shù)據(jù)庫,或者使用 deleteObjectStore
方法來刪除特定的對象存儲空間中的數(shù)據(jù)。
原生的indexedDB api 使用起來很麻煩,稍不留神就會出現(xiàn)各種問題,封裝一下方便以后使用。
這個類封裝了 IndexedDB 的常用操作,包括打開數(shù)據(jù)庫、添加數(shù)據(jù)、通過 ID 獲取數(shù)據(jù)、獲取全部數(shù)據(jù)、更新數(shù)據(jù)、刪除數(shù)據(jù)和刪除數(shù)據(jù)表。
封裝indexedDB類
class IndexedDBWrapper { constructor(dbName, storeName) { this.dbName = dbName; this.storeName = storeName; this.db = null; } openDatabase() { return new Promise((resolve, reject) => { const request = indexedDB.open(this.dbName); request.onerror = () => { console.error("Failed to open database"); reject(); }; request.onsuccess = () => { this.db = request.result; resolve(); }; request.onupgradeneeded = () => { this.db = request.result; if (!this.db.objectStoreNames.contains(this.storeName)) { this.db.createObjectStore(this.storeName, { keyPath: "id" }); } }; }); } addData(data) { return new Promise((resolve, reject) => { const transaction = this.db.transaction([this.storeName], "readwrite"); const objectStore = transaction.objectStore(this.storeName); const request = objectStore.add(data); request.onsuccess = () => { resolve(); }; request.onerror = () => { console.error("Failed to add data"); reject(); }; }); } getDataById(id) { return new Promise((resolve, reject) => { const transaction = this.db.transaction([this.storeName], "readonly"); const objectStore = transaction.objectStore(this.storeName); const request = objectStore.get(id); request.onsuccess = () => { resolve(request.result); }; request.onerror = () => { console.error(`Failed to get data with id: ${id}`); reject(); }; }); } getAllData() { return new Promise((resolve, reject) => { const transaction = this.db.transaction([this.storeName], "readonly"); const objectStore = transaction.objectStore(this.storeName); const request = objectStore.getAll(); request.onsuccess = () => { resolve(request.result); }; request.onerror = () => { console.error("Failed to get all data"); reject(); }; }); } updateData(data) { return new Promise((resolve, reject) => { const transaction = this.db.transaction([this.storeName], "readwrite"); const objectStore = transaction.objectStore(this.storeName); const request = objectStore.put(data); request.onsuccess = () => { resolve(); }; request.onerror = () => { console.error("Failed to update data"); reject(); }; }); } deleteDataById(id) { return new Promise((resolve, reject) => { const transaction = this.db.transaction([this.storeName], "readwrite"); const objectStore = transaction.objectStore(this.storeName); const request = objectStore.delete(id); request.onsuccess = () => { resolve(); }; request.onerror = () => { console.error(`Failed to delete data with id: ${id}`); reject(); }; }); } deleteStore() { return new Promise((resolve, reject) => { const version = this.db.version + 1; this.db.close(); const request = indexedDB.open(this.dbName, version); request.onupgradeneeded = () => { this.db = request.result; this.db.deleteObjectStore(this.storeName); resolve(); }; request.onsuccess = () => { resolve(); }; request.onerror = () => { console.error("Failed to delete object store"); reject(); }; }); } }
使用indexedDB類示例
const dbName = "myDatabase"; const storeName = "myStore"; const dbWrapper = new IndexedDBWrapper(dbName, storeName); dbWrapper.openDatabase().then(() => { const data = { id: 1, name: "John Doe", age: 30 }; dbWrapper.addData(data).then(() => { console.log("Data added successfully"); dbWrapper.getDataById(1).then((result) => { console.log("Data retrieved:", result); const updatedData = { id: 1, name: "Jane Smith", age: 35 }; dbWrapper.updateData(updatedData).then(() => { console.log("Data updated successfully"); dbWrapper.getDataById(1).then((updatedResult) => { console.log("Updated data retrieved:", updatedResult); dbWrapper.deleteDataById(1).then(() => { console.log("Data deleted successfully"); dbWrapper.getAllData().then((allData) => { console.log("All data:", allData); dbWrapper.deleteStore().then(() => { console.log("Object store deleted successfully"); }); }); }); }); }); }); }); });
indexedDB的使用庫 - localforage
這個庫對瀏覽器本地存儲的幾種方式做了封裝,自動降級處理。但是使用indexedDB上感覺不是很好,不可以添加索引,但是操作確實方便了很多。
文檔地址: localforage
下面展示 LocalForage 中使用 IndexedDB 存儲引擎并結(jié)合 async/await
進行異步操作
const localforage = require('localforage'); // 配置 LocalForage localforage.config({ driver: localforage.INDEXEDDB, // 使用 IndexedDB 存儲引擎 name: 'myApp', // 數(shù)據(jù)庫名稱 version: 1.0, // 數(shù)據(jù)庫版本 storeName: 'myData' // 存儲表名稱 }); // 使用 async/await 進行異步操作 (async () => { try { // 存儲數(shù)據(jù) await localforage.setItem('key', 'value'); console.log('數(shù)據(jù)保存成功'); // 獲取數(shù)據(jù) const value = await localforage.getItem('key'); console.log('獲取到的數(shù)據(jù)為:', value); // 移除數(shù)據(jù) await localforage.removeItem('key'); console.log('數(shù)據(jù)移除成功'); // 關(guān)閉 IndexedDB 連接 await localforage.close(); console.log('IndexedDB 已關(guān)閉'); } catch (err) { console.error('操作失敗', err); } })();
現(xiàn)代的瀏覽器會自動管理 IndexedDB 連接的生命周期,包括在頁面關(guān)閉時自動關(guān)閉連接,在大多數(shù)情況下,不需要顯式地打開或關(guān)閉 IndexedDB 連接。
如果你有特殊的需求或者對性能有更高的要求,可以使用 localforage.close()
方法來關(guān)閉連接。
使用 LocalForage 來刪除 IndexedDB 中的所有數(shù)據(jù)
import localforage from 'localforage'; // 使用 clear() 方法刪除所有數(shù)據(jù) localforage.clear() .then(() => { console.log('IndexedDB 中的所有數(shù)據(jù)已刪除'); }) .catch((error) => { console.error('刪除 IndexedDB 數(shù)據(jù)時出錯:', error); });
IndexedDB內(nèi)存暫用過高問題
使用 IndexedDB 可能會導(dǎo)致瀏覽器內(nèi)存占用增加的原因有很多,以下是一些可能的原因:
- 數(shù)據(jù)量過大:如果你在 IndexedDB 中存儲了大量數(shù)據(jù),那么瀏覽器可能需要消耗更多內(nèi)存來管理和處理這些數(shù)據(jù)。尤其是在讀取或?qū)懭氪罅繑?shù)據(jù)時,內(nèi)存占用會顯著增加。
- 未關(guān)閉的連接:如果在使用完 IndexedDB 后未正確關(guān)閉數(shù)據(jù)庫連接,可能會導(dǎo)致內(nèi)存泄漏。確保在不再需要使用 IndexedDB 時正確關(guān)閉數(shù)據(jù)庫連接,以釋放占用的內(nèi)存。
- 索引和查詢:如果你在 IndexedDB 中創(chuàng)建了大量索引或者執(zhí)行復(fù)雜的查詢操作,都會導(dǎo)致瀏覽器內(nèi)存占用增加,特別是在處理大型數(shù)據(jù)集時。
- 緩存:瀏覽器可能會對 IndexedDB 中的數(shù)據(jù)進行緩存,以提高訪問速度。這可能會導(dǎo)致內(nèi)存占用增加,尤其是在大規(guī)模數(shù)據(jù)操作后。
- 瀏覽器實現(xiàn):不同瀏覽器的 IndexedDB 實現(xiàn)可能存在差異,某些瀏覽器可能會在處理 IndexedDB 數(shù)據(jù)時占用更多內(nèi)存。
為了減少內(nèi)存占用,你可以考慮優(yōu)化數(shù)據(jù)存儲結(jié)構(gòu)、合理使用索引、避免長時間保持大型數(shù)據(jù)集等措施。另外,使用瀏覽器的開發(fā)者工具進行內(nèi)存分析,可以幫助你找到內(nèi)存占用增加的具體原因,從而采取相應(yīng)的優(yōu)化措施。
以上就是JavaScript實現(xiàn)下載超大文件的方法詳解的詳細內(nèi)容,更多關(guān)于JavaScript下載超大文件的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
詳解js產(chǎn)生對象的3種基本方式(工廠模式,構(gòu)造函數(shù)模式,原型模式)
本篇文章主要介紹了js產(chǎn)生對象的3種基本方式(工廠模式,構(gòu)造函數(shù)模式,原型模式) ,具有一定的參考價值,有興趣的可以了解一下2017-01-01封裝運動框架實戰(zhàn)左右與上下滑動的焦點輪播圖(實例)
下面小編就為大家?guī)硪黄庋b運動框架實戰(zhàn)左右與上下滑動的焦點輪播圖(實例)。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-10-10