Fetch详解

Fetch详解

_

一、Fetch 的本质:它到底是什么?

1.1 定义与定位

Fetch API 是浏览器提供的原生 JavaScript 接口,用于替代老旧的 XMLHttpRequest(XHR)。它基于 Promise 设计,提供了更强大、更灵活的网络请求能力。

// 最基础的用法
fetch('https://api.example.com/data')
  .then(response => response.json())
  .then(data => console.log(data));

1.2 Fetch vs XHR:核心区别

特性FetchXMLHttpRequest
API 风格Promise(现代异步)回调事件(onload/onerror)
语法简洁度⭐⭐⭐⭐⭐⭐⭐
流式处理✅ 支持 Stream API❌ 不支持
请求取消✅ AbortController✅ abort()
Cookie 携带默认不带,需配置默认带
错误处理仅网络错误 reject,HTTP 错误不 reject所有错误都触发 onerror
进度监控❌ 不支持上传/下载进度✅ onprogress
超时设置❌ 需借助 AbortController✅ timeout 属性

二、Fetch 的完整语法结构

fetch(input, init)

参数详解

1. input:请求地址

  • 字符串 URL
  • Request 对象(可复用配置)

2. init:配置对象(核心)

const init = {
  method: 'GET',              // 请求方法
  headers: {                  // 请求头
    'Content-Type': 'application/json',
    'Authorization': 'Bearer token'
  },
  body: JSON.stringify({      // 请求体(GET/HEAD 不能有)
    name: '张三'
  }),
  mode: 'cors',              // 跨域模式:cors/no-cors/same-origin
  credentials: 'include',    // 是否携带凭证:omit/same-origin/include
  cache: 'default',          // 缓存策略:default/no-cache/reload/force-cache/only-if-cached
  redirect: 'follow',        // 重定向:follow/error/manual
  referrer: 'client',        // referrer 策略
  referrerPolicy: 'strict-origin-when-cross-origin',
  integrity: '',             // 子资源完整性(SRI)
  keepalive: false,          // 页面卸载后是否保持请求
  signal: abortController.signal // 取消信号
};

三、Response 对象

Fetch 返回的 Response 对象包含了完整的响应信息:

const response = await fetch('/api/data');

// 响应属性
console.log(response.status);      // 200
console.log(response.statusText);  // 'OK'
console.log(response.ok);          // true(status 200-299)
console.log(response.headers);     // Headers 对象
console.log(response.url);         // 最终请求 URL
console.log(response.redirected);  // 是否发生过重定向
console.log(response.type);        // basic/cors/opaque/opaqueredirect

// 响应体解析方法(只能调用一次!)
response.text()        // 返回字符串
response.json()        // 返回 JSON 对象
response.blob()        // 返回 Blob(文件/图片)
response.arrayBuffer() // 返回 ArrayBuffer
response.formData()    // 返回 FormData
response.clone()       // 克隆 Response(可多次读取 body)

重要:Fetch 不抛出 HTTP 错误

// 错误示例:404 不会被 catch 捕获
fetch('/api/notfound')
  .then(response => {
    console.log('进入 then,而不是 catch');
    return response.json();
  })
  .catch(err => {
    console.log('这里不会执行!');
  });

// ✅ 正确做法:手动检查 response.ok
fetch('/api/notfound')
  .then(response => {
    if (!response.ok) {
      throw new Error(`HTTP ${response.status}: ${response.statusText}`);
    }
    return response.json();
  })
  .catch(err => {
    console.error('请求失败:', err);
  });

四、Fetch 的高级特性

4.1 流式处理(Streaming)

Fetch 支持流式读取,适合大文件下载或实时数据:

// 流式下载大文件
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;
    console.log(`下载进度: ${(receivedLength / contentLength * 100).toFixed(2)}%`);
  }

  // 合并所有 chunks
  const blob = new Blob(chunks);
  return blob;
}

4.2 取消请求(AbortController)

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

// 发起请求
fetch('/api/slow-request', { signal })
  .then(res => res.json())
  .then(data => console.log(data))
  .catch(err => {
    if (err.name === 'AbortError') {
      console.log('请求已被取消');
    } else {
      console.error('其他错误:', err);
    }
  });

// 取消请求(例如:用户点击取消按钮)
document.querySelector('#cancelBtn').addEventListener('click', () => {
  controller.abort();
});

