上傳、下載終極解決方案:切片!!!

文件傳輸是一個常見的需求。對於大文件的下載和上傳,直接使用傳統的方式可能會遇到性能和用戶體驗方面的問題。

幸運的是,前端技術提供了一些高效的解決方案:文件流操作和切片下載與上傳。本文將深入探討這些技術,幫助你理解它們的原理和實現方法,以優化文件傳輸效率和提升用戶體驗。

一、前端文件流操作

在前端開發中,文件流操作是指通過數據流的方式處理文件,對文件進行讀取寫入展示等操作。下面詳細介紹了前端文件流操作的幾個基本概念和技術。

數據流和文件處理的基本概念

數據流是指連續的數據序列,可以從一個源傳輸到另一個目的地。在前端開發中,文件可以被看作數據流的一種形式,可以通過數據流的方式進行處理。文件處理涉及讀取和寫入文件的操作,包括讀取文件的內容、寫入數據到文件,以及對文件進行刪除、重命名等操作。

Blob 對象和 ArrayBuffer:處理二進制數據

在前端處理文件時,經常需要處理二進制數據。Blob(Binary Large Object)對象是用來表示二進制數據的一個接口,可以存儲大量的二進制數據。Blob 對象可以通過構造函數進行創建,也可以通過其他 API 生成,例如通過 FormData 對象獲取上傳的文件。而 ArrayBuffer 是 JavaScript 中的一個對象類型,用於表示一個通用的、固定長度的二進制數據緩衝區。我們可以通過 ArrayBuffer 來操作和處理文件的二進制數據。 

代碼如下:

import React, { useState } from 'react';

function FileInput() {
  const [fileContent, setFileContent] = useState('');

  // 讀取文件內容到ArrayBuffer
  function readFileToArrayBuffer(file) {
    return new Promise((resolve, reject) ={
      const reader = new FileReader();

      // 註冊文件讀取完成後的回調函數
      reader.onload = function(event) {
        const arrayBuffer = event.target.result;
        resolve(arrayBuffer);
      };

      // 讀取文件內容到ArrayBuffer
      reader.readAsArrayBuffer(file);
    });
  }

  // 將ArrayBuffer轉爲十六進制字符串
  function arrayBufferToHexString(arrayBuffer) {
    const uint8Array = new Uint8Array(arrayBuffer);
    let hexString = '';
    for (let i = 0; i < uint8Array.length; i++) {
      const hex = uint8Array[i].toString(16).padStart(2, '0');
      hexString += hex;
    }
    return hexString;
  }

  // 處理文件選擇事件
  function handleFileChange(event) {
    const file = event.target.files[0];  // 獲取選中的文件

    if (file) {
      readFileToArrayBuffer(file)
        .then(arrayBuffer ={
          const hexString = arrayBufferToHexString(arrayBuffer);
          setFileContent(hexString);
        })
        .catch(error ={
          console.error('文件讀取失敗:', error);
        });
    } else {
      setFileContent('請選擇一個文件');
    }
  }

  return (
    <div>
      <input type="file" onChange={handleFileChange} />
      <div>
        <h4>文件內容:</h4>
        <pre>{fileContent}</pre>
      </div>
    </div>
  );
}

export default FileInput;

上面代碼裏,我們創建了一個名爲 FileInput 的函數式組件。該組件包含一個文件選擇框和一個用於顯示文件內容的 <pre> 元素。當用戶選擇文件時,通過 FileReader 將文件內容讀取爲 ArrayBuffer,然後將 ArrayBuffer 轉換爲十六進制字符串,並將結果顯示在頁面上。

使用 FileReader 進行文件讀取

FileReader 是前端瀏覽器提供的一個 API,用於讀取文件內容。通過 FileReader,我們可以通過異步方式讀取文件,並將文件內容轉換爲可用的數據形式,比如文本數據或二進制數據。FileReader 提供了一些讀取文件的方法,例如 readAsText()、readAsArrayBuffer() 等,可以根據需要選擇合適的方法來讀取文件內容。

將文件流展示在前端頁面中

一旦我們成功地讀取了文件的內容,就可以將文件流展示在前端頁面上。具體的展示方式取決於文件的類型。例如,對於文本文件,可以直接將其內容顯示在頁面的文本框或區域中;對於圖片文件,可以使用 <img> 標籤展示圖片;對於音視頻文件,可以使用 <video><audio> 標籤來播放。通過將文件流展示在前端頁面上,我們可以實現在線預覽和查看文件內容的功能。

好的,這一部分就基本介紹完畢,總結一下。前端文件操作流是處理大型文件的一種常見方式,他可以通過數據流的方式對文件進行操作。Blob對象 和 ArrayBuffer是處理二進制數據的重要工具。而FileReader則是讀取文件內容的的關鍵組件。通過這些技術,我們可以方便的在前端頁面上進行操作或者文件展示。

二、文件切片下載

這一步就進入到我們今天文章主題了,先來主要的看下流程

graph LR
A(開始) --> B{選擇文件}
B -- 用戶選擇文件 --> C[切割文件爲多個切片]
C --> D{上傳切片}
D -- 上傳完成 --> E[合併切片爲完整文件]
E -- 文件合併完成 --> F(上傳成功)
D -- 上傳中斷 --> G{保存上傳進度}
G -- 上傳恢復 --> D
G -- 取消上傳 --> H(上傳取消)

傳統文件下載的性能問題

文件切片下載是一種提升文件下載效率的技術,通過將大文件分割成多個小片段(切片),並使用多個併發請求同時下載這些切片,從而加快整體下載速度。

傳統的文件下載方式對於大文件來說存在性能問題。當用戶請求下載一個大文件時,服務器需要將整個文件發送給客戶端。這會導致以下幾個問題:

  1. 較長的等待時間:大文件需要較長的時間來傳輸到客戶端,用戶需要等待很長時間才能開始使用文件。

  2. 網絡阻塞:由於下載過程中佔用了網絡帶寬,其他用戶可能會遇到下載速度慢的問題。

  3. 斷點續傳困難:如果下載過程中出現網絡故障或者用戶中斷下載,需要重新下載整個文件,無法繼續之前的下載進度。

利用文件切片提升下載效率

文件切片下載通過將文件分割成多個小片段,每個片段大小通常在幾百 KB 到幾 MB 之間。然後客戶端通過多個併發請求同時下載這些片段。這樣做的好處是:

  1. 快速啓動:客戶端可以快速開始下載,因爲只需要下載第一個切片即可。

  2. 併發下載:通過使用多個併發請求下載切片,可以充分利用帶寬,並提高整體下載速度。

  3. 斷點續傳:如果下載中斷,客戶端只需要重新下載中斷的切片,而不需要重新下載整個文件。

切片上傳代碼示例:

const [selectedFile, setSelectedFile] = useState(null); 
const [progress, setProgress] = useState(0);
 // 處理文件選擇事件
 function handleFileChange(event) {
   setSelectedFile(event.target.files[0]);
 }

 // 處理文件上傳事件
 function handleFileUpload() {
   if (selectedFile) {
     // 計算切片數量和每個切片的大小
     const fileSize = selectedFile.size;
     const chunkSize = 1024 * 1024; // 設置切片大小爲1MB
     const totalChunks = Math.ceil(fileSize / chunkSize);

     // 創建FormData對象,並添加文件信息
     const formData = new FormData();
     formData.append('file', selectedFile);
     formData.append('totalChunks', totalChunks);

     // 循環上傳切片
     for (let chunkNumber = 0; chunkNumber < totalChunks; chunkNumber++) {
       const start = chunkNumber * chunkSize;
       const end = Math.min(start + chunkSize, fileSize);
       const chunk = selectedFile.slice(start, end);
       formData.append(`chunk-${chunkNumber}`, chunk, selectedFile.name);
     }

     // 發起文件上傳請求
     axios.post('/upload', formData, {
       onUploadProgress: progressEvent ={
         const progress = Math.round((progressEvent.loaded / progressEvent.total) * 100);
         setProgress(progress);
       }
     })
       .then(response ={
         console.log('文件上傳成功:', response.data);
       })
       .catch(error ={
         console.error('文件上傳失敗:', error);
       });
   }
 }

當涉及到切片上傳和下載時,前端使用的技術通常是基於前端庫或框架提供的文件處理功能,結合後端服務實現。

上面代碼裏我們提到了文件如何切片上傳。

實現客戶端切片下載的方案

實現客戶端切片下載的基本方案如下:

  1. 服務器端將大文件切割成多個切片,併爲每個切片生成唯一的標識符。

  2. 客戶端發送請求獲取切片列表,同時開始下載第一個切片。

  3. 客戶端在下載過程中,根據切片列表發起併發請求下載其他切片,並逐漸拼接合並下載的數據。

  4. 當所有切片都下載完成後,客戶端將下載的數據合併爲完整的文件。

