js - 大文件上傳下載 - 分片上傳、並行下載


js - 大文件上傳下載

大文件上傳-分片上傳

分片上傳的好處是將一個大請求分成多個小請求來執行,這樣當其中一些請求失敗后,不需要重新上傳整個文件,而只需要上傳失敗的分片就可以了。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>upload</title>
</head>
<body>
    <input type="file" name="file" id="file">
    <button id="upload" onClick="upload()">upload</button>
    <script type="text/javascript">
        var bytesPerPiece = 1024 * 1024; // 每個文件切片大小定為1MB .
        var totalPieces;
        //發送請求
        function upload() {
            var blob = document.getElementById("file").files[0];
            var start = 0;
            var end;
            var index = 0;
            var filesize = blob.size;
            var filename = blob.name;

            //計算文件切片總數
            totalPieces = Math.ceil(filesize / bytesPerPiece);
            console.log('blob:',blob)
            console.log('totalPieces:',totalPieces)
            while(start < filesize) {
                end = start + bytesPerPiece;
                if(end > filesize) {
                    end = filesize;
                }

                var chunk = blob.slice(start,end);//切割文件    
                var sliceIndex= blob.name + index;
                var formData = new FormData();
                formData.append("file", chunk, filename);
                console.log('start:',start)
                console.log('end:',end)
                console.log('chunk:',chunk)
                console.log('sliceIndex:',sliceIndex)
                console.log('formData:',formData)
                // $.ajax({
                //     url: 'http://localhost:9999/test.php',
                //     type: 'POST',
                //     cache: false,
                //     data: formData,
                //     processData: false,
                //     contentType: false,
                // }).done(function(res){ 

                // }).fail(function(res) {

                // });
                start = end;
                index++;
            }
        }
    </script>
</body>
</html>

斷點上傳

  1. 實現文件塊的上傳函數
// 文件切塊大小為1MB
const chunkSize = 1024 * 1024;

// 從start字節處開始上傳
function upload(start) {
    let fileObj = document.getElementById('file').files[0];
    // 上傳完成
    if (start >= fileObj.size) {
        return;
    }
    // 獲取文件塊的終止字節
    let end = (start + chunkSize > fileObj.size) ? fileObj.size : (start + chunkSize);
    // 將文件切塊上傳
    let fd = new FormData();
    fd.append('file', fileObj.slice(start, end));
    // POST表單數據
    let xhr = new XMLHttpRequest();
    xhr.open('post', 'upload.php', true);
    xhr.onload = function() {
        if (this.readyState == 4 && this.status == 200) {
            // 上傳一塊完成后修改進度條信息,然后上傳下一塊
            let progress = document.getElementById('progress');
            progress.max = fileObj.size;
            progress.value = end;
            upload(end);
        }
    }
    xhr.send(fd);
}
  1. 如果突然斷網或者瀏覽器意外關閉,那么上傳的是不完整的文件,我們只需要在選擇了文件以后向服務器查詢一下服務器上相同文件名的大小,然后將開始上傳位置(字節)設置到這個大小即可:
// 初始化上傳大小
function init() {
    let fileObj = document.getElementById('file').files[0];
    let xhr = new XMLHttpRequest();
    xhr.onreadystatechange = function() {
        if (this.readyState == 4 && this.status == 200) {
            // 將字符串轉化為整數
            let start = parseInt(this.responseText);
            // 設置進度條
            let progress = document.getElementById('progress');
            progress.max = fileObj.size;
            progress.value = start;
            // 開始上傳
            upload(start);
        }
    }
    xhr.open('post', 'fileSize.php', true);
    // 向服務器發送文件名查詢大小
    xhr.send(fileObj.name);
}

文件流下載

特殊的情況下我們不希望暴露文件下載地址,且文件下載地址需要有登錄權限的cookie或者token鑒權后才允許下載,並不希望用戶拿到下載地址后去別的工具上下載,能盡量規避文件傳播的風險。適用的范圍可能還會有其他的場景。

