AJAX請(qǐng)求上傳下載進(jìn)度監(jiān)控實(shí)現(xiàn)方式
1. 前言
在日常 Web 開發(fā)中,AJAX(Asynchronous JavaScript and XML)被廣泛用于異步請(qǐng)求數(shù)據(jù),而無(wú)需刷新整個(gè)頁(yè)面。然而,當(dāng)涉及到上傳下載文件或執(zhí)行長(zhǎng)時(shí)間運(yùn)行的任務(wù)時(shí),為了提升用戶體驗(yàn)通常我們需要顯示執(zhí)行的進(jìn)度條,那么監(jiān)控請(qǐng)求的進(jìn)度就變得尤為重要。
這里博主給大家講解 XMLHttpRequest 和 Fetch API 以及 Axios封裝 在進(jìn)度監(jiān)控上不同的實(shí)現(xiàn)方式

進(jìn)度監(jiān)控的核心場(chǎng)景 :
1. 大文件上傳/下載
2. 實(shí)時(shí)數(shù)據(jù)傳輸(如視頻流)
3. 長(zhǎng)耗時(shí)API請(qǐng)求
4. 用戶交互反饋優(yōu)化
2. 基于XMLHttpRequest的進(jìn)度監(jiān)控
在 JavaScript 中,XMLHttpRequest 提供了 progress 事件,允許我們監(jiān)聽請(qǐng)求的進(jìn)度。
2.1 基礎(chǔ)版文件上傳監(jiān)控
<input type="file" id="fileInput">
<progress id="uploadProgress" value="0" max="100"></progress>
<script>
document.getElementById('fileInput').addEventListener('change', function(e) {
  const file = e.target.files[0];
  if (!file) return;
  const xhr = new XMLHttpRequest();
  const progressBar = document.getElementById('uploadProgress');
  xhr.upload.addEventListener('progress', (e) => {
    if (e.lengthComputable) {
      const percent = (e.loaded / e.total) * 100;
      progressBar.value = percent;
      console.log(`上傳進(jìn)度: ${percent.toFixed(1)}%`);
    }
  });
  xhr.addEventListener('load', () => {
    console.log('上傳完成');
  });
  xhr.open('POST', '/upload', true);
  xhr.send(file);
});
</script>2.2 增強(qiáng)版多事件監(jiān)控
我們還可以集合 progress 進(jìn)行多事件監(jiān)控,如下代碼:
function uploadWithProgress(file) {
  return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest();
    // 上傳進(jìn)度監(jiān)控
    xhr.upload.addEventListener('progress', (e) => {
      handleProgress('upload', e);
    });
    // 下載進(jìn)度監(jiān)控(當(dāng)服務(wù)器返回大數(shù)據(jù)時(shí))
    xhr.addEventListener('progress', (e) => {
      handleProgress('download', e);
    });
    xhr.addEventListener('error', reject);
    xhr.addEventListener('abort', reject);
    xhr.addEventListener('load', () => {
      if (xhr.status >= 200 && xhr.status < 300) {
        resolve(xhr.response);
      } else {
        reject(xhr.statusText);
      }
    });
    xhr.open('POST', '/upload');
    xhr.setRequestHeader('Content-Type', 'application/octet-stream');
    xhr.send(file);
    function handleProgress(type, e) {
      if (e.lengthComputable) {
        const percent = (e.loaded / e.total) * 100;
        console.log(`${type} progress: ${percent.toFixed(1)}%`);
      }
    }
  });
}3. 基于Fetch API 的進(jìn)度監(jiān)控
3.1 Fetch API + ReadableStream實(shí)現(xiàn)下載監(jiān)控
Fetch API 本身沒有直接提供進(jìn)度事件,但我們可以利用 ReadableStream 對(duì)響應(yīng)體進(jìn)行分段讀取,從而計(jì)算已加載的字節(jié)數(shù)。
當(dāng)?shù)谝淮握?qǐng)求鏈接 await fetch(url) 的時(shí)候通過獲取 headers 中 Content-Length 返回的請(qǐng)求資源總大小,再結(jié)合 response.body.getReader() 來(lái)讀取 body 內(nèi)容來(lái)實(shí)現(xiàn)!
具體參考代碼如下,小伙伴可以根據(jù)自身需求進(jìn)行調(diào)整:
async function downloadLargeFile(url) {
  const response = await fetch(url);
  const reader = response.body.getReader();
  const contentLength = +response.headers.get('Content-Length');
  let receivedLength = 0;
  const chunks = [];
  while(true) {
    const {done, value} = await reader.read();
    if (done) break;
    chunks.push(value);
    receivedLength += value.length;
    const percent = (receivedLength / contentLength) * 100;
    console.log(`下載進(jìn)度: ${percent.toFixed(1)}%`);
  }
  const blob = new Blob(chunks);
  return blob;
}
// 使用示例
downloadLargeFile('/large-file.zip')
  .then(blob => {
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = 'file.zip';
    a.click();
  });3.2 Fetch API 上傳進(jìn)度監(jiān)控(偽方案)
