前端文件下載,簡單來說,就是讓用戶點擊一個按鈕或者鏈接,瀏覽器就能自動下載一個文件到本地。這事兒聽起來簡單,但實現起來還是有幾種不同的路子。
直接輸出解決方案即可
-
最簡單的: 標簽 + download 屬性
立即學習“前端免費學習筆記(深入)”;
這是最直接,也是最常用的方法。你只需要一個標簽,然后設置href屬性為文件的URL,再加一個download屬性,指定下載的文件名就行了。
<a href="path/to/your/file.pdf" download="自定義文件名.pdf">下載PDF</a>
這種方法的好處是簡單粗暴,瀏覽器原生支持,兼容性好。但缺點也很明顯,它只能下載同源的文件,如果文件在不同的域名下,瀏覽器會阻止下載,除非服務器允許跨域。而且,這種方式無法控制下載進度,也無法自定義請求頭。
-
利用 window.location.href
這種方法其實跟第一種類似,只不過是通過JavaScript來觸發下載。
function downloadFile(url) { window.location.href = url; } // 調用 downloadFile('path/to/your/file.pdf');
它的優缺點和第一種方法基本一樣,也是簡單方便,但受限于同源策略,無法控制下載過程。
-
使用 Blob 對象和 URL.createObjectURL
這種方法是目前最靈活,也是最常用的方法。它可以下載任何文件,無論是否同源,而且可以自定義請求頭,控制下載進度。
function downloadFile(url, filename) { fetch(url, { method: 'GET', // 可以添加自定義請求頭 headers: { 'Authorization': 'Bearer your_token' } }) .then(res => res.blob()) .then(blob => { const a = document.createElement('a'); const url = URL.createObjectURL(blob); a.href = url; a.download = filename; // 自定義文件名 document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); // 釋放URL對象 }); } // 調用 downloadFile('path/to/your/file.pdf', '自定義文件名.pdf');
這種方法的核心在于:
- fetch:發起網絡請求,獲取文件數據。可以設置自定義請求頭,比如Authorization,用于身份驗證。
- blob():將響應數據轉換為 Blob 對象。Blob 對象表示一個不可變的、原始數據的類文件對象。
- URL.createObjectURL(blob):創建一個指向 Blob 對象的URL。這個URL可以被標簽使用。
- URL.revokeObjectURL(url):釋放URL對象,防止內存泄漏。
這種方法的優點是:
- 可以下載任何文件,不受同源策略限制。
- 可以自定義請求頭,進行身份驗證。
- 可以控制下載進度(通過監聽 fetch 的 onprogress 事件)。
- 靈活性高,可以根據需求進行定制。
缺點是:
- 代碼相對復雜。
- 需要處理跨域問題(如果文件在不同的域名下)。
前端下載文件,這三種方法各有千秋,具體選擇哪種,取決于你的實際需求。 如果只是簡單下載同源文件,標簽 + download 屬性足夠了。 如果需要下載不同源的文件,或者需要自定義請求頭,那么 Blob 對象和 URL.createObjectURL 是更好的選擇。
下載大文件時如何優化用戶體驗?
下載大文件,最怕的就是用戶傻等,不知道下載進度。所以,優化用戶體驗的關鍵在于:
-
顯示下載進度:
使用 Blob 對象和 URL.createObjectURL 方法時,可以通過監聽 fetch 的 onprogress 事件來獲取下載進度。
fetch(url, { method: 'GET' }) .then(res => { const contentLength = res.headers.get('content-length'); let receivedLength = 0; const reader = res.body.getReader(); return new ReadableStream({ start(controller) { function push() { reader.read().then(({ done, value }) => { if (done) { controller.close(); return; } receivedLength += value.length; // 計算下載進度 const progress = (receivedLength / contentLength * 100).toFixed(2); console.log(`下載進度:${progress}%`); // 更新ui顯示下載進度 updateProgressUI(progress); controller.enqueue(value); push(); }); } push(); } }) .pipeTo(new WritableStream({ write(chunk) { // 這里可以處理下載的數據,比如寫入文件 } })) .then(() => { console.log('下載完成'); }); }); function updateProgressUI(progress) { // 更新UI顯示下載進度,比如更新進度條 document.getElementById('progress-bar').style.width = `${progress}%`; }
這段代碼的關鍵在于使用 ReadableStream 來讀取響應數據,并在讀取過程中計算下載進度,然后更新UI顯示。
-
斷點續傳:
如果文件非常大,可以考慮支持斷點續傳。斷點續傳的原理是在請求頭中添加 Range 字段,告訴服務器從哪個位置開始傳輸數據。
function downloadFileWithResume(url, filename, start) { fetch(url, { method: 'GET', headers: { 'Range': `bytes=${start}-` } }) .then(res => res.blob()) .then(blob => { // ... 后續處理與之前相同 }); }
服務器也需要支持 Range 請求頭,并返回 206 Partial Content 狀態碼。
-
使用 Web Workers:
下載大文件可能會阻塞主線程,導致頁面卡頓。可以使用 Web Workers 將下載任務放在后臺線程中執行,避免阻塞主線程。
// 主線程 const worker = new Worker('download-worker.JS'); worker.postMessage({ url: 'path/to/your/file.pdf', filename: '自定義文件名.pdf' }); worker.onmessage = function(event) { const data = event.data; if (data.progress) { // 更新UI顯示下載進度 updateProgressUI(data.progress); } else if (data.complete) { console.log('下載完成'); } }; // download-worker.js self.addEventListener('message', function(event) { const { url, filename } = event.data; downloadFile(url, filename, function(progress) { self.postMessage({ progress: progress }); }, function() { self.postMessage({ complete: true }); }); }); function downloadFile(url, filename, progressCallback, completeCallback) { // ... 下載文件的代碼,并在下載過程中調用 progressCallback 更新進度 // 下載完成后調用 completeCallback }
這種方法將下載任務放在后臺線程中執行,避免阻塞主線程,提高頁面響應速度。
如何處理下載失敗的情況?
下載文件,難免會遇到各種各樣的問題,比如網絡中斷、服務器錯誤等等。所以,我們需要處理下載失敗的情況,給用戶一個友好的提示。
-
監聽 fetch 的錯誤:
fetch 函數會返回一個 promise 對象,如果請求失敗,Promise 對象會 reject。我們可以使用 catch 方法來捕獲錯誤。
fetch(url, { method: 'GET' }) .then(res => res.blob()) .then(blob => { // ... 下載成功的處理 }) .catch(Error => { console.error('下載失敗:', error); // 顯示錯誤提示 alert('下載失敗,請稍后重試'); });
-
檢查 http 狀態碼:
如果服務器返回的 HTTP 狀態碼不是 200,也表示下載失敗。我們可以檢查 response.ok 屬性來判斷請求是否成功。
fetch(url, { method: 'GET' }) .then(res => { if (!res.ok) { throw new Error(`HTTP 錯誤:${res.status}`); } return res.blob(); }) .then(blob => { // ... 下載成功的處理 }) .catch(error => { console.error('下載失敗:', error); // 顯示錯誤提示 alert('下載失敗,請稍后重試'); });
-
重試下載:
如果下載失敗,可以嘗試重新下載。可以設置一個重試次數,如果超過重試次數仍然失敗,就放棄下載。
function downloadFileWithRetry(url, filename, retryCount = 3) { fetch(url, { method: 'GET' }) .then(res => { if (!res.ok) { throw new Error(`HTTP 錯誤:${res.status}`); } return res.blob(); }) .then(blob => { // ... 下載成功的處理 }) .catch(error => { console.error('下載失敗:', error); if (retryCount > 0) { console.log(`嘗試重新下載,剩余次數:${retryCount}`); // 延遲一段時間后重新下載 setTimeout(() => { downloadFileWithRetry(url, filename, retryCount - 1); }, 1000); } else { // 顯示錯誤提示 alert('下載失敗,請稍后重試'); } }); }
這種方法在下載失敗后,會嘗試重新下載,提高下載成功率。
-
記錄錯誤日志:
為了方便排查問題,可以將下載失敗的錯誤信息記錄到日志中。可以使用 console.error 或者第三方的日志庫來記錄錯誤信息。
fetch(url, { method: 'GET' }) .then(res => res.blob()) .then(blob => { // ... 下載成功的處理 }) .catch(error => { console.error('下載失敗:', error); // 記錄錯誤日志 logError('下載文件失敗', { url: url, error: error.message }); // 顯示錯誤提示 alert('下載失敗,請稍后重試'); }); function logError(message, data) { // 將錯誤信息發送到服務器或者記錄到本地 console.log(message, data); }
通過記錄錯誤日志,可以方便地排查問題,提高代碼質量。