前端文件相關總結

  先引用掘金上的一個總結,將前端會遇到的文件相關的知識點間的關係串聯了起來。

  前端技術提供了一些高效的解決方案:文件流操作和切片下載與上傳。

1. 文件基本操作

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

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

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

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

示例代碼:

 1 import React, { useState } from 'react';
 2 
 3 function FileInput() {
 4   const [fileContent, setFileContent] = useState('');
 5 
 6   // 讀取文件內容到ArrayBuffer
 7   function readFileToArrayBuffer(file) {
 8     return new Promise((resolve, reject) => {
 9       const reader = new FileReader();
10 
11       // 註冊文件讀取完成後的回調函數
12       reader.onload = function(event) {
13         const arrayBuffer = event.target.result;
14         resolve(arrayBuffer);
15       };
16 
17       // 讀取文件內容到ArrayBuffer
18       reader.readAsArrayBuffer(file);
19     });
20   }
21 
22   // 將ArrayBuffer轉爲十六進制字符串
23   function arrayBufferToHexString(arrayBuffer) {
24     const uint8Array = new Uint8Array(arrayBuffer);
25     let hexString = '';
26     for (let i = 0; i < uint8Array.length; i++) {
27       const hex = uint8Array[i].toString(16).padStart(2, '0');
28       hexString += hex;
29     }
30     return hexString;
31   }
32 
33   // 處理文件選擇事件
34   function handleFileChange(event) {
35     const file = event.target.files[0];  // 獲取選中的文件
36 
37     if (file) {
38       readFileToArrayBuffer(file)
39         .then(arrayBuffer => {
40           const hexString = arrayBufferToHexString(arrayBuffer);
41           setFileContent(hexString);
42         })
43         .catch(error => {
44           console.error('文件讀取失敗:', error);
45         });
46     } else {
47       setFileContent('請選擇一個文件');
48     }
49   }
50 
51   return (
52     <div>
53       <input type="file" onChange={handleFileChange} />
54       <div>
55         <h4>文件內容:</h4>
56         <pre>{fileContent}</pre>
57       </div>
58     </div>
59   );
60 }
61 
62 export default FileInput;

1.3 使用 FileReader 進行文件讀取

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

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

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

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

2. 文件切片

流程:

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

  傳統文件整體上傳下載的弊端:

  等待較長、網絡佔用、續傳困難。

  切片上傳下載好處:

  • 快速啓動:客戶端可以快速開始下載,因爲只需要下載第一個切片即可。
  • 併發下載:通過使用多個併發請求下載切片,可以充分利用帶寬,並提高整體下載速度。
  • 斷點續傳:如果下載中斷,客戶端只需要重新下載中斷的切片,而不需要重新下載整個文件。

2.1 切片上傳-藉助FormData

 1 const [selectedFile, setSelectedFile] = useState(null); 
 2 const [progress, setProgress] = useState(0);
 3  // 處理文件選擇事件
 4  function handleFileChange(event) {
 5    setSelectedFile(event.target.files[0]);
 6  }
 7 
 8  // 處理文件上傳事件
 9  function handleFileUpload() {
10    if (selectedFile) {
11      // 計算切片數量和每個切片的大小
12      const fileSize = selectedFile.size;
13      const chunkSize = 1024 * 1024; // 設置切片大小爲1MB
14      const totalChunks = Math.ceil(fileSize / chunkSize);
15 
16      // 創建FormData對象,並添加文件信息
17      const formData = new FormData();
18      formData.append('file', selectedFile);
19      formData.append('totalChunks', totalChunks);
20 
21      // 循環上傳切片
22      for (let chunkNumber = 0; chunkNumber < totalChunks; chunkNumber++) {
23        const start = chunkNumber * chunkSize;
24        const end = Math.min(start + chunkSize, fileSize);
25        const chunk = selectedFile.slice(start, end);
26        formData.append(`chunk-${chunkNumber}`, chunk, selectedFile.name);
27      }
28 
29      // 發起文件上傳請求
30      axios.post('/upload', formData, {
31        onUploadProgress: progressEvent => {
32          const progress = Math.round((progressEvent.loaded / progressEvent.total) * 100);
33          setProgress(progress);
34        }
35      })
36        .then(response => {
37          console.log('文件上傳成功:', response.data);
38        })
39        .catch(error => {
40          console.error('文件上傳失敗:', error);
41        });
42    }
43  }

