轩辕李的博客 轩辕李的博客
首页
  • Java
  • Spring
  • 其他语言
  • 工具
  • JavaScript
  • TypeScript
  • Node.js
  • Vue.js
  • 前端工程化
  • 浏览器与Web API
  • 分布式
  • 代码质量管理
  • 基础
  • 操作系统
  • 计算机网络
  • 编程范式
  • 安全
  • 中间件
  • 心得
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

轩辕李

勇猛精进,星辰大海
首页
  • Java
  • Spring
  • 其他语言
  • 工具
  • JavaScript
  • TypeScript
  • Node.js
  • Vue.js
  • 前端工程化
  • 浏览器与Web API
  • 分布式
  • 代码质量管理
  • 基础
  • 操作系统
  • 计算机网络
  • 编程范式
  • 安全
  • 中间件
  • 心得
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • JavaScript

  • TypeScript

  • Node.js

  • Vue.js

  • 工程化

  • 浏览器与Web API

    • HTML基础-语义化标签与文档结构
    • HTML基础-文本与排版标签
    • HTML基础-列表与表格
    • HTML表单-Input类型详解
    • HTML表单-表单元素与验证
    • HTML交互-多媒体元素
    • HTML工程化-模板与组件化
    • HTML工程化-性能优化
    • CSS基础-选择器与优先级
    • CSS基础-盒模型与布局基础
    • CSS基础-单位与颜色系统
    • CSS基础-文本与字体
    • CSS基础-背景、列表与表格样式
    • CSS布局-Flexbox完全指南
    • CSS布局-Grid网格布局
    • CSS布局-响应式设计实践
    • CSS进阶-动画与过渡
    • CSS进阶-渐变与阴影效果
    • CSS进阶-Transform与3D变换
    • CSS进阶-滤镜与混合模式
    • 现代CSS-CSS预处理器对比
    • 现代CSS-CSS-in-JS方案
    • 现代CSS-原子化CSS与Tailwind
    • CSS工程化-架构与规范
    • CSS工程化-性能优化
    • CSS工程化-PostCSS实战指南
    • Web API基础-DOM操作完全指南
    • Web API基础-事件处理与委托
    • Web API基础-BOM与浏览器环境
    • Web API存储-客户端存储方案
    • Web API网络-HTTP请求详解
      • 一、Fetch API基础
        • 1.1 基本用法
        • 1.2 Request对象
        • 1.3 Response对象
        • 1.4 解析响应体
      • 二、Fetch高级用法
        • 2.1 发送不同类型的数据
        • 2.2 处理不同状态码
        • 2.3 取消请求
        • 2.4 超时控制
        • 2.5 上传进度
      • 三、XMLHttpRequest详解
        • 3.1 基本用法
        • 3.2 readyState状态
        • 3.3 完整示例
        • 3.4 上传进度
        • 3.5 下载进度
      • 四、Fetch vs XMLHttpRequest
        • 4.1 对比表格
        • 4.2 选择建议
      • 五、综合示例
      • 六、最佳实践
        • 6.1 错误处理
        • 6.2 请求拦截
        • 6.3 并发控制
      • 七、总结
    • Web API网络-实时通信方案
    • Web API交互-用户体验增强
    • HTML&CSS历代版本新特性
  • 前端
  • 浏览器与Web API
轩辕李
2019-10-04
目录

Web API网络-HTTP请求详解

# Web API网络-HTTP请求详解

现代Web应用离不开HTTP请求。本文将深入讲解Fetch API和XMLHttpRequest两种HTTP请求方式,涵盖请求配置、响应处理、错误处理、取消请求等实用技巧。

# 一、Fetch API基础

Fetch API是现代浏览器提供的原生HTTP请求接口,基于Promise,语法简洁。

# 1.1 基本用法

// 最简单的GET请求
fetch('https://api.example.com/data')
  .then(response => response.json())
  .then(data => {
    console.log(data);
  })
  .catch(error => {
    console.error('请求失败:', error);
  });

// 使用async/await(推荐)
async function fetchData() {
  try {
    const response = await fetch('https://api.example.com/data');
    const data = await response.json();
    console.log(data);
  } catch (error) {
    console.error('请求失败:', error);
  }
}

# 1.2 Request对象

