最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

Vue?利用?IndexedDB?實(shí)現(xiàn)前端大文件斷點(diǎn)續(xù)傳功能

 更新時(shí)間:2026年05月20日 08:49:26   作者:fox_charon  
這篇文章給大家介紹Vue利用IndexedDB實(shí)現(xiàn)前端大文件斷點(diǎn)續(xù)傳功能,本文結(jié)合實(shí)例代碼給大家介紹的非常詳細(xì),感興趣的朋友一起看看吧

一、背景與痛點(diǎn)

前端下載大文件時(shí),我們通常的做法是一行 fetch 拿到 response,轉(zhuǎn)成 Blob,再丟給一個(gè)隱藏的 <a> 標(biāo)簽觸發(fā)下載。這套邏輯在幾十 KB 的圖片、幾百 KB 的 PDF 上完全沒(méi)問(wèn)題??梢坏┪募艿?100MB、1GB,問(wèn)題就來(lái)了:

  • 瀏覽器內(nèi)存扛不住。 fetch 把整個(gè) response body 讀到內(nèi)存數(shù)組里,100MB 的文件就是 100MB+ 的堆內(nèi)存占用,稍微大一點(diǎn)就直接 OOM。
  • 刷新歸零。 進(jìn)度條跑到 80%,用戶手滑刷新了頁(yè)面——從頭再來(lái),已經(jīng)下載的數(shù)據(jù)全丟了。
  • 無(wú)法暫?;謴?fù)。 瀏覽器原生沒(méi)有"暫停下載"的概念,fetch 一旦發(fā)起,要么等到結(jié)束,要么 abort 掉全部作廢。

這背后本質(zhì)上是兩個(gè)問(wèn)題:流式數(shù)據(jù)怎么持久化,以及已持久化的數(shù)據(jù)怎么在下次請(qǐng)求時(shí)被服務(wù)器"認(rèn)賬"。第一個(gè)問(wèn)題用 IndexedDB 解決,第二個(gè)問(wèn)題靠 HTTP Range 請(qǐng)求。

本文基于一個(gè)完整的 Vue 2 + Element UI 實(shí)戰(zhàn)項(xiàng)目,把這兩塊串起來(lái),講清楚如何實(shí)現(xiàn)一個(gè)帶持久化存儲(chǔ)、支持?jǐn)帱c(diǎn)續(xù)傳、進(jìn)度精確到小數(shù)點(diǎn)后兩位的大文件下載方案。

二、核心原理:Range 請(qǐng)求與 206 響應(yīng)

HTTP 協(xié)議從 1.1 開(kāi)始就定義了一個(gè)叫 Range 的請(qǐng)求頭,語(yǔ)義很簡(jiǎn)單:告訴服務(wù)器"我只要這個(gè)文件的一部分"。

Range: bytes=5000000-

上面這行表示:從第 5,000,000 個(gè)字節(jié)開(kāi)始,把后面的內(nèi)容都給我。服務(wù)器如果支持分段傳輸,會(huì)返回 206 Partial Content,同時(shí)在 Content-Range 響應(yīng)頭里告訴客戶端三件事:本次數(shù)據(jù)的起止位置,以及文件的完整大小。

Content-Range: bytes 5000000-104857599/104857600

格式是 bytes 起始-結(jié)束/總大小。這個(gè) header 是整個(gè)斷點(diǎn)續(xù)傳的核心——客戶端靠它知道文件有多大,以及自己還差多少。

如果服務(wù)器不支持 Range(比如某些 CDN 或靜態(tài)文件服務(wù)器沒(méi)開(kāi)這個(gè)能力),它會(huì)忽略 Range 頭,照常返回 200 和完整文件。所以我們的代碼需要兼容兩種情況:206 就走續(xù)傳邏輯,200 就清空舊數(shù)據(jù)從頭來(lái)。

三、存儲(chǔ)方案選型:為什么不用 LocalStorage

有人會(huì)想:瀏覽器不是有 LocalStorage 嗎,5MB 夠不夠?答案是一票否決的——LocalStorage 只能存字符串,不能存二進(jìn)制數(shù)據(jù)。

對(duì)比維度

LocalStorage

IndexedDB

容量上限

5-10MB

瀏覽器可用磁盤(pán)的 50%-80%

數(shù)據(jù)類型

僅字符串

String、Blob、ArrayBuffer、File

存儲(chǔ)模式

同步(阻塞主線程)

異步(不阻塞 UI)

查詢能力

僅 key-value

支持索引、游標(biāo)遍歷