2.2 切片下載

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

  1. 服務器端將大文件切割成多個切片,併爲每個切片生成唯一的標識符。
  2. 客戶端發送請求獲取切片列表,同時開始下載第一個切片。
  3. 客戶端在下載過程中,根據切片列表發起併發請求下載其他切片,並逐漸拼接合並下載的數據。
  4. 當所有切片都下載完成後,客戶端藉助blob將下載的數據合併爲完整的文件。
 1 function downloadFile() {
 2   // 發起文件下載請求
 3   fetch('/download', {
 4     method: 'GET',
 5     headers: {
 6       'Content-Type': 'application/json',
 7     },
 8   })
 9     .then(response => response.json())
10     .then(data => {
11       const totalSize = data.totalSize;
12       const totalChunks = data.totalChunks;
13 
14       let downloadedChunks = 0;
15       let chunks = [];
16 
17       // 下載每個切片
18       for (let chunkNumber = 0; chunkNumber < totalChunks; chunkNumber++) {
19         fetch(`/download/${chunkNumber}`, {
20           method: 'GET',
21         })
22           .then(response => response.blob())
23           .then(chunk => {
24             downloadedChunks++;
25             chunks.push(chunk);
26 
27             // 當所有切片都下載完成時
28             if (downloadedChunks === totalChunks) {
29               // 合併切片
30               const mergedBlob = new Blob(chunks);
31 
32               // 創建對象 URL,生成下載鏈接
33               const downloadUrl = window.URL.createObjectURL(mergedBlob);
34 
35               // 創建 <a> 元素並設置屬性
36               const link = document.createElement('a');
37               link.href = downloadUrl;
38               link.setAttribute('download', 'file.txt');
39 
40               // 模擬點擊下載
41               link.click();
42 
43               // 釋放資源
44               window.URL.revokeObjectURL(downloadUrl);
45             }
46           });
47       }
48     })
49     .catch(error => {
50       console.error('文件下載失敗:', error);
51     });
52 }

2.3 顯示下載進度和完成狀態

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

  1. 顯示進度條:客戶端可以通過監聽每個切片的下載進度來計算整體下載進度,並實時更新進度條的顯示。
  2. 顯示完成狀態:當所有切片都下載完成後,客戶端可以顯示下載完成的狀態,例如顯示一個完成的圖標或者文本。

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

  • 當用戶點擊下載按鈕時,通過 handleFileDownload 函數處理文件下載事件。
  • 在 handleFileDownload 函數中,使用 axios 庫發起文件下載請求,並設置 responseType: 'blob' 表示返回二進制數據。
  • 通過監聽 onDownloadProgress 屬性獲取下載進度,並更新進度條的顯示。
  • 下載完成後,創建一個臨時的 URL 對象用於下載,並通過動態創建 <a> 元素模擬點擊下載。
 1 function handleFileDownload() {
 2     axios.get('/download', {
 3       responseType: 'blob',
 4       onDownloadProgress: progressEvent => {
 5         const progress = Math.round((progressEvent.loaded / progressEvent.total) * 100);
 6         setProgress(progress);
 7       }
 8     })
 9       .then(response => {
10         // 創建一個臨時的URL對象用於下載
11         const url = window.URL.createObjectURL(new Blob([response.data]));
12         const link = document.createElement('a');
13         link.href = url;
14         link.setAttribute('download', 'file.txt');
15         document.body.appendChild(link);
16         link.click();
17         document.body.removeChild(link);
18       })
19       .catch(error => {
20         console.error('文件下載失敗:', error);
21       });
22   }
23   
24   
25   <button onClick={handleFileDownload}>下載文件</button>
26   <div>進度:{progress}%</div>

3. 大文件上傳下載

3.1 大文件切片上傳

