JavaScript實(shí)現(xiàn)下載超大文件的方法詳解
本文從前端方面出發(fā)實(shí)現(xiàn)瀏覽器下載大文件的功能。不考慮網(wǎng)絡(luò)異常、關(guān)閉網(wǎng)頁(yè)等原因造成傳輸中斷的情況。分片下載采用串行方式(并行下載需要對(duì)切片計(jì)算hash,比對(duì)hash,丟失重傳,合并chunks的時(shí)候需要按順序合并等,很麻煩。對(duì)傳輸速度有追求的,并且在帶寬允許的情況下可以做并行分片下載)。
測(cè)試發(fā)現(xiàn)存一兩個(gè)G左右數(shù)據(jù)到IndexedDB后,瀏覽器確實(shí)會(huì)內(nèi)存占用過高導(dǎo)致退出 (我測(cè)試使用的是chrome103版本瀏覽器)
實(shí)現(xiàn)步驟
- 使用分片下載: 將大文件分割成多個(gè)小塊進(jìn)行下載,可以降低內(nèi)存占用和網(wǎng)絡(luò)傳輸中斷的風(fēng)險(xiǎn)。這樣可以避免一次性下載整個(gè)大文件造成的性能問題。
- 斷點(diǎn)續(xù)傳: 實(shí)現(xiàn)斷點(diǎn)續(xù)傳功能,即在下載中途中斷后,可以從已下載的部分繼續(xù)下載,而不需要重新下載整個(gè)文件。
- 進(jìn)度條顯示: 在頁(yè)面上展示下載進(jìn)度,讓用戶清晰地看到文件下載的進(jìn)度。如果一次全部下載可以從process中直接拿到參數(shù)計(jì)算得出(很精細(xì)),如果是分片下載,也是計(jì)算已下載的和總大小,只不過已下載的會(huì)成片成片的增加(不是很精細(xì))。
- 取消下載和暫停下載功能: 提供取消下載和暫停下載的按鈕,讓用戶可以根據(jù)需要中止或暫停下載過程。
- 合并文件: 下載完成后,將所有分片文件合并成一個(gè)完整的文件。
以下是一個(gè)基本的前端大文件下載的實(shí)現(xiàn)示例:
可以在類里面增加注入一個(gè)回調(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();
分片下載怎么實(shí)現(xiàn)斷點(diǎn)續(xù)傳?已下載的文件怎么存儲(chǔ)?
瀏覽器的安全策略禁止網(wǎng)頁(yè)(JS)直接訪問和操作用戶計(jì)算機(jī)上的文件系統(tǒng)。
在分片下載過程中,每個(gè)下載的文件塊(chunk)都需要在客戶端進(jìn)行緩存或存儲(chǔ),方便實(shí)現(xiàn)斷點(diǎn)續(xù)傳功能,同時(shí)也方便后續(xù)將這些文件塊合并成完整的文件。這些文件塊可以暫時(shí)保存在內(nèi)存中或者存儲(chǔ)在客戶端的本地存儲(chǔ)(如 IndexedDB、LocalStorage 等)中。
一般情況下,為了避免占用過多的內(nèi)存,推薦將文件塊暫時(shí)保存在客戶端的本地存儲(chǔ)中。這樣可以確保在下載大文件時(shí)不會(huì)因?yàn)閮?nèi)存占用過多而導(dǎo)致性能問題。
在上面提供的示例代碼中,文件塊是暫時(shí)保存在一個(gè)數(shù)組中的,最終在mergeChunks()方法中將這些文件塊合并成完整的文件。如果你希望將文件塊保存在本地存儲(chǔ)中,可以根據(jù)需要修改代碼,將文件塊保存到 IndexedDB 或 LocalStorage 中。
IndexedDB本地存儲(chǔ)
IndexedDB文檔:IndexedDB_API
IndexedDB 瀏覽器存儲(chǔ)限制和清理標(biāo)準(zhǔn)
無(wú)痕模式是瀏覽器提供的一種隱私保護(hù)功能,它會(huì)在用戶關(guān)閉瀏覽器窗口后自動(dòng)清除所有的瀏覽數(shù)據(jù),包括 LocalStorage、IndexedDB 和其他存儲(chǔ)機(jī)制中的數(shù)據(jù)。
IndexedDB 數(shù)據(jù)實(shí)際上存儲(chǔ)在瀏覽器的文件系統(tǒng)中,是瀏覽器的隱私目錄之一,不同瀏覽器可能會(huì)有不同的存儲(chǔ)位置,普通用戶無(wú)法直接訪問和手動(dòng)刪除這些文件,因?yàn)樗鼈兪艿綖g覽器的安全限制。可以使用 deleteDatabase 方法來刪除整個(gè)數(shù)據(jù)庫(kù),或者使用 deleteObjectStore 方法來刪除特定的對(duì)象存儲(chǔ)空間中的數(shù)據(jù)。
原生的indexedDB api 使用起來很麻煩,稍不留神就會(huì)出現(xiàn)各種問題,封裝一下方便以后使用。
這個(gè)類封裝了 IndexedDB 的常用操作,包括打開數(shù)據(jù)庫(kù)、添加數(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的使用庫(kù) - localforage
這個(gè)庫(kù)對(duì)瀏覽器本地存儲(chǔ)的幾種方式做了封裝,自動(dòng)降級(jí)處理。但是使用indexedDB上感覺不是很好,不可以添加索引,但是操作確實(shí)方便了很多。
文檔地址: localforage
下面展示 LocalForage 中使用 IndexedDB 存儲(chǔ)引擎并結(jié)合 async/await 進(jìn)行異步操作
const localforage = require('localforage');
// 配置 LocalForage
localforage.config({
driver: localforage.INDEXEDDB, // 使用 IndexedDB 存儲(chǔ)引擎
name: 'myApp', // 數(shù)據(jù)庫(kù)名稱
version: 1.0, // 數(shù)據(jù)庫(kù)版本
storeName: 'myData' // 存儲(chǔ)表名稱
});
// 使用 async/await 進(jìn)行異步操作
(async () => {
try {
// 存儲(chǔ)數(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)代的瀏覽器會(huì)自動(dòng)管理 IndexedDB 連接的生命周期,包括在頁(yè)面關(guān)閉時(shí)自動(dòng)關(guān)閉連接,在大多數(shù)情況下,不需要顯式地打開或關(guān)閉 IndexedDB 連接。
如果你有特殊的需求或者對(duì)性能有更高的要求,可以使用 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ù)時(shí)出錯(cuò):', error);
});
IndexedDB內(nèi)存暫用過高問題
使用 IndexedDB 可能會(huì)導(dǎo)致瀏覽器內(nèi)存占用增加的原因有很多,以下是一些可能的原因:
- 數(shù)據(jù)量過大:如果你在 IndexedDB 中存儲(chǔ)了大量數(shù)據(jù),那么瀏覽器可能需要消耗更多內(nèi)存來管理和處理這些數(shù)據(jù)。尤其是在讀取或?qū)懭氪罅繑?shù)據(jù)時(shí),內(nèi)存占用會(huì)顯著增加。
- 未關(guān)閉的連接:如果在使用完 IndexedDB 后未正確關(guān)閉數(shù)據(jù)庫(kù)連接,可能會(huì)導(dǎo)致內(nèi)存泄漏。確保在不再需要使用 IndexedDB 時(shí)正確關(guān)閉數(shù)據(jù)庫(kù)連接,以釋放占用的內(nèi)存。
- 索引和查詢:如果你在 IndexedDB 中創(chuàng)建了大量索引或者執(zhí)行復(fù)雜的查詢操作,都會(huì)導(dǎo)致瀏覽器內(nèi)存占用增加,特別是在處理大型數(shù)據(jù)集時(shí)。
- 緩存:瀏覽器可能會(huì)對(duì) IndexedDB 中的數(shù)據(jù)進(jìn)行緩存,以提高訪問速度。這可能會(huì)導(dǎo)致內(nèi)存占用增加,尤其是在大規(guī)模數(shù)據(jù)操作后。
- 瀏覽器實(shí)現(xiàn):不同瀏覽器的 IndexedDB 實(shí)現(xiàn)可能存在差異,某些瀏覽器可能會(huì)在處理 IndexedDB 數(shù)據(jù)時(shí)占用更多內(nèi)存。
為了減少內(nèi)存占用,你可以考慮優(yōu)化數(shù)據(jù)存儲(chǔ)結(jié)構(gòu)、合理使用索引、避免長(zhǎng)時(shí)間保持大型數(shù)據(jù)集等措施。另外,使用瀏覽器的開發(fā)者工具進(jìn)行內(nèi)存分析,可以幫助你找到內(nèi)存占用增加的具體原因,從而采取相應(yīng)的優(yōu)化措施。
以上就是JavaScript實(shí)現(xiàn)下載超大文件的方法詳解的詳細(xì)內(nèi)容,更多關(guān)于JavaScript下載超大文件的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
實(shí)現(xiàn)png圖片和png背景透明(支持多瀏覽器)的方法
Firefox和Opera對(duì)PNG的支持非常的好,都是IE卻無(wú)視PNG圖片這一特性的“存在”,雖然IE7已經(jīng)支持都是IE6還是不行。2009-09-09
Javascript數(shù)據(jù)結(jié)構(gòu)與算法之列表詳解
這篇文章主要介紹了Javascript數(shù)據(jù)結(jié)構(gòu)與算法之列表詳解,本文講解了列表的抽象數(shù)據(jù)類型定義、如何實(shí)現(xiàn)列表類等內(nèi)容,需要的朋友可以參考下2015-03-03
原生javascript實(shí)現(xiàn)類似vue的數(shù)據(jù)綁定功能示例【觀察者模式】
這篇文章主要介紹了原生javascript實(shí)現(xiàn)類似vue的數(shù)據(jù)綁定功能,結(jié)合實(shí)例形式分析了JavaScript基于觀察者模式實(shí)現(xiàn)類似vue的數(shù)據(jù)綁定相關(guān)操作技巧,需要的朋友可以參考下2020-02-02
JS使用iView的Dropdown實(shí)現(xiàn)一個(gè)右鍵菜單
這篇文章主要介紹了JS使用iView的Dropdown實(shí)現(xiàn)一個(gè)右鍵菜單功能,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2019-05-05
JavaScript實(shí)現(xiàn)的反序列化json字符串操作示例
這篇文章主要介紹了JavaScript實(shí)現(xiàn)的反序列化json字符串操作,結(jié)合實(shí)例形式分析了eval與JSON.parse兩種反序列化json字符串的相關(guān)操作技巧,需要的朋友可以參考下2018-07-07
JavaScript實(shí)現(xiàn)手勢(shì)識(shí)別的示例詳解
這篇文章主要為大家詳細(xì)介紹了JavaScrip如何利用?TensorFlow.js?中的?HandPose?模型,實(shí)現(xiàn)一個(gè)基于視頻流的手勢(shì)識(shí)別系統(tǒng),感興趣的可以了解下2024-12-12