Fetch API 原生并不支持上傳進(jìn)度監(jiān)控。不過可以采用將文件包裝成一個(gè)自定義的 ReadableStream 來(lái)實(shí)現(xiàn) “偽”上傳進(jìn)度監(jiān)控 。
需要注意的是,由于各瀏覽器對(duì) Request 流處理的支持程度不一,該方法可能并非在所有環(huán)境下都能穩(wěn)定工作。博主建議非必要不要采用這種方式 ~
<!DOCTYPE html>
<html lang="zh">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Fetch API 上傳進(jìn)度監(jiān)控</title>
</head>
<body>
  <input type="file" id="fileInput">
  <button onclick="uploadFile()">上傳文件</button>
  <progress id="progressBar" value="0" max="100"></progress>
  <span id="progressText">0%</span>
  <script>
    function uploadFile() {
      const fileInput = document.getElementById('fileInput');
      const file = fileInput.files[0];
      if (!file) {
        alert('請(qǐng)選擇文件');
        return;
      }
      const progressBar = document.getElementById('progressBar');
      const progressText = document.getElementById('progressText');
      const total = file.size;
      let uploaded = 0;
      // 構(gòu)造一個(gè)自定義的 ReadableStream 來(lái)包裝文件流
      const stream = new ReadableStream({
        start(controller) {
          const reader = file.stream().getReader();
          function push() {
            reader.read().then(({ done, value }) => {
              if (done) {
                controller.close();
                return;
              }
              uploaded += value.byteLength;
              const percent = (uploaded / total) * 100;
              progressBar.value = percent;
              progressText.innerText = percent.toFixed(2) + '%';
              controller.enqueue(value);
              push();
            }).catch(error => {
              console.error('讀取文件錯(cuò)誤:', error);
              controller.error(error);
            });
          }
          push();
        }
      });
      // 使用 Fetch API 發(fā)送 POST 請(qǐng)求
      fetch('/upload', {
        method: 'POST',
        headers: {
          // 根據(jù)后端要求設(shè)置合適的 Content-Type
          // 注意:如果使用 FormData 上傳文件,瀏覽器會(huì)自動(dòng)設(shè)置 multipart/form-data
          'Content-Type': 'application/octet-stream'
        },
        body: stream
      })
      .then(response => response.json())
      .then(data => {
        console.log('上傳成功:', data);
        alert('上傳完成');
      })
      .catch(error => {
        console.error('上傳失敗:', error);
        alert('上傳失敗');
      });
    }
  </script>
</body>
</html>注意事項(xiàng)
- 上傳進(jìn)度:Fetch API 本身不提供上傳進(jìn)度事件,上述方法通過包裝文件流來(lái)模擬上傳進(jìn)度,但并非所有瀏覽器都支持這種方式,穩(wěn)定性可能不如 XMLHttpRequest
 - 內(nèi)容類型:如果后端要求 multipart/form-data 格式,建議仍采用 XMLHttpRequest 或使用 FormData 對(duì)象,因?yàn)樽远x流方式上傳數(shù)據(jù)可能需要后端特殊處理
 