// 设置超时(结合 AbortController)
function fetchWithTimeout(url, timeout = 5000) {
  const controller = new AbortController();
  const timeoutId = setTimeout(() => controller.abort(), timeout);
  
  return fetch(url, { signal: controller.signal })
    .finally(() => clearTimeout(timeoutId));
}

4.3 请求重试机制

async function fetchWithRetry(url, options = {}, maxRetries = 3) {
  let lastError;
  
  for (let i = 0; i < maxRetries; i++) {
    try {
      const response = await fetch(url, options);
      if (!response.ok) throw new Error(`HTTP ${response.status}`);
      return response;
    } catch (error) {
      lastError = error;
      console.log(`第 ${i + 1} 次尝试失败,${maxRetries - i - 1} 次重试剩余`);
      
      // 指数退避:等待 2^i 秒
      if (i < maxRetries - 1) {
        await new Promise(resolve => setTimeout(resolve, Math.pow(2, i) * 1000));
      }
    }
  }
  
  throw lastError;
}

// 使用
fetchWithRetry('/api/unstable-endpoint', { method: 'GET' }, 5)
  .then(res => res.json())
  .then(data => console.log('成功:', data))
  .catch(err => console.error('全部重试失败:', err));

4.4 并发请求与控制

// 1. 并行请求(Promise.all)
const [users, posts, comments] = await Promise.all([
  fetch('/api/users').then(r => r.json()),
  fetch('/api/posts').then(r => r.json()),
  fetch('/api/comments').then(r => r.json())
]);

// 2. 竞速请求(Promise.race)- 获取最快响应的 CDN
const cdnUrls = [
  'https://cdn1.example.com/data.json',
  'https://cdn2.example.com/data.json',
  'https://cdn3.example.com/data.json'
];

const fastest = await Promise.race(
  cdnUrls.map(url => fetch(url).then(r => r.json()))
);
console.log('最快返回的数据:', fastest);

// 3. 控制并发数(限制同时请求数量)
async function concurrentFetch(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);
}

4.5 流式上传(Streaming Request Body)

// 使用流上传大文件(无需完全加载到内存)
async function uploadLargeFile(file) {
  const stream = file.stream(); // File 对象支持流
  const reader = stream.getReader();
  
  // 创建可读流
  const readableStream = new ReadableStream({
    start(controller) {
      return pump();
      function pump() {
        return reader.read().then(({ done, value }) => {
          if (done) {
            controller.close();
            return;
          }
          controller.enqueue(value);
          return pump();
        });
      }
    }
  });

  const response = await fetch('/api/upload', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/octet-stream',
    },
    body: readableStream,
    duplex: 'half' // 必须设置
  });
  
  return response.json();
}

五、Fetch 的配置详解

// 默认:不发送 cookie(跨域时不发,同源才发)
fetch('/api/data', { credentials: 'same-origin' });

// 跨域请求携带 cookie(需要后端支持 CORS)
fetch('https://other-domain.com/api', {
  credentials: 'include' // ✅ 跨域也发送
});

// 完全不发送 cookie
fetch('/api/data', { credentials: 'omit' });

5.2 mode:跨域模式

// cors:默认,允许跨域请求
fetch('/api/data', { mode: 'cors' });

// no-cors:只允许简单请求(不能读取响应内容)
fetch('/api/data', { mode: 'no-cors' })
  .then(res => {
    console.log(res.type); // 'opaque'
    console.log(res.status); // 0
    // ❌ 无法读取响应体
  });

// same-origin:只允许同源请求
fetch('https://other-domain.com/api', { mode: 'same-origin' }); // 报错

5.3 cache:缓存策略

// default:根据 HTTP 缓存头决定
fetch('/api/data', { cache: 'default' });

// no-cache:每次都验证缓存(ETag)
fetch('/api/data', { cache: 'no-cache' });

// reload:强制从服务器获取(忽略缓存)
fetch('/api/data', { cache: 'reload' });

// force-cache:强制使用缓存(即使过期)
fetch('/api/data', { cache: 'force-cache' });

// only-if-cached:只使用缓存(无缓存则报错)
fetch('/api/data', { cache: 'only-if-cached' });

5.4 redirect:重定向处理

// follow:自动跟随重定向(默认)
fetch('/api/redirect', { redirect: 'follow' });

// error:遇到重定向就报错
fetch('/api/redirect', { redirect: 'error' });

