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

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

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

一、背景與痛點

前端下載大文件時,我們通常的做法是一行 fetch 拿到 response,轉(zhuǎn)成 Blob,再丟給一個隱藏的 <a> 標(biāo)簽觸發(fā)下載。這套邏輯在幾十 KB 的圖片、幾百 KB 的 PDF 上完全沒問題。可一旦文件跑到 100MB、1GB,問題就來了:

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

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

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

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

HTTP 協(xié)議從 1.1 開始就定義了一個叫 Range 的請求頭,語義很簡單:告訴服務(wù)器"我只要這個文件的一部分"。

Range: bytes=5000000-

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

Content-Range: bytes 5000000-104857599/104857600

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

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

三、存儲方案選型:為什么不用 LocalStorage

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

對比維度

LocalStorage

IndexedDB

容量上限

5-10MB

瀏覽器可用磁盤的 50%-80%

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

僅字符串

String、Blob、ArrayBuffer、File

存儲模式

同步(阻塞主線程)

異步(不阻塞 UI)

查詢能力

僅 key-value

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

對于大文件下載場景,核心要求有三個:

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

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

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

4.1 項目架構(gòu)簡述

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

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

首先看數(shù)據(jù)庫結(jié)構(gòu)——一個 object store,一個索引,足以支撐整個斷點續(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)
  })
}

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

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

4.3 核心下載流程:Range 請求 + 流式持久化

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

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

let loadedSize = await getDownloadedSize(filename)

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

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

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 個字節(jié)開始給我"。