// 发送POST请求
fetch('https://api.example.com/users', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer token123'
  },
  body: JSON.stringify({
    name: '张三',
    email: 'zhangsan@example.com'
  })
})
  .then(response => response.json())
  .then(data => console.log(data));

// 完整配置示例
fetch('https://api.example.com/data', {
  method: 'GET',           // 请求方法
  headers: {},             // 请求头
  body: null,              // 请求体(GET/HEAD不能有body)
  mode: 'cors',            // 请求模式:same-origin, no-cors, cors
  credentials: 'include',  // 凭证:omit, same-origin, include
  cache: 'default',        // 缓存模式:default, no-cache, reload, force-cache, only-if-cached
  redirect: 'follow',      // 重定向:follow, error, manual
  referrer: 'client',      // 引用页
  referrerPolicy: 'no-referrer-when-downgrade', // 引用策略
  integrity: '',           // 子资源完整性值
  keepalive: false,        // 页面卸载后保持连接
  signal: abortController.signal  // 用于取消请求
});

# 1.3 Response对象

async function handleResponse() {
  const response = await fetch('https://api.example.com/data');
  
  // 响应状态
  console.log(response.status);     // 200, 404, 500 等
  console.log(response.statusText); // "OK", "Not Found" 等
  console.log(response.ok);         // status 200-299 返回true
  
  // 响应头
  console.log(response.headers.get('Content-Type'));
  console.log(response.headers.get('Date'));
  
  // 遍历响应头
  for (const [key, value] of response.headers) {
    console.log(`${key}: ${value}`);
  }
  
  // 响应类型
  console.log(response.type); // basic, cors, error, opaque
  
  // 响应URL
  console.log(response.url);
  
  // 是否重定向
  console.log(response.redirected);
}

# 1.4 解析响应体

// 解析JSON
const data = await response.json();

// 解析文本
const text = await response.text();

// 解析Blob(文件、图片等)
const blob = await response.blob();

// 解析ArrayBuffer(二进制数据)
const buffer = await response.arrayBuffer();

// 解析FormData
const formData = await response.formData();

// 克隆响应(只能读取一次,克隆后可多次读取)
const response1 = await fetch('/api/data');
const response2 = response1.clone();

const json = await response1.json();
const text = await response2.text();

# 二、Fetch高级用法

# 2.1 发送不同类型的数据

// 1. JSON数据
fetch('/api/user', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({ name: '张三', age: 25 })
});

// 2. 表单数据(application/x-www-form-urlencoded)
const formData = new URLSearchParams();
formData.append('name', '张三');
formData.append('age', '25');

fetch('/api/user', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/x-www-form-urlencoded'
  },
  body: formData
});

// 3. FormData(multipart/form-data,用于文件上传)
const formData2 = new FormData();
formData2.append('name', '张三');
formData2.append('file', fileInput.files[0]);

fetch('/api/upload', {
  method: 'POST',
  body: formData2  // 不需要设置Content-Type,浏览器自动设置
});

// 4. 文本数据
fetch('/api/note', {
  method: 'POST',
  headers: {
    'Content-Type': 'text/plain'
  },
  body: '这是一段文本内容'
});

// 5. Blob数据
const blob = new Blob(['文件内容'], { type: 'text/plain' });

fetch('/api/file', {
  method: 'POST',
  body: blob
});

# 2.2 处理不同状态码

async function handleRequest() {
  try {
    const response = await fetch('/api/data');
    
    // 检查响应状态
    if (!response.ok) {
      // HTTP错误状态
      if (response.status === 404) {
        throw new Error('资源不存在');
      } else if (response.status === 401) {
        throw new Error('未授权,请登录');
      } else if (response.status === 500) {
        throw new Error('服务器错误');
      } else {
        throw new Error(`HTTP错误: ${response.status}`);
      }
    }
    
    const data = await response.json();
    return data;
  } catch (error) {
    console.error('请求失败:', error.message);
    throw error;
  }
}

// 封装错误处理
async function fetchWithErrorHandling(url, options) {
  const response = await fetch(url, options);
  
  if (!response.ok) {
    const errorData = await response.json().catch(() => ({}));
    
    const error = new Error(errorData.message || `HTTP ${response.status}`);
    error.status = response.status;
    error.data = errorData;
    
    throw error;
  }
  
  return response;
}

# 2.3 取消请求

// 使用AbortController取消请求
const controller = new AbortController();
const signal = controller.signal;