4. Axios封裝進(jìn)度監(jiān)控方案
通過封裝 Axios 請(qǐng)求,可以同時(shí)監(jiān)聽上傳和下載的進(jìn)度,提升用戶體驗(yàn),再次之前我們先來(lái)看看未封裝前最原始的上傳和下載是如何實(shí)現(xiàn)的~
4.1 Axios 上傳進(jìn)度監(jiān)控
Axios 支持通過配置項(xiàng) onUploadProgress 來(lái)監(jiān)聽上傳進(jìn)度。以下示例展示了如何使用 Axios 上傳文件,并在頁(yè)面上顯示進(jìn)度信息:
<!DOCTYPE html>
<html lang="zh">
<head>
  <meta charset="UTF-8">
  <title>Axios 上傳進(jìn)度監(jiān)控</title>
</head>
<body>
  <h2>文件上傳(Axios)</h2>
  <input type="file" id="fileInput">
  <button onclick="uploadFile()">上傳文件</button>
  <br>
  <progress id="uploadProgress" value="0" max="100" style="width: 300px;"></progress>
  <span id="uploadText">0%</span>
  <!-- 引入 Axios 庫(kù) -->
  <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
  <script>
    function uploadFile() {
      const fileInput = document.getElementById('fileInput');
      const file = fileInput.files[0];
      if (!file) {
        alert('請(qǐng)選擇文件');
        return;
      }
      const formData = new FormData();
      formData.append('file', file);
      axios.post('/upload', formData, {
        headers: {
          'Content-Type': 'multipart/form-data'
        },
        onUploadProgress: function (progressEvent) {
          if (progressEvent.lengthComputable) {
            const percent = Math.round((progressEvent.loaded * 100) / progressEvent.total);
            document.getElementById('uploadProgress').value = percent;
            document.getElementById('uploadText').innerText = percent + '%';
          }
        }
      })
      .then(response => {
        alert('上傳成功');
        console.log(response.data);
      })
      .catch(error => {
        alert('上傳失敗');
        console.error(error);
      });
    }
  </script>
</body>
</html>代碼解釋:
使用 FormData 對(duì)象包裝上傳的文件;
通過 Axios 的 onUploadProgress 事件監(jiān)聽上傳過程,并實(shí)時(shí)更新進(jìn)度條和百分比顯示。
4.2 Axios 下載進(jìn)度監(jiān)控
同理,Axios 還支持 onDownloadProgress 事件來(lái)監(jiān)控文件下載進(jìn)度。下面的示例展示了如何通過 Axios 下載文件并實(shí)時(shí)顯示下載進(jìn)度:
<!DOCTYPE html>
<html lang="zh">
<head>
  <meta charset="UTF-8">
  <title>Axios 下載進(jìn)度監(jiān)控</title>
</head>
<body>
  <h2>文件下載(Axios)</h2>
  <button onclick="downloadFile()">下載文件</button>
  <br>
  <progress id="downloadProgress" value="0" max="100" style="width: 300px;"></progress>
  <span id="downloadText">0%</span>
  <!-- 引入 Axios 庫(kù) -->
  <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
  <script>
    function downloadFile() {
      axios.get('/download', {
        responseType: 'blob',  // 指定響應(yīng)數(shù)據(jù)類型為 Blob
        onDownloadProgress: function (progressEvent) {
          if (progressEvent.lengthComputable) {
            const percent = Math.round((progressEvent.loaded * 100) / progressEvent.total);
            document.getElementById('downloadProgress').value = percent;
            document.getElementById('downloadText').innerText = percent + '%';
          }
        }
      })
      .then(response => {
        // 創(chuàng)建一個(gè)臨時(shí)鏈接下載文件
        const url = window.URL.createObjectURL(new Blob([response.data]));
        const a = document.createElement('a');
        a.href = url;
        a.download = 'downloaded_file';
        document.body.appendChild(a);
        a.click();
        a.remove();
        window.URL.revokeObjectURL(url);
      })
      .catch(error => {
        alert('下載失敗');
        console.error(error);
      });
    }
  </script>