- 使用 JavaScript 的 `File API` 獲取文件對象,並使用 `Blob.prototype.slice()` 方法將文件切割爲多個切片。

  • 使用 FormData 對象將切片數據通過 AJAX 或 Fetch API 發送到服務器。
  • 在後端服務器上接收切片並保存到臨時存儲中,等待後續合併。
  • 在客戶端通過監聽上傳進度事件,在進度條或提示中展示上傳進度。 代碼示例
 1 const [file, setFile] = useState(null);  //用來存放我本地上傳的文件
 2 
 3 const chunkSize = 1024 * 1024; // 1MB 切片大小
 4 
 5   const upload = () => {
 6     if (!file) {
 7       alert("請選擇要上傳的文件!");
 8       return;
 9     }
10 
11     const chunkSize = 1024 * 1024; // 1MB
12 
13     let start = 0;
14     let end = Math.min(chunkSize, file.size);
15 
16     while (start < file.size) {
17       const chunk = file.slice(start, end);
18       
19       // 創建FormData對象
20       const formData = new FormData();
21       formData.append('file', chunk);
22 
23       // 發送切片到服務器
24       fetch('上傳接口xxxx', {
25         method: 'POST',
26         body: formData
27       })
28       .then(response => response.json())
29       .then(data => {
30         console.log(data);
31         // 處理響應結果
32       })
33       .catch(error => {
34         console.error(error);
35         // 處理錯誤
36       });
37 
38       start = end;
39       end = Math.min(start + chunkSize, file.size);
40     }
41   };
42   
43  return (
44     <div>
45       <input type="file" onChange={handleFileChange} />
46       <button onClick={upload}>上傳</button>
47     </div>
48   );
49 }

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

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

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

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

  • 在前端,可以使用 localStorage 或 sessionStorage 來存儲已上傳的切片信息,包括已上傳的切片索引、切片大小等。
  • 每次上傳前,先檢查本地存儲中是否存在已上傳的切片信息,若存在,則從斷點處繼續上傳。
  • 在後端,可以使用一個臨時文件夾或數據庫來記錄已接收到的切片信息,包括已上傳的切片索引、切片大小等。
  • 在上傳完成前,保存上傳狀態,以便在上傳中斷後能夠恢復上傳進度
 1 import React, { useState, useRef, useEffect } from 'react';
 2 
 3 function Upload() {
 4   const [file, setFile] = useState(null);
 5   const [uploadedChunks, setUploadedChunks] = useState([]);
 6   const [uploading, setUploading] = useState(false);
 7   const uploadRequestRef = useRef();
 8 
 9   const handleFileChange = (event) => {
10     const selectedFile = event.target.files[0];
11     setFile(selectedFile);
12   };
13 
14   const uploadChunk = (chunk) => {
15     // 創建FormData對象
16     const formData = new FormData();
17     formData.append('file', chunk);
18 
19     // 發送切片到服務器
20     return fetch('your-upload-url', {
21       method: 'POST',
22       body: formData
23     })
24     .then(response => response.json())
25     .then(data => {
26       console.log(data);
27       // 處理響應結果
28       return data;
29     });
30   };
31 
32   const upload = async () => {
33     if (!file) {
34       alert("請選擇要上傳的文件!");
35       return;
36     }
37 
38     const chunkSize = 1024 * 1024; // 1MB
39     const totalChunks = Math.ceil(file.size / chunkSize);
40 
41     let start = 0;
42     let end = Math.min(chunkSize, file.size);
43 
44     setUploading(true);
45 
46     for (let i = 0; i < totalChunks; i++) {
47       const chunk = file.slice(start, end);
48       const uploadedChunkIndex = uploadedChunks.indexOf(i);
49 
50       if (uploadedChunkIndex === -1) {
51         try {
52           const response = await uploadChunk(chunk);
53           setUploadedChunks((prevChunks) => [...prevChunks, i]);
54 
55           // 保存已上傳的切片信息到本地存儲
56           localStorage.setItem('uploadedChunks', JSON.stringify(uploadedChunks));
57         } catch (error) {
58           console.error(error);
59           // 處理錯誤
60         }
61       }
62 
63       start = end;
64       end = Math.min(start + chunkSize, file.size);
65     }
66 
67     setUploading(false);
68 
69     // 上傳完畢,清除本地存儲的切片信息
70     localStorage.removeItem('uploadedChunks');
71   };
72 
73   useEffect(() => {
74     const storedUploadedChunks = localStorage.getItem('uploadedChunks');
75 
76     if (storedUploadedChunks) {
77       setUploadedChunks(JSON.parse(storedUploadedChunks));
78     }
79   }, []);
80 
81   return (
82     <div>
83       <input type="file" onChange={handleFileChange} />
84       <button onClick={upload} disabled={uploading}>
85         {uploading ? '上傳中...' : '上傳'}
86       </button>
87     </div>
88   );
89 }

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

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

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

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

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

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

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

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

 

本文參考掘金:https://juejin.cn/post/7255189826226602045

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章