對(duì)于大文件下載場(chǎng)景,核心要求有三個(gè):

  • 必須存二進(jìn)制。 fetch 流式讀取出來(lái)的是 Uint8Array 二進(jìn)制塊,LocalStorage 根本塞不進(jìn)去。
  • 容量必須大。 一個(gè) 100MB 的測(cè)試文件,存進(jìn)去就是 100MB。LocalStorage 的 5MB 上限連塞牙縫都不夠。
  • 必須異步。 你不想每次寫(xiě)盤(pán)都卡 UI 渲染。

IndexedDB 是瀏覽器內(nèi)置的事務(wù)型對(duì)象數(shù)據(jù)庫(kù),支持結(jié)構(gòu)化克隆算法,ArrayBuffer 可以直接作為值存入,這些特性讓它天然適合做文件分片的持久化。

四、實(shí)戰(zhàn)代碼拆解

4.1 項(xiàng)目架構(gòu)簡(jiǎn)述

src/
├── db/downloadDB.js              # IndexedDB 操作封裝
├── store/index.js                # Vuex 狀態(tài)管理
├── App.vue                       # 根實(shí)例:核心下載邏輯
└── components/DownloadChild.vue  # 下載按鈕子組件
  • DownloadChild 通過(guò) Vuex dispatch('requestDownload', { url, filename }) 發(fā)起下載指令
  • App.vue 通過(guò) computed 屬性監(jiān)聽(tīng) Vuex 中 downloadTask.timestamp 的變化,觸發(fā)實(shí)際下載
  • downloadDB.js 封裝所有 IndexedDB CRUD 操作,與業(yè)務(wù)邏輯解耦

4.2 數(shù)據(jù)庫(kù)設(shè)計(jì)

首先看數(shù)據(jù)庫(kù)結(jié)構(gòu)——一個(gè) object store,一個(gè)索引,足以支撐整個(gè)斷點(diǎn)續(xù)傳:

const DB_NAME = 'DownloadDB'
const STORE_NAME = 'chunks'
function openDB() {
  return new Promise((resolve, reject) => {
    const request = indexedDB.open(DB_NAME, 1)
    request.onupgradeneeded = (e) => {
      const db = e.target.result
      if (!db.objectStoreNames.contains(STORE_NAME)) {
        const store = db.createObjectStore(STORE_NAME, {
          keyPath: 'id',
          autoIncrement: true
        })
        store.createIndex('filename', 'filename', { unique: false })
      }
    }
    request.onsuccess = (e) => resolve(e.target.result)
    request.onerror = (e) => reject(e.target.error)
  })
}

每條記錄包含五個(gè)字段:id(自增主鍵)、filename(用于索引查詢)、chunkIndex(分片序號(hào),用于最終排序合并)、dataArrayBuffer,實(shí)際二進(jìn)制數(shù)據(jù))、size(當(dāng)前分片的字節(jié)數(shù),用于進(jìn)度累加)。

選擇 autoIncrement 主鍵而非 [filename, chunkIndex] 復(fù)合主鍵,是因?yàn)?IndexedDB 的復(fù)合主鍵在游標(biāo)遍歷時(shí)性能不如單字段索引,而我們的 clearChunks 操作依賴游標(biāo)刪除,這是寫(xiě)入頻率較高的路徑。

4.3 核心下載流程:Range 請(qǐng)求 + 流式持久化

整個(gè) startDownload 方法是斷點(diǎn)續(xù)傳的核心,我按執(zhí)行順序拆成五個(gè)階段來(lái)講解。

階段一:查詢已下載字節(jié)

let loadedSize = await getDownloadedSize(filename)

這里遍歷 IndexedDB 中該文件的所有 chunk,累加 size 字段求和。即使之前頁(yè)面刷新過(guò)、甚至瀏覽器重啟過(guò),只要 IndexedDB 中的數(shù)據(jù)還在,就能準(zhǔn)確拿到已經(jīng)下載了多少字節(jié)。

階段二:構(gòu)造 Range 請(qǐng)求

const headers = {}
if (loadedSize > 0) {
  headers['Range'] = `bytes=${loadedSize}-`
}
const response = await fetch(url, {
  signal: this.abortController.signal,
  headers
})

如果 loadedSize 為 0(首次下載或之前已清理),就不帶 Range 頭,服務(wù)器返回完整文件。如果大于 0,告訴服務(wù)器"從第 N 個(gè)字節(jié)開(kāi)始給我"。

這里同時(shí)傳入 AbortController 的 signal,方便用戶取消下載時(shí)中斷 fetch。