</body>
</html>代碼解釋:
1、我們通過 axios.get 請(qǐng)求下載文件,并設(shè)置 responseType 為 blob;
2、通過 onDownloadProgress 事件監(jiān)聽下載進(jìn)度,并更新進(jìn)度條;
3、下載完成后利用 Blob 和臨時(shí)鏈接觸發(fā)瀏覽器下載文件。
4.3 封裝 Axios 實(shí)例
為了在項(xiàng)目中更方便地使用進(jìn)度監(jiān)控功能,可以將 Axios 進(jìn)行封裝。例如,我們可以創(chuàng)建一個(gè) Axios 實(shí)例,并在請(qǐng)求配置中統(tǒng)一處理上傳和下載進(jìn)度
import axios from 'axios';
// 請(qǐng)求配置
const axiosInstance = axios.create({
  onUploadProgress: progressEvent => {
    const percent = Math.round(
      (progressEvent.loaded * 100) / progressEvent.total
    );
    console.log(`上傳進(jìn)度: ${percent}%`);
  },
  onDownloadProgress: progressEvent => {
    const percent = Math.round(
      (progressEvent.loaded * 100) / progressEvent.total
    );
    console.log(`下載進(jìn)度: ${percent}%`);
  }
});
// 封裝上傳方法
async function axiosUpload(file) {
  const formData = new FormData();
  formData.append('file', file);
  try {
    const response = await axiosInstance.post('/upload', formData, {
      headers: {'Content-Type': 'multipart/form-data'}
    });
    return response.data;
  } catch (error) {
    console.error('上傳失敗:', error);
    throw error;
  }
}使用時(shí),在頁(yè)面中調(diào)用封裝方法即可,這樣通過封裝后的 Axios 實(shí)例,我們可以在項(xiàng)目中更加方便地復(fù)用進(jìn)度監(jiān)控功能。
5. 特殊場(chǎng)景處理技巧
通常在一些特殊場(chǎng)景下,針對(duì)進(jìn)度監(jiān)控我們還需要一些處理技巧,這里博主分享兩個(gè)分別是 分塊上傳監(jiān)控 和 帶寬計(jì)算與預(yù)估
5. 1 分塊上傳監(jiān)控
async function chunkedUpload(file, chunkSize = 1024 * 1024) {
  const chunks = Math.ceil(file.size / chunkSize);
  let uploadedChunks = 0;
  for (let i = 0; i < chunks; i++) {
    const start = i * chunkSize;
    const end = Math.min(start + chunkSize, file.size);
    const chunk = file.slice(start, end);
    await axios.post('/upload-chunk', chunk, {
      headers: {'Content-Range': `bytes ${start}-${end-1}/${file.size}`},
      onUploadProgress: e => {
        const chunkPercent = (e.loaded / e.total) * 100;
        const totalPercent = ((uploadedChunks + e.loaded) / file.size) * 100;
        console.log(`分塊進(jìn)度: ${chunkPercent.toFixed(1)}%`);
        console.log(`總進(jìn)度: ${totalPercent.toFixed(1)}%`);
      }
    });
    uploadedChunks += chunk.size;
  }
}5. 2 帶寬計(jì)算與預(yù)估
let startTime;
let lastLoaded = 0;
xhr.upload.addEventListener('progress', e => {
  if (!startTime) startTime = Date.now();
  const currentTime = Date.now();
  const elapsed = (currentTime - startTime) / 1000; // 秒
  const speed = e.loaded / elapsed; // bytes/s
  const remaining = (e.total - e.loaded) / speed; // 剩余時(shí)間
  console.log(`傳輸速度: ${formatBytes(speed)}/s`);
  console.log(`預(yù)計(jì)剩余時(shí)間: ${remaining.toFixed(1)}秒`);
});
function formatBytes(bytes) {
  const units = ['B', 'KB', 'MB', 'GB'];
  let unitIndex = 0;
  while (bytes >= 1024 && unitIndex < units.length - 1) {
    bytes /= 1024;
    unitIndex++;
  }
  return `${bytes.toFixed(1)} ${units[unitIndex]}`;
}6. 結(jié)語(yǔ)
本篇文章介紹了如何在前端請(qǐng)求中監(jiān)控上傳和下載進(jìn)度,并提供了完整的前端和后端代碼示例。通過合理運(yùn)用這些技術(shù),開發(fā)者可以構(gòu)建出具有專業(yè)級(jí)進(jìn)度反饋的Web應(yīng)用,顯著提升用戶在處理大文件傳輸時(shí)的體驗(yàn)。
希望能幫助小伙伴們?cè)陧?xiàng)目中更好地處理大文件傳輸,提高用戶體驗(yàn)!
到此這篇關(guān)于AJAX請(qǐng)求上傳下載進(jìn)度監(jiān)控指南的文章就介紹到這了,更多相關(guān)AJAX上傳下載內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
 Ajax獲得站點(diǎn)文件內(nèi)容實(shí)例