// manual:手动处理重定向
fetch('/api/redirect', { redirect: 'manual' })
  .then(res => {
    if (res.type === 'opaqueredirect') {
      // 手动跳转
      window.location.href = res.url;
    }
  });

5.5 referrerPolicy:来源控制

// 所有场景都发送完整 referrer
fetch('/api/data', { referrerPolicy: 'unsafe-url' });

// 同源发送完整,跨域只发送源
fetch('/api/data', { referrerPolicy: 'strict-origin-when-cross-origin' });

// 完全不发送 referrer
fetch('/api/data', { referrerPolicy: 'no-referrer' });

5.6 integrity:子资源完整性(SRI)

// 验证资源哈希,防止 CDN 被篡改
fetch('https://cdn.example.com/library.js', {
  integrity: 'sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC'
})
.then(res => res.text())
.then(script => eval(script)); // 仅当哈希匹配才执行

六、Fetch 的实战技巧与踩坑

// ❌ 错误:只设置了 credentials,服务端也要配置
fetch('https://api.example.com', {
  credentials: 'include'
});

// ✅ 正确:服务端需要设置
// Access-Control-Allow-Credentials: true
// Access-Control-Allow-Origin: 具体域名(不能是 *)

坑2:Response body 只能读取一次

const response = await fetch('/api/data');
const text = await response.text(); // 读取第一次
const json = await response.json(); // ❌ 报错:Body already read

// ✅ 解决方案:使用 clone()
const response = await fetch('/api/data');
const responseClone = response.clone();
const text = await response.text();
const json = await responseClone.json(); // 正常

坑3:JSON 解析错误处理

// ❌ 错误:直接假设返回 JSON
fetch('/api/data')
  .then(res => res.json()) // 如果返回 HTML 或纯文本会报错
  .catch(err => console.error('解析错误:', err));

// ✅ 正确:检查 Content-Type
async function safeFetch(url) {
  const res = await fetch(url);
  const contentType = res.headers.get('content-type');
  
  if (contentType?.includes('application/json')) {
    return await res.json();
  } else if (contentType?.includes('text/')) {
    return await res.text();
  } else {
    return await res.blob();
  }
}

坑4:GET 请求不能带 body

// ❌ 错误:GET 请求有 body(会被忽略)
fetch('/api/data', {
  method: 'GET',
  body: JSON.stringify({ id: 1 }) // 被忽略
});

// ✅ 正确:使用 query string
fetch('/api/data?id=1');
// 或使用 URLSearchParams
const params = new URLSearchParams({ id: 1 });
fetch(`/api/data?${params}`);

坑5:文件上传 FormData

// 上传单个文件
const formData = new FormData();
formData.append('file', fileInput.files[0]);
formData.append('name', '张三');

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

// 上传多个文件
const files = fileInput.files;
for (let i = 0; i < files.length; i++) {
  formData.append('files[]', files[i]);
}

七、性能优化与最佳实践

7.1 复用 Request 对象

// 创建可复用的 Request
const baseRequest = new Request('/api/data', {
  method: 'GET',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer token'
  }
});

// 多次使用
fetch(baseRequest)
fetch(baseRequest) // 每次都是独立的请求

7.2 使用 URLSearchParams 构建查询参数

const params = new URLSearchParams({
  page: 1,
  limit: 20,
  sort: 'desc',
  keyword: '搜索词'
});

fetch(`/api/search?${params}`);

7.3 预连接(Preconnect)

<!-- HTML 中提前建立连接 -->
<link rel="preconnect" href="https://api.example.com">
<link rel="dns-prefetch" href="https://api.example.com">

7.4 数据缓存策略(内存缓存)

// 简单的内存缓存
class CacheManager {
  constructor() {
    this.cache = new Map();
  }

  async fetch(url, options = {}) {
    const key = `${url}_${JSON.stringify(options)}`;
    
    if (this.cache.has(key)) {
      console.log('从缓存返回');
      return this.cache.get(key).clone();
    }

    const response = await fetch(url, options);
    // 只缓存 GET 请求且状态成功
    if (options.method !== 'GET' || !response.ok) {
      return response;
    }

    // 克隆后缓存
    const cloned = response.clone();
    this.cache.set(key, cloned);
    return response;
  }

  clear() {
    this.cache.clear();
  }
}

const cache = new CacheManager();
Ajax入门 2026-08-24
Axios详解 2026-08-24

© 2026 日志记录