階段三:處理 206 vs 200

let totalSize
if (response.status === 206) {
  const contentRange = response.headers.get('Content-Range')
  totalSize = parseInt(contentRange.split('/')[1])
} else if (response.status === 200) {
  if (loadedSize > 0) {
    await clearChunks(filename)
    loadedSize = 0
  }
  totalSize = +response.headers.get('Content-Length')
} else if (!response.ok) {
  throw new Error(`HTTP ${response.status}: ${response.statusText}`)
}

這是整個(gè)方法里最容易踩坑的分支邏輯:

  • 206:服務(wù)器認(rèn)了 Range,返回部分內(nèi)容。我們從 Content-Range: bytes 5000000-104857599/104857600 中取斜杠后面的 104857600 作為文件總大小。注意不是 Content-Length——206 響應(yīng)中的 Content-Length 只表示本次返回的 body 大小,不是完整文件大小。
  • 200:服務(wù)器不支持 Range。如果此時(shí) IndexedDB 里還有舊數(shù)據(jù),必須清空,因?yàn)橹按娴?chunk 和現(xiàn)在返回的完整數(shù)據(jù)會(huì)產(chǎn)生重疊和錯(cuò)亂。
  • 其他非 2xx:直接拋異常,交給 catch 處理。

階段四:流式讀取并逐塊寫(xiě)入 IndexedDB

const reader = response.body.getReader()
let chunkIndex = await getChunkCount(filename)
while (true) {
  const { done, value } = await reader.read()
  if (done) break
  await saveChunk(filename, chunkIndex++, value)
  loadedSize += value.length
  this.downloadPercent = parseFloat(
    ((loadedSize / totalSize) * 100).toFixed(2)
  )
}

response.body.getReader() 拿到的是 ReadableStream 的 reader,每次 read() 返回一個(gè) { done, value } 對(duì)象,value 是一個(gè) Uint8Array,大約是幾 KB 到幾十 KB 不等的二進(jìn)制塊。

關(guān)鍵細(xì)節(jié)在 saveChunk 里:

export async function saveChunk(filename, chunkIndex, data) {
  const db = await openDB()
  return new Promise((resolve, reject) => {
    const tx = db.transaction(STORE_NAME, 'readwrite')
    const store = tx.objectStore(STORE_NAME)
    const buffer = new Uint8Array(data).buffer  // 拷貝一份
    store.add({ filename, chunkIndex, data: buffer, size: buffer.byteLength })
    tx.oncomplete = () => { db.close(); resolve() }
    tx.onerror = (e) => { db.close(); reject(e.target.error) }
  })
}

new Uint8Array(data).buffer 這一步一定要做reader.read() 返回的 Uint8Array 可能共享一個(gè)更大的底層 ArrayBuffer,直接存入 IndexedDB 會(huì)導(dǎo)致數(shù)據(jù)不完整或錯(cuò)亂。做一個(gè)拷貝,雖然多花了一點(diǎn)內(nèi)存,但保證了數(shù)據(jù)的正確性。

階段五:合并分片,觸發(fā)下載,清理

async mergeAndSave(filename) {
  const chunks = await getAllChunks(filename)
  const blob = new Blob(chunks, { type: 'application/octet-stream' })
  const url = URL.createObjectURL(blob)
  const a = document.createElement('a')
  a.href = url
  a.download = filename
  document.body.appendChild(a)
  a.click()
  document.body.removeChild(a)
  URL.revokeObjectURL(url)
  await clearChunks(filename)
}

從 IndexedDB 按 chunkIndex 升序取出所有分片,每個(gè)分片還原成 Uint8Array,直接傳給 Blob 構(gòu)造函數(shù)。Blob 可以接受 Uint8Array[] 數(shù)組,會(huì)自動(dòng)拼接。

下載完成后務(wù)必調(diào)用 clearChunks——既釋放用戶磁盤(pán)空間,也避免下次下載時(shí) IndexedDB 里殘留舊數(shù)據(jù)。清理邏輯使用游標(biāo)遍歷刪除:

export async function clearChunks(filename) {
  const db = await openDB()
  return new Promise((resolve, reject) => {
    const tx = db.transaction(STORE_NAME, 'readwrite')
    const store = tx.objectStore(STORE_NAME)
    const index = store.index('filename')
    const cursorReq = index.openCursor(IDBKeyRange.only(filename))
    cursorReq.onsuccess = (e) => {
      const cursor = e.target.result
      if (cursor) {
        cursor.delete()
        cursor.continue()
      }
    }
    tx.oncomplete = () => { db.close(); resolve() }
    tx.onerror = (e) => { db.close(); reject(e.target.error) }
  })
}