Ajax獲得站點(diǎn)文件內(nèi)容實(shí)例:選擇一部著作,會(huì)通過 Ajax 實(shí)時(shí)獲得相關(guān)的名字,具體實(shí)現(xiàn)如下,感興趣的朋友可以參考下2013-09-09
 ajax實(shí)現(xiàn)提交時(shí)校驗(yàn)表單方法
這篇文章主要為大家詳細(xì)介紹了ajax實(shí)現(xiàn)提交時(shí)校驗(yàn)表單方法,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-02-02
 實(shí)現(xiàn)類似facebook無(wú)刷新ajax更新
這篇文章主要介紹了實(shí)現(xiàn)類似facebook無(wú)刷新ajax更新,需要的朋友可以參考下2014-03-03
 妙用Ajax技術(shù)實(shí)現(xiàn)局部刷新商品數(shù)量和總價(jià)實(shí)例代碼
這篇文章主要給大家介紹妙用Ajax技術(shù)實(shí)現(xiàn)局部刷新商品數(shù)量和總價(jià)實(shí)例代碼,非常不錯(cuò),需要的朋友一起看看吧2016-05-05
 ajax實(shí)現(xiàn)城市三級(jí)聯(lián)動(dòng)
這篇文章主要為大家詳細(xì)介紹了ajax實(shí)現(xiàn)城市三級(jí)聯(lián)動(dòng),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-10-10
 AJAX應(yīng)用中必須要掌握的重點(diǎn)知識(shí)(分享)
下面小編就為大家?guī)?lái)一篇AJAX應(yīng)用中必須要掌握的重點(diǎn)知識(shí)(分享)。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來(lái)看看吧2016-08-08
 用 ajax 的方法解決網(wǎng)頁(yè)廣告顯示的問題
用 ajax 的方法解決網(wǎng)頁(yè)廣告顯示的問題...2006-12-12
 解決ajax跨域請(qǐng)求數(shù)據(jù)cookie丟失問題
本文主要是從前端jquery和服務(wù)端php為例,分別使用實(shí)例解決ajax跨域請(qǐng)求數(shù)據(jù)cookie丟失問題,推薦給有相同需求的小伙伴們。2015-03-03
 Ajax獲取數(shù)據(jù)然后顯示在頁(yè)面的實(shí)現(xiàn)方法
下面小編就為大家?guī)?lái)一篇Ajax獲取數(shù)據(jù)然后顯示在頁(yè)面的實(shí)現(xiàn)方法。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來(lái)看看吧2016-08-08