fetch('/api/data', { signal })
  .then(response => response.json())
  .then(data => console.log(data))
  .catch(error => {
    if (error.name === 'AbortError') {
      console.log('请求已取消');
    } else {
      console.error('请求失败:', error);
    }
  });

// 5秒后取消请求
setTimeout(() => {
  controller.abort();
}, 5000);

// 实际应用:搜索防抖
let controller = null;

function search(keyword) {
  // 取消上一次请求
  if (controller) {
    controller.abort();
  }
  
  controller = new AbortController();
  
  fetch(`/api/search?q=${keyword}`, { signal: controller.signal })
    .then(response => response.json())
    .then(data => {
      displayResults(data);
    })
    .catch(error => {
      if (error.name !== 'AbortError') {
        console.error('搜索失败:', error);
      }
    });
}

# 2.4 超时控制

// Fetch没有内置超时,需要手动实现
function fetchWithTimeout(url, options = {}, timeout = 5000) {
  return Promise.race([
    fetch(url, options),
    new Promise((_, reject) =>
      setTimeout(() => reject(new Error('请求超时')), timeout)
    )
  ]);
}

// 使用示例
try {
  const response = await fetchWithTimeout('/api/data', {}, 3000);
  const data = await response.json();
} catch (error) {
  console.error(error.message); // "请求超时"
}

// 结合AbortController实现超时
function fetchWithAbortTimeout(url, options = {}, timeout = 5000) {
  const controller = new AbortController();
  const id = setTimeout(() => controller.abort(), timeout);
  
  return fetch(url, {
    ...options,
    signal: controller.signal
  }).finally(() => {
    clearTimeout(id);
  });
}

# 2.5 上传进度

// Fetch API本身不支持上传进度,需要使用XMLHttpRequest
// 或使用fetch结合ReadableStream

async function uploadWithProgress(file, onProgress) {
  const formData = new FormData();
  formData.append('file', file);
  
  // 注意:Fetch不支持原生上传进度,需要服务端配合
  const response = await fetch('/api/upload', {
    method: 'POST',
    body: formData
  });
  
  return response.json();
}

// 下载进度
async function downloadWithProgress(url, onProgress) {
  const response = await fetch(url);
  const contentLength = response.headers.get('Content-Length');
  const total = parseInt(contentLength, 10);
  
  let loaded = 0;
  
  const reader = response.body.getReader();
  const chunks = [];
  
  while (true) {
    const { done, value } = await reader.read();
    
    if (done) break;
    
    chunks.push(value);
    loaded += value.length;
    
    if (onProgress) {
      onProgress({ loaded, total, percentage: (loaded / total * 100).toFixed(2) });
    }
  }
  
  const blob = new Blob(chunks);
  return blob;
}

// 使用示例
downloadWithProgress('/api/file', (progress) => {
  console.log(`下载进度: ${progress.percentage}%`);
});

# 三、XMLHttpRequest详解

XMLHttpRequest是传统的HTTP请求API,功能强大但语法复杂。

# 3.1 基本用法

const xhr = new XMLHttpRequest();

// 初始化请求
xhr.open('GET', 'https://api.example.com/data', true);

// 设置请求头
xhr.setRequestHeader('Content-Type', 'application/json');

// 设置响应类型
xhr.responseType = 'json';

// 监听状态变化
xhr.onreadystatechange = function() {
  if (xhr.readyState === XMLHttpRequest.DONE) {
    if (xhr.status === 200) {
      console.log(xhr.response);
    } else {
      console.error('请求失败:', xhr.status);
    }
  }
};

// 发送请求
xhr.send();

# 3.2 readyState状态

xhr.onreadystatechange = function() {
  console.log('readyState:', xhr.readyState);
  
  // 0: UNSENT - 代理被创建,但尚未调用open()
  // 1: OPENED - open()已被调用
  // 2: HEADERS_RECEIVED - send()已被调用,响应头已被接收
  // 3: LOADING - 响应体接收中
  // 4: DONE - 请求完成
};

# 3.3 完整示例

function ajax(options) {
  return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest();
    
    // 默认配置
    const config = {
      method: 'GET',
      url: '',
      headers: {},
      data: null,
      timeout: 0,
      withCredentials: false,
      responseType: 'json',
      ...options
    };
    
    // 初始化
    xhr.open(config.method, config.url, true);
    
    // 设置响应类型
    xhr.responseType = config.responseType;
    
    // 设置超时
    if (config.timeout) {
      xhr.timeout = config.timeout;
    }
    
    // 跨域携带凭证
    xhr.withCredentials = config.withCredentials;
    
    // 设置请求头
    Object.keys(config.headers).forEach(key => {
      xhr.setRequestHeader(key, config.headers[key]);
    });
    
    // 成功回调
    xhr.onload = function() {
      if (xhr.status >= 200 && xhr.status < 300) {
        resolve({
          data: xhr.response,
          status: xhr.status,
          statusText: xhr.statusText,
          headers: xhr.getAllResponseHeaders()
        });
      } else {
        reject(new Error(`HTTP ${xhr.status}: ${xhr.statusText}`));
      }
    };
    
    // 错误回调
    xhr.onerror = function() {
      reject(new Error('网络错误'));
    };
    
    // 超时回调
    xhr.ontimeout = function() {
      reject(new Error('请求超时'));
    };
    
    // 取消回调
    xhr.onabort = function() {
      reject(new Error('请求已取消'));
    };
    
    // 发送请求
    xhr.send(config.data);
  });
}

// 使用示例
ajax({
  method: 'POST',
  url: '/api/user',
  headers: {
    'Content-Type': 'application/json'
  },
  data: JSON.stringify({ name: '张三' }),
  timeout: 5000
})
  .then(response => {
    console.log(response.data);
  })
  .catch(error => {
    console.error(error.message);
  });

# 3.4 上传进度

function uploadFile(file, onProgress) {
  return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest();
    const formData = new FormData();
    formData.append('file', file);
    
    // 监听上传进度
    xhr.upload.addEventListener('progress', (e) => {
      if (e.lengthComputable) {
        const percentage = (e.loaded / e.total * 100).toFixed(2);
        
        if (onProgress) {
          onProgress({
            loaded: e.loaded,
            total: e.total,
            percentage: percentage
          });
        }
      }
    });
    
    // 上传开始
    xhr.upload.addEventListener('loadstart', () => {
      console.log('上传开始');
    });
    
    // 上传完成
    xhr.upload.addEventListener('load', () => {
      console.log('上传完成');
    });
    
    // 上传错误
    xhr.upload.addEventListener('error', () => {
      reject(new Error('上传失败'));
    });
    
    // 上传中止
    xhr.upload.addEventListener('abort', () => {
      reject(new Error('上传已取消'));
    });
    
    xhr.onload = function() {
      if (xhr.status === 200) {
        resolve(JSON.parse(xhr.responseText));
      } else {
        reject(new Error(`上传失败: ${xhr.status}`));
      }
    };
    
    xhr.open('POST', '/api/upload');
    xhr.send(formData);
  });
}

// 使用示例
const fileInput = document.querySelector('input[type="file"]');

fileInput.addEventListener('change', async () => {
  const file = fileInput.files[0];
  
  try {
    const result = await uploadFile(file, (progress) => {
      console.log(`上传进度: ${progress.percentage}%`);
      updateProgressBar(progress.percentage);
    });
    
    console.log('上传成功:', result);
  } catch (error) {
    console.error(error.message);
  }
});

# 3.5 下载进度

function downloadFile(url, onProgress) {
  return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest();
    
    // 监听下载进度
    xhr.addEventListener('progress', (e) => {
      if (e.lengthComputable) {
        const percentage = (e.loaded / e.total * 100).toFixed(2);
        
        if (onProgress) {
          onProgress({
            loaded: e.loaded,
            total: e.total,
            percentage: percentage
          });
        }
      }
    });
    
    xhr.onload = function() {
      if (xhr.status === 200) {
        resolve(xhr.response);
      } else {
        reject(new Error(`下载失败: ${xhr.status}`));
      }
    };
    
    xhr.onerror = function() {
      reject(new Error('下载错误'));
    };
    
    xhr.open('GET', url);
    xhr.responseType = 'blob';
    xhr.send();
  });
}

// 使用示例
downloadFile('/api/file/large.pdf', (progress) => {
  console.log(`下载进度: ${progress.percentage}%`);
})
  .then(blob => {
    // 创建下载链接
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = 'file.pdf';
    a.click();
    URL.revokeObjectURL(url);
  });

# 四、Fetch vs XMLHttpRequest

# 4.1 对比表格

特性 Fetch XMLHttpRequest
语法 简洁(Promise) 复杂(回调)
上传进度 不支持 支持
下载进度 支持(复杂) 支持
超时 不支持(需手动实现) 支持
取消请求 AbortController abort()
请求拦截 不支持 支持
响应拦截 不支持 支持
兼容性 IE不支持 全部浏览器

# 4.2 选择建议

// 优先使用Fetch(现代浏览器)
// - 简单的GET/POST请求
// - 不需要进度监控
// - 基于Promise的现代代码

// 使用XMLHttpRequest
// - 需要上传/下载进度
// - 需要兼容老浏览器
// - 需要更精细的控制

# 五、综合示例

<html>
  <div id="http-demo-f1g2h">
    <h3>HTTP请求综合示例</h3>
    
    <div class="section-f1g2h">
      <h4>Fetch API示例</h4>
      <div class="controls-f1g2h">
        <select id="fetch-method-f1g2h">
          <option value="GET">GET</option>
          <option value="POST">POST</option>
          <option value="PUT">PUT</option>
          <option value="DELETE">DELETE</option>
        </select>
        <input type="text" id="fetch-url-f1g2h" placeholder="URL" value="https://jsonplaceholder.typicode.com/posts/1">
        <button id="fetch-send-f1g2h">发送请求</button>
        <button id="fetch-cancel-f1g2h">取消请求</button>
      </div>
      <div class="result-f1g2h" id="fetch-result-f1g2h"></div>
    </div>
    
    <div class="section-f1g2h">
      <h4>文件上传示例</h4>
      <div class="controls-f1g2h">
        <input type="file" id="file-input-f1g2h">
        <button id="upload-btn-f1g2h">上传文件</button>
      </div>
      <div class="progress-f1g2h">
        <div class="progress-bar-f1g2h" id="progress-bar-f1g2h"></div>
      </div>
      <div class="progress-text-f1g2h" id="progress-text-f1g2h">0%</div>
      <div class="result-f1g2h" id="upload-result-f1g2h"></div>
    </div>
    
    <div class="section-f1g2h">
      <h4>请求对比</h4>
      <table class="compare-table-f1g2h">
        <thead>
          <tr>
            <th>特性</th>
            <th>Fetch</th>
            <th>XMLHttpRequest</th>
          </tr>
        </thead>
        <tbody>
          <tr>
            <td>语法</td>
            <td>简洁(Promise)</td>
            <td>复杂(回调)</td>
          </tr>
          <tr>
            <td>上传进度</td>
            <td>不支持</td>
            <td>✓ 支持</td>
          </tr>
          <tr>
            <td>超时控制</td>
            <td>需手动实现</td>
            <td>✓ 原生支持</td>
          </tr>
          <tr>
            <td>取消请求</td>
            <td>AbortController</td>
            <td>abort()</td>
          </tr>
          <tr>
            <td>兼容性</td>
            <td>现代浏览器</td>
            <td>全部浏览器</td>
          </tr>
        </tbody>
      </table>
    </div>
  </div>
</html>

<style>
  #http-demo-f1g2h {
    font-family: sans-serif;
    max-width: 800px;
  }
  
  .section-f1g2h {
    margin-bottom: 24px;
    padding: 20px;
    background: #f9f9f9;
    border-radius: 8px;
  }
  
  .section-f1g2h h4 {
    margin: 0 0 16px 0;
    color: #333;
    border-bottom: 2px solid #2196F3;
    padding-bottom: 8px;
  }
  
  .controls-f1g2h {
    display: flex;
    gap: 8px;
    margin-bottom: 16px;
    flex-wrap: wrap;
  }
  
  .controls-f1g2h select,
  .controls-f1g2h input[type="text"] {
    padding: 8px 12px;
    border: 1px solid #ddd;
    border-radius: 4px;
    font-size: 14px;
  }
  
  .controls-f1g2h select {
    min-width: 100px;
  }
  
  .controls-f1g2h input[type="text"] {
    flex: 1;
    min-width: 200px;
  }
  
  .controls-f1g2h input[type="file"] {
    flex: 1;
    padding: 8px;
    border: 1px solid #ddd;
    border-radius: 4px;
  }
  
  .controls-f1g2h button {
    padding: 8px 16px;
    background: #2196F3;
    color: white;
    border: none;
    border-radius: 4px;
    cursor: pointer;
    font-size: 14px;
    transition: background 0.2s;
  }
  
  .controls-f1g2h button:hover {
    background: #1976D2;
  }
  
  .result-f1g2h {
    padding: 16px;
    background: white;
    border-radius: 4px;
    border: 1px solid #e0e0e0;
    min-height: 60px;
    max-height: 300px;
    overflow-y: auto;
    font-family: monospace;
    font-size: 12px;
    white-space: pre-wrap;
    word-break: break-all;
  }
  
  .progress-f1g2h {
    width: 100%;
    height: 30px;
    background: #e0e0e0;
    border-radius: 15px;
    overflow: hidden;
    margin-bottom: 8px;
  }
  
  .progress-bar-f1g2h {
    height: 100%;
    background: linear-gradient(90deg, #2196F3, #21CBF3);
    transition: width 0.3s;
    display: flex;
    align-items: center;
    justify-content: center;
    color: white;
    font-size: 12px;
    font-weight: bold;
    width: 0%;
  }
  
  .progress-text-f1g2h {
    text-align: center;
    font-size: 14px;
    font-weight: bold;
    color: #2196F3;
    margin-bottom: 12px;
  }
  
  .compare-table-f1g2h {
    width: 100%;
    border-collapse: collapse;
    background: white;
  }
  
  .compare-table-f1g2h th,
  .compare-table-f1g2h td {
    padding: 12px;
    text-align: left;
    border: 1px solid #e0e0e0;
    font-size: 13px;
  }
  
  .compare-table-f1g2h th {
    background: #f5f5f5;
    font-weight: bold;
    color: #333;
  }
  
  .compare-table-f1g2h tbody tr:hover {
    background: #f9f9f9;
  }
</style>

<script>
  let fetchController = null;
  
  document.getElementById('fetch-send-f1g2h').addEventListener('click', async () => {
    const method = document.getElementById('fetch-method-f1g2h').value;
    const url = document.getElementById('fetch-url-f1g2h').value;
    const result = document.getElementById('fetch-result-f1g2h');
    
    if (!url) {
      result.textContent = '请输入URL';
      return;
    }
    
    result.textContent = '请求中...';
    
    fetchController = new AbortController();
    
    try {
      const options = {
        method: method,
        signal: fetchController.signal
      };
      
      if (method === 'POST' || method === 'PUT') {
        options.headers = {
          'Content-Type': 'application/json'
        };
        options.body = JSON.stringify({
          title: '测试标题',
          body: '测试内容',
          userId: 1
        });
      }
      
      const response = await fetch(url, options);
      
      if (!response.ok) {
        throw new Error(`HTTP ${response.status}: ${response.statusText}`);
      }
      
      const data = await response.json();
      
      result.textContent = JSON.stringify(data, null, 2);
    } catch (error) {
      if (error.name === 'AbortError') {
        result.textContent = '✗ 请求已取消';
      } else {
        result.textContent = `✗ 请求失败: ${error.message}`;
      }
    }
  });
  
  document.getElementById('fetch-cancel-f1g2h').addEventListener('click', () => {
    if (fetchController) {
      fetchController.abort();
      fetchController = null;
    }
  });

  document.getElementById('upload-btn-f1g2h').addEventListener('click', () => {
    const fileInput = document.getElementById('file-input-f1g2h');
    const file = fileInput.files[0];
    
    if (!file) {
      document.getElementById('upload-result-f1g2h').textContent = '请选择文件';
      return;
    }
    
    uploadFileWithProgress(file);
  });
  
  function uploadFileWithProgress(file) {
    const progressBar = document.getElementById('progress-bar-f1g2h');
    const progressText = document.getElementById('progress-text-f1g2h');
    const result = document.getElementById('upload-result-f1g2h');
    
    progressBar.style.width = '0%';
    progressText.textContent = '0%';
    result.textContent = '';
    
    const xhr = new XMLHttpRequest();
    const formData = new FormData();
    formData.append('file', file);
    
    xhr.upload.addEventListener('progress', (e) => {
      if (e.lengthComputable) {
        const percentage = Math.round((e.loaded / e.total) * 100);
        progressBar.style.width = percentage + '%';
        progressText.textContent = percentage + '%';
      }
    });
    
    xhr.upload.addEventListener('loadstart', () => {
      result.textContent = '上传开始...';
    });
    
    xhr.upload.addEventListener('load', () => {
      result.textContent = '上传完成,等待服务器响应...';
    });
    
    xhr.upload.addEventListener('error', () => {
      result.textContent = '✗ 上传失败';
      progressBar.style.width = '0%';
    });
    
    xhr.onload = function() {
      if (xhr.status === 200) {
        result.textContent = '✓ 上传成功!\n\n由于这是演示环境,实际上传功能未启用。\n在真实环境中,文件会被上传到服务器。';
      } else {
        result.textContent = `✗ 上传失败: HTTP ${xhr.status}`;
      }
    };
    
    xhr.onerror = function() {
      result.textContent = '✗ 网络错误';
      progressBar.style.width = '0%';
    };
    
    setTimeout(() => {
      let progress = 0;
      const interval = setInterval(() => {
        progress += 10;
        progressBar.style.width = progress + '%';
        progressText.textContent = progress + '%';
        
        if (progress >= 100) {
          clearInterval(interval);
          result.textContent = '✓ 模拟上传成功!\n\n文件名: ' + file.name + '\n大小: ' + (file.size / 1024).toFixed(2) + ' KB';
        }
      }, 300);
    }, 100);
  }
</script>

# 六、最佳实践

# 6.1 错误处理

async function fetchWithRetry(url, options = {}, retries = 3) {
  for (let i = 0; i < retries; i++) {
    try {
      const response = await fetch(url, options);
      
      if (!response.ok) {
        throw new Error(`HTTP ${response.status}`);
      }
      
      return response;
    } catch (error) {
      if (i === retries - 1) {
        throw error;
      }
      
      console.log(`重试第 ${i + 1} 次...`);
      await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1)));
    }
  }
}