代碼示例:

function downloadFile() {
  // 發起文件下載請求
  fetch('/download'{
    method: 'GET',
    headers: {
      'Content-Type''application/json',
    },
  })
    .then(response => response.json())
    .then(data ={
      const totalSize = data.totalSize;
      const totalChunks = data.totalChunks;

      let downloadedChunks = 0;
      let chunks = [];

      // 下載每個切片
      for (let chunkNumber = 0; chunkNumber < totalChunks; chunkNumber++) {
        fetch(`/download/${chunkNumber}`{
          method: 'GET',
        })
          .then(response => response.blob())
          .then(chunk ={
            downloadedChunks++;
            chunks.push(chunk);

            // 當所有切片都下載完成時
            if (downloadedChunks === totalChunks) {
              // 合併切片
              const mergedBlob = new Blob(chunks);

              // 創建對象 URL,生成下載鏈接
              const downloadUrl = window.URL.createObjectURL(mergedBlob);

              // 創建 <a> 元素並設置屬性
              const link = document.createElement('a');
              link.href = downloadUrl;
              link.setAttribute('download''file.txt');

              // 模擬點擊下載
              link.click();

              // 釋放資源
              window.URL.revokeObjectURL(downloadUrl);
            }
          });
      }
    })
    .catch(error ={
      console.error('文件下載失敗:', error);
    });
}

我們看下代碼,首先使用BLOB對象創建一共對象 URL,用於生成下載連接,然後創建a標籤並且設置href的屬性爲剛剛創建的對象 URL, 繼續設置a標籤的download屬性是文件名,方便點擊的時候自動下載文件。

顯示下載進度和完成狀態

爲了顯示下載進度和完成狀態,可以在客戶端實現以下功能:

  1. 顯示進度條:客戶端可以通過監聽每個切片的下載進度來計算整體下載進度,並實時更新進度條的顯示。

  2. 顯示完成狀態:當所有切片都下載完成後,客戶端可以顯示下載完成的狀態,例如顯示一個完成的圖標或者文本。

這裏我們可以繼續接着切片上傳代碼示例裏的繼續寫。

代碼示例:

  // 處理文件下載事件
  function handleFileDownload() {
    axios.get('/download'{
      responseType: 'blob',
      onDownloadProgress: progressEvent ={
        const progress = Math.round((progressEvent.loaded / progressEvent.total) * 100);
        setProgress(progress);
      }
    })
      .then(response ={
        // 創建一個臨時的URL對象用於下載
        const url = window.URL.createObjectURL(new Blob([response.data]));
        const link = document.createElement('a');
        link.href = url;
        link.setAttribute('download''file.txt');
        document.body.appendChild(link);
        link.click();
        document.body.removeChild(link);
      })
      .catch(error ={
        console.error('文件下載失敗:', error);
      });
  }
  
  
  <button onClick={handleFileDownload}>下載文件</button>
  <div>進度:{progress}%</div>

三、大文件上傳的問題與解決方案

傳統的文件上傳方式存在的問題

前端文件切片上傳的優勢

實現前端切片上傳的方法

  1. - 使用 JavaScript 的 `File API` 獲取文件對象,並使用 `Blob.prototype.slice()` 方法將文件切割爲多個切片。
const [file, setFile] = useState(null);  //用來存放我本地上傳的文件

const chunkSize = 1024 * 1024; // 1MB 切片大小

  const upload = () ={
    if (!file) {
      alert("請選擇要上傳的文件!");
      return;
    }

    const chunkSize = 1024 * 1024; // 1MB

    let start = 0;
    let end = Math.min(chunkSize, file.size);

    while (start < file.size) {
      const chunk = file.slice(start, end);
      
      // 創建FormData對象
      const formData = new FormData();
      formData.append('file', chunk);

      // 發送切片到服務器
      fetch('上傳接口xxxx'{
        method: 'POST',
        body: formData
      })
      .then(response => response.json())
      .then(data ={
        console.log(data);
        // 處理響應結果
      })
      .catch(error ={
        console.error(error);
        // 處理錯誤
      });

      start = end;
      end = Math.min(start + chunkSize, file.size);
    }
  };
  
 return (
    <div>
      <input type="file" onChange={handleFileChange} />
      <button onClick={upload}>上傳</button>
    </div>
  );
}

