15.2 fetch API 与 AbortController

fetch API 深度解析

基本用法与现代实践

// 基础GET请求
async function fetchData(url) {
  try {
    const response = await fetch(url);
    
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }
    
    const data = await response.json();
    return data;
  } catch (error) {
    console.error('Fetch error:', error);
    throw error; // 重新抛出以便外部处理
  }
}

// 高级POST请求
async function postData(url, data) {
  const response = await fetch(url, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${getToken()}`
    },
    body: JSON.stringify(data),
    credentials: 'include' // 包含cookies
  });

  if (!response.ok) {
    const errorData = await response.json();
    throw new ApiError(response.status, errorData.message);
  }

  return response.json();
}

请求配置选项详解

const fetchOptions = {
  method: 'PUT', // GET, POST, PUT, DELETE, etc.
  headers: {
    'Content-Type': 'application/json',
    'X-Custom-Header': 'value'
  },
  body: JSON.stringify({ key: 'value' }), // Blob, FormData, URLSearchParams等
  mode: 'cors', // no-cors, *cors, same-origin
  cache: 'no-cache', // *default, no-cache, reload, force-cache, only-if-cached
  credentials: 'same-origin', // include, *same-origin, omit
  redirect: 'follow', // manual, *follow, error
  referrerPolicy: 'no-referrer', // no-referrer, *client
  signal: null // 用于取消请求的AbortSignal
};

响应处理高级技巧

流式处理大数据

async function processLargeData(url) {
  const response = await fetch(url);
  const reader = response.body.getReader();
  const decoder = new TextDecoder();
  let result = '';

  while (true) {
    const { done, value } = await reader.read();
    if (done) break;
    
    result += decoder.decode(value, { stream: true });
    console.log('Received chunk:', value.byteLength, 'bytes');
  }

  console.log('Complete data:', result);
  return result;
}

进度监控

async function fetchWithProgress(url, onProgress) {
  const response = await fetch(url);
  const contentLength = +response.headers.get('Content-Length');
  let receivedLength = 0;
  const chunks = [];
  const reader = response.body.getReader();

  while (true) {
    const { done, value } = await reader.read();
    if (done) break;

    chunks.push(value);
    receivedLength += value.length;
    
    if (contentLength) {
      const percent = Math.round((receivedLength / contentLength) * 100);
      onProgress(percent, receivedLength, contentLength);
    }
  }

  const chunksAll = new Uint8Array(receivedLength);
  let position = 0;
  for (const chunk of chunks) {
    chunksAll.set(chunk, position);
    position += chunk.length;
  }

  return new TextDecoder().decode(chunksAll);
}

// 使用示例
fetchWithProgress('large-file.txt', (percent) => {
  console.log(`Downloaded: ${percent}%`);
});

AbortController 实战应用

基本取消请求

// 创建控制器
const controller = new AbortController();
const { signal } = controller;

// 发起可取消的请求
fetch('some-api', { signal })
  .then(response => response.json())
  .catch(err => {
    if (err.name === 'AbortError') {
      console.log('Request was aborted');
    } else {
      console.error('Fetch error:', err);
    }
  });

// 取消请求
controller.abort();

超时自动取消

function fetchWithTimeout(url, options = {}, timeout = 5000) {
  const controller = new AbortController();
  const { signal } = controller;
  
  options.signal = signal;
  
  const timeoutId = setTimeout(() => {
    controller.abort();
  }, timeout);

  return fetch(url, options)
    .then(response => {
      clearTimeout(timeoutId);
      return response;
    })
    .catch(err => {
      clearTimeout(timeoutId);
      throw err;
    });
}

// 使用示例
fetchWithTimeout('https://api.example.com/data', {}, 3000)
  .then(response => response.json())
  .then(data => console.log(data))
  .catch(err => {
    if (err.name === 'AbortError') {
      console.error('Request timed out');
    } else {
      console.error('Error:', err);
    }
  });

多请求竞争与取消

async function fetchFastest(urls, timeout = 3000) {
  const controller = new AbortController();
  const { signal } = controller;
  
  const timeoutId = setTimeout(() => {
    controller.abort();
  }, timeout);

  try {
    const promises = urls.map(url => 
      fetch(url, { signal })
        .then(response => {
          clearTimeout(timeoutId);
          return response.json();
        })
    );
    
    return await Promise.any(promises);
  } catch (err) {
    if (err.name === 'AggregateError') {
      throw new Error('All requests failed');
    } else if (err.name === 'AbortError') {
      throw new Error('Request timeout');
    }
    throw err;
  } finally {
    clearTimeout(timeoutId);
  }
}

// 使用示例
fetchFastest([
  'https://api1.example.com/data',
  'https://api2.example.com/data',
  'https://api3.example.com/data'
])
.then(data => console.log('Fastest response:', data))
.catch(err => console.error('Error:', err.message));

高级应用场景

可取消的并行请求

async function fetchMultiple(urls, { timeout = 5000 } = {}) {
  const controller = new AbortController();
  const { signal } = controller;
  
  const timeoutId = setTimeout(() => {
    controller.abort();
  }, timeout);

  try {
    const requests = urls.map(url => 
      fetch(url, { signal }).then(res => res.json())
    );
    
    const results = await Promise.all(requests);
    clearTimeout(timeoutId);
    return results;
  } catch (err) {
    clearTimeout(timeoutId);
    if (err.name === 'AbortError') {
      throw new Error('Request timeout exceeded');
    }
    throw err;
  }
}

// 使用示例
fetchMultiple([
  'https://api.example.com/users',
  'https://api.example.com/posts'
])
.then(([users, posts]) => {
  console.log('Users:', users);
  console.log('Posts:', posts);
})
.catch(err => console.error('Error:', err.message));

重试机制实现

async function fetchWithRetry(url, options = {}, maxRetries = 3, retryDelay = 1000) {
  let lastError;
  
  for (let i = 0; i < maxRetries; i++) {
    const controller = new AbortController();
    options.signal = controller.signal;
    
    try {
      const response = await fetch(url, options);
      
      if (!response.ok) {
        const error = new Error(`HTTP error! status: ${response.status}`);
        error.status = response.status;
        
        // 如果是服务器错误(5xx)才重试
        if (response.status >= 500 && i < maxRetries - 1) {
          await new Promise(resolve => setTimeout(resolve, retryDelay));
          continue;
        }
        
        throw error;
      }
      
      return response.json();
    } catch (error) {
      lastError = error;
      
      // 如果不是中止错误且不是最后一次尝试,则等待后重试
      if (error.name !== 'AbortError' && i < maxRetries - 1) {
        await new Promise(resolve => setTimeout(resolve, retryDelay));
      }
    }
  }
  
  throw lastError;
}

// 使用示例
fetchWithRetry('https://api.example.com/unstable', {}, 3, 1000)
  .then(data => console.log(data))
  .catch(err => console.error('Failed after retries:', err));

错误处理与调试

自定义错误类

class FetchError extends Error {
  constructor(message, { status, statusText, url } = {}) {
    super(message);
    this.name = 'FetchError';
    this.status = status;
    this.statusText = statusText;
    this.url = url;
  }
  
  toString() {
    return `${this.name}: ${this.message} (${this.status} ${this.statusText} at ${this.url})`;
  }
}

async function fetchWithEnhancedError(url) {
  const response = await fetch(url);
  
  if (!response.ok) {
    let errorData;
    try {
      errorData = await response.json();
    } catch (e) {
      errorData = { message: response.statusText };
    }
    
    throw new FetchError(errorData.message || 'Request failed', {
      status: response.status,
      statusText: response.statusText,
      url: response.url
    });
  }
  
  return response.json();
}

网络状态检测

function checkNetworkStatus() {
  if (!navigator.onLine) {
    throw new Error('Offline: No network connection');
  }
  
  // 更精确的检测方式
  return fetch('https://httpbin.org/get', {
    method: 'HEAD',
    cache: 'no-store',
    mode: 'no-cors'
  }).catch(() => {
    throw new Error('Network may be unstable');
  });
}

async function fetchWithNetworkCheck(url) {
  await checkNetworkStatus();
  return fetchWithRetry(url);
}

最佳实践总结

  1. 请求取消

    • 总是为长时间请求添加取消能力
    • 页面切换时取消未完成请求
    • 用户导航离开时清理资源
  2. 错误处理

    • 区分网络错误、服务器错误和应用错误
    • 为错误提供足够上下文
    • 实现适当的重试机制
  3. 性能优化

    • 使用流式处理大响应
    • 实现进度反馈
    • 合理设置超时
  4. 安全实践

    • 验证和清理所有API响应
    • 使用CSP和CORs策略
    • 保护敏感请求头
  5. 现代功能

    • 利用AbortController进行请求控制
    • 使用Promise.any实现竞速请求
    • 考虑使用Streams API处理大数据

实际应用示例

文件分片上传

async function uploadFile(file, url, onProgress) {
  const CHUNK_SIZE = 5 * 1024 * 1024; // 5MB
  const chunks = Math.ceil(file.size / CHUNK_SIZE);
  const controller = new AbortController();
  
  try {
    for (let i = 0; i < chunks; i++) {
      const start = i * CHUNK_SIZE;
      const end = Math.min(start + CHUNK_SIZE, file.size);
      const chunk = file.slice(start, end);
      
      const formData = new FormData();
      formData.append('file', chunk);
      formData.append('chunkIndex', i);
      formData.append('totalChunks', chunks);
      formData.append('originalName', file.name);
      
      await fetch(url, {
        method: 'POST',
        body: formData,
        signal: controller.signal
      });
      
      const percent = Math.round(((i + 1) / chunks) * 100);
      onProgress(percent);
    }
    
    return { success: true };
  } catch (err) {
    if (err.name !== 'AbortError') {
      throw err;
    }
    return { success: false, aborted: true };
  }
}

// 使用示例
const fileInput = document.getElementById('file-input');
fileInput.addEventListener('change', async (e) => {
  const file = e.target.files[0];
  if (!file) return;
  
  uploadFile(file, '/api/upload', (percent) => {
    console.log(`Upload progress: ${percent}%`);
  })
  .then(result => {
    if (result.aborted) {
      console.log('Upload was aborted');
    } else {
      console.log('Upload completed');
    }
  })
  .catch(err => {
    console.error('Upload failed:', err);
  });
});

实时数据轮询

class DataPoller {
  constructor(url, interval = 5000) {
    this.url = url;
    this.interval = interval;
    this.controller = null;
    this.isPolling = false;
  }
  
  start(callback) {
    if (this.isPolling) return;
    
    this.isPolling = true;
    this.controller = new AbortController();
    
    const poll = async () => {
      if (!this.isPolling) return;
      
      try {
        const response = await fetch(this.url, {
          signal: this.controller.signal
        });
        
        if (!response.ok) {
          throw new Error(`HTTP error! status: ${response.status}`);
        }
        
        const data = await response.json();
        callback(null, data);
      } catch (err) {
        if (err.name !== 'AbortError') {
          callback(err);
        }
      } finally {
        if (this.isPolling) {
          this.timeoutId = setTimeout(poll, this.interval);
        }
      }
    };
    
    poll();
  }
  
  stop() {
    if (!this.isPolling) return;
    
    this.isPolling = false;
    clearTimeout(this.timeoutId);
    this.controller.abort();
  }
}

// 使用示例
const poller = new DataPoller('https://api.example.com/updates', 3000);

poller.start((err, data) => {
  if (err) {
    console.error('Polling error:', err);
    return;
  }
  console.log('New data:', data);
});

// 停止轮询
// poller.stop();
#前端开发 分享于 2025-03-25

【 内容由 AI 共享,不代表本站观点,请谨慎参考 】