這里同時傳入 AbortController 的 signal,方便用戶取消下載時中斷 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}`)
}

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

  • 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。如果此時 IndexedDB 里還有舊數(shù)據(jù),必須清空,因為之前存的 chunk 和現(xiàn)在返回的完整數(shù)據(jù)會產(chǎn)生重疊和錯亂。
  • 其他非 2xx:直接拋異常,交給 catch 處理。

階段四:流式讀取并逐塊寫入 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() 返回一個 { done, value } 對象,value 是一個 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 可能共享一個更大的底層 ArrayBuffer,直接存入 IndexedDB 會導(dǎo)致數(shù)據(jù)不完整或錯亂。做一個拷貝,雖然多花了一點內(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 升序取出所有分片,每個分片還原成 Uint8Array,直接傳給 Blob 構(gòu)造函數(shù)。Blob 可以接受 Uint8Array[] 數(shù)組,會自動拼接。

下載完成后務(wù)必調(diào)用 clearChunks——既釋放用戶磁盤空間,也避免下次下載時 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() 的原因有兩個:一是游標(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 代表用戶主動取消——此時 IndexedDB 中的數(shù)據(jù)不會被清理。用戶下次點擊下載按鈕,getDownloadedSize 會讀到之前累積的字節(jié)數(shù),自動從斷點繼續(xù)。

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

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

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

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

配合 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)部會顯示 33.56% 這樣的精確數(shù)值,而不是默認(rèn)的 34%。

五、總結(jié)

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

實戰(zhàn)中值得記住的幾個點:

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

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

相關(guān)文章

  • uniapp微信小程序webview和h5數(shù)據(jù)通信代碼示例

    uniapp微信小程序webview和h5數(shù)據(jù)通信代碼示例

    這篇文章主要介紹了uniapp微信小程序webview和h5數(shù)據(jù)通信的相關(guān)資料,文章還列出了項目的結(jié)構(gòu),包括微信小程序和h5端的主要文件和組件,文中通過代碼介紹的非常詳細(xì),需要的朋友可以參考下
    2025-01-01
  • vue+mockjs模擬數(shù)據(jù)實現(xiàn)前后端分離開發(fā)的實例代碼

    vue+mockjs模擬數(shù)據(jù)實現(xiàn)前后端分離開發(fā)的實例代碼

    本篇文章主要介紹了vue+mockjs模擬數(shù)據(jù)實現(xiàn)前后端分離開發(fā)的實例代碼,具有一定的參考價值,有興趣的可以了解一下
    2017-08-08
  • Vue如何得到一行的數(shù)據(jù)

    Vue如何得到一行的數(shù)據(jù)

    在Vue中,rows為Object數(shù)組,直接使用rows[0]即可獲取單行數(shù)據(jù),無需調(diào)用getById;修改數(shù)據(jù)時可用toRaw避免重復(fù)查詢,提升效率
    2025-08-08
  • Vue3+Element Plus進(jìn)行圖片加載優(yōu)化全攻略

    Vue3+Element Plus進(jìn)行圖片加載優(yōu)化全攻略

    在Web開發(fā)中,未優(yōu)化的圖片會導(dǎo)致很多問題,本文將為大家介紹一下Vue3如何通過Element Plus進(jìn)行圖片加載優(yōu)化,希望對大家有所幫助
    2025-03-03
  • vuex的簡單使用教程

    vuex的簡單使用教程

    vuex是一個專門為vue.js設(shè)計的集中式狀態(tài)管理架構(gòu)。這篇文章主要介紹了vuex的簡單使用,需要的朋友可以參考下
    2018-02-02
  • Vue+Element一步步實現(xiàn)動態(tài)添加Input_輸入框案例

    Vue+Element一步步實現(xiàn)動態(tài)添加Input_輸入框案例

    這篇文章主要介紹了Vue+Element一步步實現(xiàn)動態(tài)添加Input_輸入框案例,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2022-09-09
  • vue項目打包優(yōu)化的方法實戰(zhàn)記錄

    vue項目打包優(yōu)化的方法實戰(zhàn)記錄

    最近入職了新公司,接手了一個新拆分出來的Vue項目,針對該項目做了個打包優(yōu)化,把經(jīng)驗分享出來,下面這篇文章主要給大家介紹了關(guān)于vue項目打包優(yōu)化的相關(guān)資料,文中通過實例代碼介紹的非常詳細(xì),需要的朋友可以參考下
    2022-08-08
  • Vue render渲染時間戳轉(zhuǎn)時間,時間轉(zhuǎn)時間戳及渲染進(jìn)度條效果

    Vue render渲染時間戳轉(zhuǎn)時間,時間轉(zhuǎn)時間戳及渲染進(jìn)度條效果

    這篇文章主要介紹了Vue render渲染時間戳轉(zhuǎn)時間,時間轉(zhuǎn)時間戳及渲染進(jìn)度條效果,通過實例代碼相結(jié)合的形式給大家介紹的非常詳細(xì),具有一定的參考借鑒價值,需要的朋友可以參考下
    2018-07-07
  • 詳解如何在Vue3+TS的項目中使用NProgress進(jìn)度條

    詳解如何在Vue3+TS的項目中使用NProgress進(jìn)度條

    本文主要介紹了詳解如何在Vue3+TS的項目中使用NProgress進(jìn)度條,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2022-06-06
  • Vue封裝實現(xiàn)可配置的搜索列表組件

    Vue封裝實現(xiàn)可配置的搜索列表組件

    在Vue.js開發(fā)中,經(jīng)常會遇到需要展示搜索和列表的需求,為了提高代碼復(fù)用性和開發(fā)效率,我們可以封裝一個可配置的搜索列表組件,下面我們就來講講如何實現(xiàn)這樣一個組件吧
    2023-08-08

最新評論

正安县| 台湾省| 广州市| 余江县| 扶余县| 社会| 南澳县| 景泰县| 梨树县| 南乐县| 东兰县| 理塘县| 雅安市| 新建县| 固阳县| 孟村| 阿克陶县| 德化县| 长沙市| 六安市| 辽宁省| 色达县| 醴陵市| 馆陶县| 惠水县| 蕲春县| 庆城县| 同江市| 富川| 德昌县| 泗阳县| 荔浦县| 台南县| 含山县| 蒙自县| 临海市| 丽江市| 遵化市| 锡林郭勒盟| 青海省| 康保县|