axios({
    method: 'post',
    url: 'api/file',
    responseType: 'blob'
}).then(res=> {
     if (res.data){
      filename = 'filename';
      let blob = new Blob([res.data],{type:"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=UTF-8"});
      if (window.navigator.msSaveOrOpenBlob){
          // IE10+下載
        navigator.msSaveOrBlob(blob, filename);
      }else{
          // 非IE10+下載
        let link = document.createElement('a');
        link.href = window.URL.createObjectURL(blob);
        link.download = filename;
        document.body.appendChild(link);
        var evt = document.createEvent("MouseEvents");
        evt.initEvent("click", false, false);
        link.dispatchEvent(evt);//釋放URL 對象
        document.body.removeChild(link);
      }
}).catch((error) => {
  console.log(error)
})

大文件的下載

需求:大文件分片下載,合並,另存為文件,完成大文件的斷點續傳下載功能。

道理差不多,戶端的下載文件拼接的方式,並且壓縮包要考慮服務端的打包壓縮時間

var blob = new Blob([arrFiles], { type: 'application/octet-stream' });
var glbReader = new window.FileReader();
reader.readAsArrayBuffer(blob);
reader.onloadend = function () {
    var newBuffer = reader.result;
      // 再轉成blobURL的形式本地下載
};

分片下載

斷點下載

斷點下載原理參考斷點上傳

  1. js 通過ajax 下載分片文件獲得blob是很好實現。
var xhr = new XMLHttpRequest();
xhr.open("GET", downobj.filePartitionUrls[downobj.downSuccessPartitionCount], true);//open false 是同步請求,不會異步觸發
xhr.responseType = 'blob';
xhr.onload=(e)=>{this.partitionDownSucCall(downobj,xhr,"onload");};//3.數據是否已經全部下載完成,如果是執行最后的下載操作
xhr.ontimeout =(e)=>{this.partitionDownSucCall(downobj,xhr,"timeout");};
xhr.onerror =(e)=>{this.partitionDownSucCall(downobj,xhr,"error");};
xhr.send();
  1. 下載的blob文件需要在本地瀏覽器中做持久化存儲,為什么要在瀏覽器中持久化存儲,因為幾千個分片的文件,用戶可能下載了幾個分片,就把瀏覽器關掉,去干別的事情了,回頭再來打開瀏覽器進行下載,這個情況下如果已經下載的分片沒有持久化存儲,就會丟失,文件就得從頭重新下載了。

那h5中如何實現類容的持久存儲呢,我們都知道瀏覽器有LocalStorage 和sessionStorage,但是很明顯sessionStorage是不適合的,因為瀏覽器關閉會清空會話數據,LocalStorage能夠實現持久化存儲,不會在網頁關閉后丟失數據,但是LocalStorage有一個限制,同一個域名下最大存儲5M數據(瀏覽器不同稍有差異)。

顯然LocalStorage的存儲容量5M完全無法滿足我們的文件分片存儲需求,超過5M的文件無法被存起來。

其實瀏覽器還有一個數據存儲,但是日常中非常少用到,所以很多人不知道,這就是 indexedDb,瀏覽器提供的索引數據庫(https://developer.mozilla.org/zh-CN/docs/Web/API/IndexedDB_API),能持久化存儲數據,並且存儲容量很大,主要容量現在來源於硬盤,所以容量范圍很寬。具體限制:https://developer.mozilla.org/zh-CN/docs/Web/API/IndexedDB_API/Browser_storage_limits_and_eviction_criteria

有了indexedDb ,我們下載的文件分片就能全部存在indexedDb中了。

因為indexedDb 的原生調用方式還是很復雜的,我們就引用localforage.min.js查件,簡化對indexedDb的操作。讀寫數據非常方便,文檔:https://localforage.github.io/localForage/

  1. 當文件分片都下載完成了,我們就應該從indexedDb 中取出所有的分片文件,並且將分片按照順序合並成一個文件包

將所有分片文件的blob數據合並成一個數組。並將數組合並為一個Blob數據對象。

偽代碼
let pratition1=blobData1;//分片1的blob數據
let pratition2=blobData2;//分片2的blob數據
...
let allFileData = [pratition1,pratition2,...];//將所有分片BLOB數據合並到一個數組
var fileBlob = new Blob(allData,{type:"application/octet-stream;charset=utf-8"});//合並后的數組轉成一個Blob對象。
  1. 將合並好的文件觸發本地下載或者說另存為的操作。
      if (window.navigator.msSaveOrOpenBlob){
          // IE10+下載
        navigator.msSaveOrBlob(blob, filename);
      }else{
          // 非IE10+下載
        let link = document.createElement('a');
        link.href = window.URL.createObjectURL(blob);
        link.download = filename;
        document.body.appendChild(link);
        var evt = document.createEvent("MouseEvents");
        evt.initEvent("click", false, false);
        link.dispatchEvent(evt);//釋放URL 對象
        document.body.removeChild(link);
      }

並發下載

  1. 發送head請求獲取文件大小
  2. 計算文件分塊數
  3. 使用asyncPool執行並發下載
  4. 分塊下載完成后轉換為Uint8Array
  5. 執行合並操作
  6. 利用BlobUR執行保存操作
// 1. 獲取文件的長度。在該函數中,我們通過發送 HEAD 請求,然后從響應頭中讀取 Content-Length 的信息,進而獲取當前 url 對應文件的內容長度。
function getContentLength(url) {
  return new Promise((resolve, reject) => {
    let xhr = new XMLHttpRequest();
    xhr.open("HEAD", url);
    xhr.send();
    xhr.onload = function () {
      resolve(
        ~~xhr.getResponseHeader("Content-Length") 
      );
    };
    xhr.onerror = reject;
  });
}

// 2. 實現異步任務的並發控制
// poolLimit(數字類型):表示限制的並發數;
// array(數組類型):表示任務數組;
// iteratorFn(函數類型):表示迭代函數,用於實現對每個任務項進行處理,該函數會返回一個 Promise 對象或異步函數。

async function asyncPool(poolLimit, array, iteratorFn) {
  const ret = []; // 存儲所有的異步任務
  const executing = []; // 存儲正在執行的異步任務
  for (const item of array) {
    // 調用iteratorFn函數創建異步任務
    const p = Promise.resolve().then(() => iteratorFn(item, array));
    ret.push(p); // 保存新的異步任務
 
    // 當poolLimit值小於或等於總任務個數時,進行並發控制
    if (poolLimit <= array.length) {
      // 當任務完成后,從正在執行的任務數組中移除已完成的任務
      const e = p.then(() => executing.splice(executing.indexOf(e), 1));
      executing.push(e); // 保存正在執行的異步任務
      if (executing.length >= poolLimit) {
        await Promise.race(executing); // 等待較快的任務執行完成
      }
    }
  }
  return Promise.all(ret);
}

// 3. 根據傳入的參數發起范圍請求,從而下載指定范圍內的文件數據塊:
function getBinaryContent(url, start, end, i) {
  return new Promise((resolve, reject) => {
    try {
      let xhr = new XMLHttpRequest();
      xhr.open("GET", url, true);
      xhr.setRequestHeader("range", `bytes=${start}-${end}`); // 請求頭上設置范圍請求信息
      xhr.responseType = "arraybuffer"; // 設置返回的類型為arraybuffer
      xhr.onload = function () {
        resolve({
          index: i, // 文件塊的索引
          buffer: xhr.response, // 范圍請求對應的數據
        });
      };
      xhr.send();
    } catch (err) {
      reject(new Error(err));
    }
  });
}
// 需要注意的是 ArrayBuffer 對象用來表示通用的、固定長度的原始二進制數據緩沖區。我們不能直接操作 ArrayBuffer 的內容,而是要通過類型數組對象或 DataView 對象來操作,它們會將緩沖區中的數據表示為特定的格式,並通過這些格式來讀寫緩沖區的內容。

// 4. 由於不能直接操作 ArrayBuffer 對象,所以我們需要先把 ArrayBuffer 對象轉換為 Uint8Array 對象,然后在執行合並操作。以下定義的 concatenate 函數就是為了合並已下載的文件數據塊,具體代碼如下所示:

function concatenate(arrays) {
  if (!arrays.length) return null;
  let totalLength = arrays.reduce((acc, value) => acc + value.length, 0);
  let result = new Uint8Array(totalLength);
  let length = 0;
  for (let array of arrays) {
    result.set(array, length);
    length += array.length;
  }
  return result;
}

// 5. saveAs 函數用於實現客戶端文件保存的功能,這里只是一個簡單的實現。在實際項目中,你可以考慮直接使用 FileSaver.js 

function saveAs({ name, buffers, mime = "application/octet-stream" }) {
  const blob = new Blob([buffers], { type: mime });
  const blobUrl = URL.createObjectURL(blob);
  const a = document.createElement("a");
  a.download = name || Math.random();
  a.href = blobUrl;
  a.click();
  URL.revokeObjectURL(blob);
}

// 6. download 函數用於實現下載操作,它支持 3 個參數:
// url(字符串類型):預下載資源的地址;
// chunkSize(數字類型):分塊的大小,單位為字節;
// poolLimit(數字類型):表示限制的並發數。

async function download({ url, chunkSize, poolLimit = 1 }) {
  const contentLength = await getContentLength(url);
  const chunks = typeof chunkSize === "number" ? Math.ceil(contentLength / chunkSize) : 1;
  const results = await asyncPool(
    poolLimit,
    [...new Array(chunks).keys()],
    (i) => {
      let start = i * chunkSize;
      let end = i + 1 == chunks ? contentLength - 1 : (i + 1) * chunkSize - 1;
      return getBinaryContent(url, start, end, i);
    }
  );
  const sortedBuffers = results
    .map((item) => new Uint8Array(item.buffer));
  return concatenate(sortedBuffers);
}

大文件下載使用示例


// 基於前面定義的輔助函數,我們就可以輕松地實現大文件並行下載,具體代碼如下所示:

function multiThreadedDownload() {
  const url = document.querySelector("#fileUrl").value;
  if (!url || !/https?/.test(url)) return;
  console.log("多線程下載開始: " + +new Date());
  download({
    url,
    chunkSize: 0.1 * 1024 * 1024,
    poolLimit: 6,
  }).then((buffers) => {
    console.log("多線程下載結束: " + +new Date());
    saveAs({ buffers, name: "我的壓縮包", mime: "application/zip" });
  });
}

參考地址

JS大文件上傳解決方案
js實現大文件分片上傳的方法
js大文件上傳解決方案(500M以上)
js大文件上傳
基於Node.js的大文件分片上傳
使用JS實現可斷點續傳的文件上傳方案
JavaScript 中如何實現大文件並行下載?
js文件下載,使用indexedDB 在H5頁面中完成大文件的斷點分片下載能力,並完成最終的分片合並另存為文件


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM