每日一个知识点:实现AJAX和Fetch请求进度条

首先我们来分析AJAX与Fetch,对于AJAX,XMLHttpRequest有progress事件,可以监听上传和下载的进度。而Fetch API本身不直接支持进度事件,但可以通过读取Response.body的ReadableStream来手动计算进度。

其次我们需要知道实现请求进度条的核心是​​监听请求过程中的数据传输进度​​,并通过 DOM 更新可视化进度。

下面我来设计一个页面,展示如何使用AJAX和Fetch API实现进度条功能,并比较两者的实现方式。

设计思路

  • 创建两个独立的部分分别展示AJAX和Fetch进度条

  • 使用模拟的大型文件下载展示进度效果

  • 添加样式美化进度条和界面

  • 提供代码实现示例

下面是完整的实现代码:

xml 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>AJAX与Fetch进度条实现</title>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
        }
        
        body {
            background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
            color: #333;
            line-height: 1.6;
            padding: 20px;
            min-height: 100vh;
        }
        
        .container {
            max-width: 1000px;
            margin: 0 auto;
        }
        
        header {
            text-align: center;
            margin-bottom: 40px;
            padding: 20px;
        }
        
        h1 {
            font-size: 2.5rem;
            color: #2c3e50;
            margin-bottom: 10px;
        }
        
        .subtitle {
            font-size: 1.2rem;
            color: #7f8c8d;
            margin-bottom: 20px;
        }
        
        .comparison {
            display: flex;
            flex-wrap: wrap;
            gap: 30px;
            margin-bottom: 40px;
        }
        
        .card {
            flex: 1;
            min-width: 300px;
            background: white;
            border-radius: 10px;
            box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
            padding: 25px;
            transition: transform 0.3s ease;
        }
        
        .card:hover {
            transform: translateY(-5px);
        }
        
        .card h2 {
            color: #2c3e50;
            margin-bottom: 20px;
            padding-bottom: 10px;
            border-bottom: 2px solid #3498db;
            display: flex;
            align-items: center;
        }
        
        .card h2 i {
            margin-right: 10px;
            font-size: 1.5rem;
        }
        
        .ajax-color {
            color: #e74c3c;
        }
        
        .fetch-color {
            color: #2ecc71;
        }
        
        .progress-container {
            margin: 20px 0;
            background: #ecf0f1;
            border-radius: 5px;
            height: 20px;
            overflow: hidden;
        }
        
        .progress-bar {
            height: 100%;
            width: 0;
            border-radius: 5px;
            transition: width 0.3s ease;
            display: flex;
            align-items: center;
            justify-content: center;
            color: white;
            font-size: 12px;
            font-weight: bold;
        }
        
        .ajax-progress {
            background: linear-gradient(90deg, #e74c3c, #e67e22);
        }
        
        .fetch-progress {
            background: linear-gradient(90deg, #2ecc71, #3498db);
        }
        
        .btn {
            display: inline-block;
            padding: 12px 24px;
            background: #3498db;
            color: white;
            border: none;
            border-radius: 5px;
            cursor: pointer;
            font-size: 16px;
            font-weight: bold;
            transition: all 0.3s ease;
            margin: 10px 5px;
        }
        
        .btn-ajax {
            background: #e74c3c;
        }
        
        .btn-fetch {
            background: #2ecc71;
        }
        
        .btn:hover {
            opacity: 0.9;
            transform: translateY(-2px);
        }
        
        .btn:active {
            transform: translateY(0);
        }
        
        .status {
            margin-top: 15px;
            padding: 10px;
            border-radius: 5px;
            background: #f8f9fa;
            font-family: monospace;
            min-height: 40px;
        }
        
        .code-container {
            margin-top: 25px;
            background: #2c3e50;
            color: #f8f8f2;
            padding: 15px;
            border-radius: 5px;
            overflow-x: auto;
            font-family: 'Consolas', monospace;
        }
        
        .code-header {
            display: flex;
            justify-content: space-between;
            align-items: center;
            margin-bottom: 10px;
            color: #ddd;
        }
        
        .copy-btn {
            background: #7f8c8d;
            color: white;
            border: none;
            padding: 5px 10px;
            border-radius: 3px;
            cursor: pointer;
            font-size: 12px;
        }
        
        .keyword {
            color: #f92672;
        }
        
        .function {
            color: #66d9ef;
        }
        
        .comment {
            color: #75715e;
        }
        
        .string {
            color: #a6e22e;
        }
        
        .highlight {
            background: linear-gradient(120deg, #a1c4fd 0%, #c2e9fb 100%);
            padding: 25px;
            border-radius: 10px;
            margin-top: 20px;
        }
        
        .conclusion {
            background: white;
            border-radius: 10px;
            box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
            padding: 25px;
            text-align: center;
            margin-top: 40px;
        }
        
        .conclusion h2 {
            color: #2c3e50;
            margin-bottom: 20px;
        }
        
        footer {
            text-align: center;
            margin-top: 40px;
            color: #7f8c8d;
        }
        
        @media (max-width: 768px) {
            .comparison {
                flex-direction: column;
            }
        }
    </style>
</head>
<body>
    <div class="container">
        <header>
            <h1>AJAX与Fetch进度条实现</h1>
            <p class="subtitle">比较两种技术实现进度监控的方法</p>
        </header>
        
        <div class="comparison">
            <div class="card">
                <h2><span class="ajax-color">◉</span> AJAX 进度条</h2>
                <p>使用XMLHttpRequest对象可以实现上传和下载进度监控。</p>
                
                <div class="progress-container">
                    <div id="ajaxProgress" class="progress-bar ajax-progress">0%</div>
                </div>
                
                <button id="ajaxBtn" class="btn btn-ajax">开始AJAX请求</button>
                <button id="ajaxCancel" class="btn">取消请求</button>
                
                <div id="ajaxStatus" class="status">等待请求...</div>
                
                <div class="code-container">
                    <div class="code-header">
                        <span>JavaScript 代码示例</span>
                        <button class="copy-btn">复制代码</button>
                    </div>
                    <pre><code><span class="keyword">const</span> xhr = <span class="keyword">new</span> XMLHttpRequest();
xhr.open(<span class="string">'GET'</span>, <span class="string">'https://example.com/large-file'</span>);

<span class="comment">// 监听进度事件</span>
xhr.addEventListener(<span class="string">'progress'</span>, (event) => {
  <span class="keyword">if</span> (event.lengthComputable) {
    <span class="keyword">const</span> percent = (event.loaded / event.total) * 100;
    progressBar.style.width = percent + <span class="string">'%'</span>;
    progressBar.textContent = Math.round(percent) + <span class="string">'%'</span>;
  }
});

xhr.send();</code></pre>
                </div>
            </div>
            
            <div class="card">
                <h2><span class="fetch-color">◉</span> Fetch 进度条</h2>
                <p>使用Fetch API配合ReadableStream可以实现下载进度监控。</p>
                
                <div class="progress-container">
                    <div id="fetchProgress" class="progress-bar fetch-progress">0%</div>
                </div>
                
                <button id="fetchBtn" class="btn btn-fetch">开始Fetch请求</button>
                <button id="fetchCancel" class="btn">取消请求</button>
                
                <div id="fetchStatus" class="status">等待请求...</div>
                
                <div class="code-container">
                    <div class="code-header">
                        <span>JavaScript 代码示例</span>
                        <button class="copy-btn">复制代码</button>
                    </div>
                    <pre><code><span class="keyword">const</span> response = <span class="keyword">await</span> fetch(<span class="string">'https://example.com/large-file'</span>);
<span class="keyword">const</span> reader = response.body.getReader();
<span class="keyword">const</span> contentLength = response.headers.get(<span class="string">'Content-Length'</span>);

<span class="keyword">let</span> receivedLength = 0;
<span class="keyword">while</span>(<span class="keyword">true</span>) {
  <span class="keyword">const</span> {done, value} = <span class="keyword">await</span> reader.read();
  
  <span class="keyword">if</span> (done) <span class="keyword">break</span>;
  
  receivedLength += value.length;
  <span class="keyword">const</span> percent = (receivedLength / contentLength) * 100;
  progressBar.style.width = percent + <span class="string">'%'</span>;
  progressBar.textContent = Math.round(percent) + <span class="string">'%'</span>;
}</code></pre>
                </div>
            </div>
        </div>
        
        <div class="highlight">
            <h3>关键技术点</h3>
            <ul>
                <li><strong>AJAX</strong>:使用XMLHttpRequest的progress事件可以监控上传和下载进度</li>
                <li><strong>Fetch</strong>:使用Response.body的ReadableStream可以监控下载进度,但上传进度监控较为复杂</li>
                <li>两者都需要服务器提供Content-Length头部信息才能计算准确的百分比</li>
                <li>Fetch API是现代JavaScript的标准,但AJAX在某些场景下仍然有用</li>
            </ul>
        </div>
        
        <div class="conclusion">
            <h2>总结</h2>
            <p>AJAX和Fetch都可以实现进度条功能,但实现方式有所不同。</p>
            <p>AJAX提供更简单的进度监控API,而Fetch需要手动处理数据流。</p>
            <p>根据项目需求和个人偏好选择合适的技术方案。</p>
        </div>
        
        <footer>
            <p>© 2025 进度条实现示例 | 设计用于学习</p>
        </footer>
    </div>

    <script>
        // AJAX实现
        const ajaxProgress = document.getElementById('ajaxProgress');
        const ajaxBtn = document.getElementById('ajaxBtn');
        const ajaxCancel = document.getElementById('ajaxCancel');
        const ajaxStatus = document.getElementById('ajaxStatus');
        
        let ajaxXHR = null;
        
        ajaxBtn.addEventListener('click', () => {
            // 重置进度条
            ajaxProgress.style.width = '0%';
            ajaxProgress.textContent = '0%';
            ajaxStatus.textContent = '开始请求...';
            
            // 创建模拟的AJAX请求
            ajaxXHR = new XMLHttpRequest();
            ajaxXHR.open('GET', 'https://httpbin.org/delay/3', true);
            
            // 监听进度事件
            ajaxXHR.addEventListener('progress', (event) => {
                if (event.lengthComputable) {
                    const percent = (event.loaded / event.total) * 100;
                    ajaxProgress.style.width = percent + '%';
                    ajaxProgress.textContent = Math.round(percent) + '%';
                    ajaxStatus.textContent = `已加载: ${event.loaded} / ${event.total} 字节`;
                } else {
                    // 如果无法计算总大小,使用模拟进度
                    const currentWidth = parseInt(ajaxProgress.style.width || '0');
                    const newWidth = Math.min(currentWidth + 10, 100);
                    ajaxProgress.style.width = newWidth + '%';
                    ajaxProgress.textContent = newWidth + '%';
                    ajaxStatus.textContent = `已加载: ${event.loaded} 字节 (总大小未知)`;
                }
            });
            
            ajaxXHR.addEventListener('load', () => {
                ajaxStatus.textContent = '请求完成!';
            });
            
            ajaxXHR.addEventListener('error', () => {
                ajaxStatus.textContent = '请求出错!';
            });
            
            ajaxXHR.send();
        });
        
        ajaxCancel.addEventListener('click', () => {
            if (ajaxXHR) {
                ajaxXHR.abort();
                ajaxStatus.textContent = '请求已取消';
                ajaxProgress.style.width = '0%';
                ajaxProgress.textContent = '0%';
            }
        });
        
        // Fetch实现
        const fetchProgress = document.getElementById('fetchProgress');
        const fetchBtn = document.getElementById('fetchBtn');
        const fetchCancel = document.getElementById('fetchCancel');
        const fetchStatus = document.getElementById('fetchStatus');
        
        let fetchController = null;
        
        fetchBtn.addEventListener('click', async () => {
            // 重置进度条
            fetchProgress.style.width = '0%';
            fetchProgress.textContent = '0%';
            fetchStatus.textContent = '开始请求...';
            
            // 创建AbortController以便可以取消请求
            fetchController = new AbortController();
            const signal = fetchController.signal;
            
            try {
                const response = await fetch('https://httpbin.org/delay/3', { signal });
                
                if (!response.ok) {
                    throw new Error('网络响应不正常');
                }
                
                const contentLength = response.headers.get('content-length');
                let receivedLength = 0;
                
                if (contentLength) {
                    // 读取数据流
                    const reader = response.body.getReader();
                    const chunks = [];
                    
                    while (true) {
                        const { done, value } = await reader.read();
                        
                        if (done) break;
                        
                        chunks.push(value);
                        receivedLength += value.length;
                        
                        const percent = (receivedLength / contentLength) * 100;
                        fetchProgress.style.width = percent + '%';
                        fetchProgress.textContent = Math.round(percent) + '%';
                        fetchStatus.textContent = `已加载: ${receivedLength} / ${contentLength} 字节`;
                    }
                    
                    // 合并所有chunks
                    const chunksAll = new Uint8Array(receivedLength);
                    let position = 0;
                    for (let chunk of chunks) {
                        chunksAll.set(chunk, position);
                        position += chunk.length;
                    }
                    
                    // 解码成字符串
                    const result = new TextDecoder("utf-8").decode(chunksAll);
                    fetchStatus.textContent = '请求完成!';
                } else {
                    // 如果没有content-length头,使用模拟进度
                    for (let i = 0; i <= 100; i += 5) {
                        if (signal.aborted) break;
                        await new Promise(resolve => setTimeout(resolve, 150));
                        fetchProgress.style.width = i + '%';
                        fetchProgress.textContent = i + '%';
                        fetchStatus.textContent = `已加载: ${i}% (总大小未知)`;
                    }
                    
                    if (!signal.aborted) {
                        fetchStatus.textContent = '请求完成!';
                    }
                }
            } catch (error) {
                if (error.name === 'AbortError') {
                    fetchStatus.textContent = '请求已取消';
                } else {
                    fetchStatus.textContent = '请求出错: ' + error.message;
                }
            }
        });
        
        fetchCancel.addEventListener('click', () => {
            if (fetchController) {
                fetchController.abort();
                fetchProgress.style.width = '0%';
                fetchProgress.textContent = '0%';
            }
        });
        
        // 复制代码功能
        document.querySelectorAll('.copy-btn').forEach(button => {
            button.addEventListener('click', () => {
                const codeBlock = button.parentElement.nextElementSibling;
                const textToCopy = codeBlock.textContent;
                
                navigator.clipboard.writeText(textToCopy).then(() => {
                    const originalText = button.textContent;
                    button.textContent = '已复制!';
                    
                    setTimeout(() => {
                        button.textContent = originalText;
                    }, 2000);
                }).catch(err => {
                    console.error('无法复制文本: ', err);
                });
            });
        });
    </script>
</body>
</html>

功能说明

这个页面实现了以下功能:

  1. AJAX进度条

    • 使用XMLHttpRequest对象发送请求

    • 通过progress事件监听下载进度

    • 提供取消请求功能

  2. Fetch进度条

    • 使用Fetch API发送请求

    • 通过ReadableStream读取数据流实现进度监控

    • 使用AbortController实现请求取消功能

  3. 用户体验

    • 响应式设计,适配各种屏幕尺寸

    • 直观的进度条显示

    • 状态提示信息

    • 代码示例和复制功能

  4. 模拟请求

    • 使用httpbin.org的延迟接口模拟大型文件下载

    • 处理未知文件大小的情况

诸位看官可以直接将上述代码复制到HTML文件中,并在浏览器中打开查看效果。

那么我们来总结一下

  • AJAX(XHR)​ ​:简单直接,通过 onprogress事件原生支持进度监听,适合传统场景。

  • ​Fetch API​​:需手动通过流式读取实现进度,更灵活但代码复杂度高,适合现代前端项目(如配合 React/Vue)。

当然,合适自己的才是最好的,谨言慎行。

相关推荐
复苏季风13 分钟前
聊聊 ?? 运算符:一个懂得 "分寸" 的默认值高手
前端·javascript
探码科技15 分钟前
AI驱动的知识库:客户支持与文档工作的新时代
前端
朱程42 分钟前
写给自己的 LangChain 开发教程(一):Hello world & 历史记录
前端·人工智能
luckyCover44 分钟前
js基础:手写call、apply、bind函数
前端·javascript
Dragon Wu2 小时前
前端 下载后端返回的二进制excel数据
前端·javascript·html5
北海几经夏2 小时前
React响应式链路
前端·react.js
晴空雨2 小时前
React Media 深度解析:从使用到 window.matchMedia API 详解
前端·react.js
一个有故事的男同学2 小时前
React性能优化全景图:从问题发现到解决方案
前端
探码科技2 小时前
2025年20+超实用技术文档工具清单推荐
前端
Juchecar2 小时前
Vue 3 推荐选择组合式 API 风格(附录与选项式的代码对比)
前端·vue.js