這里用游標(biāo)而不是 index.getAll() + 逐條 store.delete() 的原因有兩個(gè):一是游標(biāo)一次只持有當(dāng)前記錄,內(nèi)存友好;二是游標(biāo)刪除在一次事務(wù)內(nèi)完成,要么全部成功要么全部回滾。

4.4 取消即保存

注意代碼中 startDownload 的 catch 分支:

catch (err) {
  if (err.name === 'AbortError') {
    this.progressStatus = 'exception'
    return   // 不清理 IndexedDB
  }
  // ...
}

AbortError 代表用戶主動(dòng)取消——此時(shí) IndexedDB 中的數(shù)據(jù)不會(huì)被清理。用戶下次點(diǎn)擊下載按鈕,getDownloadedSize 會(huì)讀到之前累積的字節(jié)數(shù),自動(dòng)從斷點(diǎn)繼續(xù)。

4.5 進(jìn)度精度:為什么是 toFixed(2)

進(jìn)度計(jì)算公式本身很簡(jiǎn)單:

this.downloadPercent = parseFloat(((loadedSize / totalSize) * 100).toFixed(2))

toFixed(2) 把百分比控制到兩位小數(shù),這樣 100MB 文件的最小分辨率約為 1MB × 0.01% ≈ 10KB。每次 reader.read() 返回的 chunk 大約是幾十 KB,進(jìn)度條每隔幾個(gè) chunk 就會(huì)有一次肉眼可見(jiàn)的變化,不會(huì)出現(xiàn)"卡在 99% 不動(dòng)"的體感。

配合 Element UI 的 el-progress 組件,用 format 屬性自定義顯示文本:

<el-progress
  :percentage="downloadPercent"
  :stroke-width="20"
  :text-inside="true"
  :format="percentFormat"
/>
percentFormat(pct) {
  return pct.toFixed(2) + '%'
}

這樣進(jìn)度條內(nèi)部會(huì)顯示 33.56% 這樣的精確數(shù)值,而不是默認(rèn)的 34%。

五、總結(jié)

這套方案本質(zhì)上做了一件事:把瀏覽器的持久化存儲(chǔ)能力和 HTTP 協(xié)議的分段傳輸能力結(jié)合起來(lái),讓大文件下載從"一次性內(nèi)存操作"變成"可暫停、可恢復(fù)、狀態(tài)可持久化的流式管道"。

實(shí)戰(zhàn)中值得記住的幾個(gè)點(diǎn):

  • fetch + ReadableStream 是流式處理二進(jìn)制數(shù)據(jù)的基礎(chǔ)設(shè)施,response.body.getReader() 是拿到這塊能力的入口。
  • IndexedDB 是前端唯一能存大容量二進(jìn)制數(shù)據(jù)的方案,但它基于事務(wù),有并發(fā)限制,封裝時(shí)要注意 Promise 化并確保每次操作后關(guān)閉連接。
  • 斷點(diǎn)續(xù)傳的關(guān)鍵在于對(duì) 206 和 200 的分支處理,尤其是"服務(wù)器不支持 Range 時(shí)要清空舊數(shù)據(jù)"這個(gè)邏輯,遺漏了就會(huì)導(dǎo)致文件損壞。
  • 進(jìn)度條只是 UI 層面的反饋,真正的可靠性在于 IndexedDB 中的數(shù)據(jù)完整性——每一塊 chunk 存入前做一次獨(dú)立的 ArrayBuffer 拷貝,代價(jià)很小,收益是絕對(duì)的確定性。

到此這篇關(guān)于Vue 利用 IndexedDB 實(shí)現(xiàn)前端大文件斷點(diǎn)續(xù)傳的文章就介紹到這了,更多相關(guān)vue前端大文件斷點(diǎn)續(xù)傳內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

最新評(píng)論

诸暨市| 麻栗坡县| 凤山县| 偏关县| 家居| 北川| 盐源县| 龙川县| 沂水县| 阿鲁科尔沁旗| 寿阳县| 鸡泽县| 蓬安县| 婺源县| 英超| 武宣县| 凤城市| 雷山县| 阜宁县| 镇坪县| 二连浩特市| 抚松县| 漯河市| 三江| 青岛市| 当雄县| 潜山县| 麻栗坡县| 永定县| 扶风县| 北票市| 大冶市| 靖西县| 双柏县| 万荣县| 剑川县| 天水市| 双鸭山市| 师宗县| 玉门市| 榆林市|