在上面的代碼中,創建了一個名爲Upload的函數組件。它使用了 React 的useState鉤子來管理選中的文件。

通過onChange事件監聽文件輸入框的變化,並在handleFileChange函數中獲取選擇的文件,並更新file狀態。

點擊 “上傳” 按鈕時,調用upload函數。它與之前的示例代碼類似,將文件切割爲多個大小相等的切片,並使用FormData對象和fetch函數發送切片數據到服務器。

實現斷點續傳的技術:記錄和恢復上傳狀態

import React, { useState, useRef, useEffect } from 'react';

function Upload() {
  const [file, setFile] = useState(null);
  const [uploadedChunks, setUploadedChunks] = useState([]);
  const [uploading, setUploading] = useState(false);
  const uploadRequestRef = useRef();

  const handleFileChange = (event) ={
    const selectedFile = event.target.files[0];
    setFile(selectedFile);
  };

  const uploadChunk = (chunk) ={
    // 創建FormData對象
    const formData = new FormData();
    formData.append('file', chunk);

    // 發送切片到服務器
    return fetch('your-upload-url'{
      method: 'POST',
      body: formData
    })
    .then(response => response.json())
    .then(data ={
      console.log(data);
      // 處理響應結果
      return data;
    });
  };

  const upload = async () ={
    if (!file) {
      alert("請選擇要上傳的文件!");
      return;
    }

    const chunkSize = 1024 * 1024; // 1MB
    const totalChunks = Math.ceil(file.size / chunkSize);

    let start = 0;
    let end = Math.min(chunkSize, file.size);

    setUploading(true);

    for (let i = 0; i < totalChunks; i++) {
      const chunk = file.slice(start, end);
      const uploadedChunkIndex = uploadedChunks.indexOf(i);

      if (uploadedChunkIndex === -1) {
        try {
          const response = await uploadChunk(chunk);
          setUploadedChunks((prevChunks) =[...prevChunks, i]);

          // 保存已上傳的切片信息到本地存儲
          localStorage.setItem('uploadedChunks', JSON.stringify(uploadedChunks));
        } catch (error) {
          console.error(error);
          // 處理錯誤
        }
      }

      start = end;
      end = Math.min(start + chunkSize, file.size);
    }

    setUploading(false);

    // 上傳完畢,清除本地存儲的切片信息
    localStorage.removeItem('uploadedChunks');
  };

  useEffect(() ={
    const storedUploadedChunks = localStorage.getItem('uploadedChunks');

    if (storedUploadedChunks) {
      setUploadedChunks(JSON.parse(storedUploadedChunks));
    }
  }[]);

  return (
    <div>
      <input type="file" onChange={handleFileChange} />
      <button onClick={upload} disabled={uploading}>
        {uploading ? '上傳中...' : '上傳'}
      </button>
    </div>
  );
}

首先,使用useState鉤子創建了一個uploadedChunks狀態來保存已上傳的切片索引數組。初始值爲空數組。

然後,我們使用useRef鉤子創建了一個uploadRequestRef引用,用於存儲當前的上傳請求。

handleFileChange函數中,我們更新了file狀態以選擇要上傳的文件。

uploadChunk函數中,我們發送切片到服務器,並返回一個Promise對象來處理響應結果。

upload函數中,我們添加了斷點續傳的邏輯。首先,我們獲取切片的總數,並設置uploading狀態爲true來禁用上傳按鈕。

然後,我們使用for循環遍歷所有切片。對於每個切片,我們檢查uploadedChunks數組中是否已經包含該索引,如果不包含,則進行上傳操作。

在上傳切片之後,我們將已上傳的切片索引添加到uploadedChunks數組,並使用localStorage保存已上傳的切片信息。

最後,在上傳完畢後,我們將uploading狀態設爲false,並清除本地存儲的切片信息。

在實現大文件上傳時要考慮服務器端的處理能力和存儲空間,以及安全性問題。同時,爲了保障斷點續傳的準確性,應該儘量避免併發上傳相同文件的情況,可以採用文件唯一標識符或用戶會話標識符進行區分。

四、優化用戶體驗:切片下載與上傳的應用場景

後臺管理系統中的文件下載和上傳:

圖片 / 視頻上傳和預覽:

雲存儲和雲盤應用中的文件操作:

作者:狗頭大軍之江蘇分軍 https://juejin.cn/post/7255189826226602045

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/vQbNHG4BivPz0MXmi47Kng