# 6.2 请求拦截

class HttpClient {
  constructor(baseURL = '') {
    this.baseURL = baseURL;
    this.interceptors = {
      request: [],
      response: []
    };
  }
  
  use(type, handler) {
    this.interceptors[type].push(handler);
  }
  
  async fetch(url, options = {}) {
    url = this.baseURL + url;
    
    for (const interceptor of this.interceptors.request) {
      const result = await interceptor({ url, options });
      if (result) {
        url = result.url;
        options = result.options;
      }
    }
    
    let response = await fetch(url, options);
    
    for (const interceptor of this.interceptors.response) {
      response = await interceptor(response);
    }
    
    return response;
  }
}

const client = new HttpClient('https://api.example.com');

client.use('request', async ({ url, options }) => {
  options.headers = {
    ...options.headers,
    'Authorization': `Bearer ${getToken()}`
  };
  
  return { url, options };
});

client.use('response', async (response) => {
  if (response.status === 401) {
    await refreshToken();
    throw new Error('需要重新登录');
  }
  
  return response;
});

# 6.3 并发控制

async function fetchWithConcurrency(urls, limit = 3) {
  const results = [];
  const executing = [];
  
  for (const url of urls) {
    const promise = fetch(url).then(r => r.json());
    results.push(promise);
    
    if (limit <= urls.length) {
      const e = promise.then(() => executing.splice(executing.indexOf(e), 1));
      executing.push(e);
      
      if (executing.length >= limit) {
        await Promise.race(executing);
      }
    }
  }
  
  return Promise.all(results);
}

# 七、总结

HTTP请求是Web应用的基础:

  1. Fetch API:现代、简洁、基于Promise
  2. XMLHttpRequest:功能完整、支持进度监控
  3. 选择建议:优先Fetch,需要进度时用XHR
  4. 最佳实践:错误处理、请求拦截、并发控制

掌握HTTP请求API,能够构建高效、可靠的数据交互应用。

祝你变得更强!

编辑 (opens new window)
#Web API#Fetch#XMLHttpRequest#AJAX#HTTP
上次更新: 2025/11/28
Web API存储-客户端存储方案
Web API网络-实时通信方案

← Web API存储-客户端存储方案 Web API网络-实时通信方案→

最近更新
01
AI编程时代的一些心得
09-11
02
Claude Code与Codex的协同工作
09-01
03
Claude Code 最佳实践(个人版)
08-01
更多文章>
Theme by Vdoing | Copyright © 2018-2025 京ICP备2021021832号-2 | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式