Fetch API 及其与 Web Streams API 的结合使用

一、Fetch 原生 API 介绍

1. Fetch API 简介

Fetch API 是现代浏览器提供的一种原生网络请求接口,用于替代传统的 XMLHttpRequest。它提供了一种更简洁、更灵活的方式来发送网络请求并处理响应。Fetch API 基于 Promise,使得异步操作处理更加优雅。

2. 基本用法

2.1. 基本语法

javascript 复制代码
fetch(url, options)
  .then(response => response.json())
  .then(data => console.log(data))
  .catch(error => console.error('Error:', error));

2.2. 简单示例

javascript 复制代码
// 发送 GET 请求
fetch('https://api.example.com/data')
  .then(response => {
    // 检查响应状态
    if (!response.ok) {
      throw new Error('网络响应不正常');
    }
    return response.json();
  })
  .then(data => {
    console.log('获取到的数据:', data);
  })
  .catch(error => {
    console.error('获取数据出错:', error);
  });

3. 请求配置选项

Fetch 的第二个参数是一个配置对象,用于自定义请求行为:

javascript 复制代码
fetch(url, {
  method: 'POST', // 请求方法: GET, POST, PUT, DELETE 等
  headers: {      // 请求头
    'Content-Type': 'application/json',
    'Authorization': 'Bearer token123'
  },
  body: JSON.stringify({ key: 'value' }), // 请求体
  mode: 'cors',        // 跨域模式: cors, no-cors, same-origin
  credentials: 'same-origin', // 凭证: omit, same-origin, include
  cache: 'no-cache',   // 缓存控制
  redirect: 'follow',  // 重定向控制: follow, error, manual
  referrer: 'client',  // 来源
  referrerPolicy: 'no-referrer-when-downgrade', // 引用策略
  integrity: 'sha256-abcdef', // 子资源完整性校验
  keepalive: false,    // 请求在页面关闭后是否继续有效
  signal: abortController.signal // 用于取消请求的信号
})

4. 处理响应

Fetch API 返回的 Response 对象提供了多种方法来处理不同类型的响应数据:

javascript 复制代码
fetch('https://api.example.com/data')
  .then(response => {
    // 响应元数据
    console.log(response.status);        // 状态码 (例如 200, 404)
    console.log(response.statusText);    // 状态文本 (例如 "OK", "Not Found")
    console.log(response.headers.get('Content-Type')); // 获取特定响应头
    console.log(response.ok);            // 如果状态码在 200-299 范围内则为 true
    console.log(response.redirected);    // 是否经过重定向
    console.log(response.type);          // 响应类型: basic, cors, error, opaque
    console.log(response.url);           // 响应的最终URL

    // 根据内容类型选择不同的解析方法
    // return response.json();           // 解析 JSON
    // return response.text();           // 解析文本
    // return response.blob();           // 解析二进制大对象
    // return response.formData();       // 解析表单数据
    // return response.arrayBuffer();    // 解析 ArrayBuffer
    
    return response.json();
  })
  .then(data => {
    console.log(data);
  });

5. 不同类型的请求示例

5.1. POST 请求

javascript 复制代码
fetch('https://api.example.com/submit', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    name: '张三',
    email: '[email protected]'
  })
})
.then(response => response.json())
.then(data => console.log('提交成功:', data))
.catch(error => console.error('提交失败:', error));

5.2. 上传文件

javascript 复制代码
const formData = new FormData();
const fileField = document.querySelector('input[type="file"]');

formData.append('username', 'abc123');
formData.append('file', fileField.files[0]);

fetch('https://api.example.com/upload', {
  method: 'POST',
  body: formData
})
.then(response => response.json())
.then(data => console.log('上传成功:', data))
.catch(error => console.error('上传失败:', error));

6. 取消请求

使用 AbortController 可以取消正在进行的 fetch 请求:

javascript 复制代码
const controller = new AbortController();
const signal = controller.signal;

fetch('https://api.example.com/longprocess', { signal })
  .then(response => response.json())
  .then(data => console.log(data))
  .catch(error => {
    if (error.name === 'AbortError') {
      console.log('请求被取消');
    } else {
      console.error('请求出错:', error);
    }
  });

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

7. 异步/等待写法

利用 async/await 语法可以让 Fetch API 的使用更加简洁优雅:

javascript 复制代码
async function fetchData() {
  try {
    const response = await fetch('https://api.example.com/data');
    
    if (!response.ok) {
      throw new Error(`HTTP 错误: ${response.status}`);
    }
    
    const data = await response.json();
    console.log('获取的数据:', data);
    return data;
  } catch (error) {
    console.error('获取数据出错:', error);
  }
}

// 调用函数
fetchData();

8. 跨域请求与 CORS

Fetch API 遵循同源策略,发起跨域请求时需要服务器端支持 CORS (跨域资源共享):

javascript 复制代码
// 发起需要凭证的跨域请求
fetch('https://api.otherdomain.com/data', {
  credentials: 'include', // 包含凭证(如cookies)
  mode: 'cors'            // 明确指定跨域模式
})
.then(response => response.json())
.then(data => console.log(data));

9. 比较 Fetch API、Axios 和 XMLHttpRequest

特性 Fetch API Axios XMLHttpRequest
类型 浏览器原生API 第三方库 浏览器原生API
语法风格 基于Promise,简洁 基于Promise,更简洁易用 基于回调,较繁琐
自动JSON解析 需手动调用.json() 自动处理 需手动处理
错误处理 不会自动捕获HTTP错误状态 自动捕获HTTP错误状态 需在回调中手动处理
请求取消 使用AbortController 使用CancelToken或AbortController abort()方法
超时设置 需自行实现 内置支持 内置支持
拦截器 不支持 支持请求和响应拦截器 不支持
下载进度 需自行通过流处理实现 内置支持 内置完整支持
跨域支持 原生支持CORS 支持CORS 需额外配置
并发请求 需使用Promise.all 提供axios.all和axios.spread 需手动管理多个实例
流式处理 支持 有限支持 不支持
同步请求 不支持 不支持 支持(但不推荐)
浏览器兼容性 现代浏览器(IE不支持) 广泛支持(内置polyfill) 几乎所有浏览器
文件大小 原生(无需引入) ~12KB min+gzip 原生(无需引入)
全局配置 不支持 支持 不支持

总结

Fetch API 作为一种现代化的网络请求方式,以其简洁的语法和强大的功能得到了广泛应用。它基于 Promise 的特性使得异步操作处理更加优雅,是前端开发中进行HTTP请求的重要选择。

虽然Axios等第三方库提供了更多开箱即用的功能,但 Fetch 作为浏览器原生API,不需要额外引入依赖,适合追求轻量级解决方案的项目。随着 Web 标准的发展,Fetch API 的功能也在不断完善,已经能够满足大多数应用场景的需求。

二、Fetch API 与 Web Streams API 的结合使用

1. 简介

在 Web 开发中,流式数据处理允许我们在完整响应到达之前就开始处理数据。这对于处理大型文件、实时数据或长时间运行的请求特别有用。Web Streams API 允许我们以流式方式处理数据,它提供了一种处理流数据的标准方式,包括:

  • ReadableStream:可读取的数据流
  • WritableStream:可写入的数据流
  • TransformStream:可转换的数据流

Fetch API 的Response对象提供了对底层数据流的访问,这使开发者可以:

  • 在数据到达时逐块处理
  • 减少内存使用(无需等待全部数据加载)
  • 提高应用响应速度
  • 支持实时数据处理

2. 使用Response.body读取流数据

2.1. 基本概念

Response.body属性返回一个ReadableStream对象,允许我们以流的方式访问响应体:

ini 复制代码
fetch('https://api.example.com/large-data')
  .then(response => {
    const reader = response.body.getReader();
    
    // 处理流数据...
  });

2.2. 完整示例:流式处理文本数据

ini 复制代码
async function streamText() {
  const response = await fetch('https://api.example.com/stream');
  const reader = response.body.getReader();
  
  const decoder = new TextDecoder();
  let result = '';
  
  while (true) {
    const { done, value } = await reader.read();
    
    if (done) {
      console.log('流读取完成');
      break;
    }
    
    // 将Uint8Array转换为文本
    const chunk = decoder.decode(value, { stream: true });
    result += chunk;
    
    console.log('收到数据块:', chunk);
    // 可以在这里处理每个数据块
  }
  
  return result;
}

streamText().then(fullText => {
  console.log('完整文本:', fullText);
});

3. 使用Response流处理方法

除了直接使用body.getReader(),Response 对象还提供了几种便捷的流处理方法:

3.1. 文本流处理

vbnet 复制代码
const response = await fetch('https://api.example.com/stream');

// 使用response.text()获取完整文本
const text = await response.text();
console.log(text);

// 注意:一旦使用了response.text(),流就会被消费
// 不能再次读取body.getReader()

3.2. JSON 流处理

对于 JSON 流,可以使用自定义处理逻辑:

ini 复制代码
async function streamJSON() {
  const response = await fetch('https://api.example.com/json-stream');
  const reader = response.body.getReader();
  const decoder = new TextDecoder();
  
  let buffer = '';
  
  while (true) {
    const { done, value } = await reader.read();
    
    if (done) break;
    
    buffer += decoder.decode(value, { stream: true });
    
    // 尝试解析完整的JSON对象
    try {
      // 查找JSON对象的边界
      const boundary = buffer.indexOf('}\n');
      if (boundary !== -1) {
        const jsonStr = buffer.substring(0, boundary + 1);
        buffer = buffer.substring(boundary + 2);
        
        const jsonObj = JSON.parse(jsonStr);
        console.log('解析到JSON对象:', jsonObj);
        // 在这里处理JSON对象
      }
    } catch (e) {
      // JSON尚未完整,继续等待更多数据
    }
  }
}

4. 流式处理二进制数据

Fetch API 对二进制数据的流式处理同样强大:

ini 复制代码
async function streamBinaryData() {
  const response = await fetch('https://example.com/large-file.zip');
  const contentLength = response.headers.get('Content-Length');
  const totalLength = contentLength ? parseInt(contentLength, 10) : 0;
  
  const reader = response.body.getReader();
  let receivedLength = 0;
  const chunks = [];
  
  while (true) {
    const { done, value } = await reader.read();
    
    if (done) {
      console.log('流读取完成');
      break;
    }
    
    chunks.push(value);
    receivedLength += value.length;
    
    const progress = totalLength 
      ? Math.round((receivedLength / totalLength) * 100) 
      : '未知';
    
    console.log(`已接收: ${receivedLength} 字节 (${progress}%)`);
  }
  
  // 合并数据块
  const allChunks = new Uint8Array(receivedLength);
  let position = 0;
  
  for (const chunk of chunks) {
    allChunks.set(chunk, position);
    position += chunk.length;
  }
  
  return allChunks;
}

// 使用示例 - 下载并处理二进制文件
streamBinaryData().then(data => {
  // 创建Blob对象
  const blob = new Blob([data]);
  
  // 创建下载链接
  const url = URL.createObjectURL(blob);
  const a = document.createElement('a');
  a.href = url;
  a.download = 'downloaded-file.zip';
  a.click();
  
  URL.revokeObjectURL(url);
});

5. 使用TransformStream处理流数据

Web Streams API 提供了TransformStream,可用于在流式传输过程中转换数据:

ini 复制代码
async function fetchAndTransform() {
  const response = await fetch('https://api.example.com/data');
  
  // 创建一个TransformStream,将小写文本转换为大写
  const toUpperCaseStream = new TransformStream({
    transform(chunk, controller) {
      const text = new TextDecoder().decode(chunk);
      const upperText = text.toUpperCase();
      const upperChunk = new TextEncoder().encode(upperText);
      controller.enqueue(upperChunk);
    }
  });
  
  // 将响应通过转换流
  const transformedStream = response.body.pipeThrough(toUpperCaseStream);
  
  // 读取转换后的流
  const reader = transformedStream.getReader();
  const decoder = new TextDecoder();
  let result = '';
  
  while (true) {
    const { done, value } = await reader.read();
    if (done) break;
    result += decoder.decode(value, { stream: true });
  }
  
  return result;
}

6. 服务器发送事件(SSE)与Fetch

服务器发送事件(SSE)是一种常见的流式数据应用,可以与 Fetch 结合使用:

ini 复制代码
async function setupSSE() {
  const response = await fetch('https://api.example.com/events');
  const reader = response.body.getReader();
  const decoder = new TextDecoder();
  let buffer = '';
  
  while (true) {
    const { done, value } = await reader.read();
    
    if (done) break;
    
    buffer += decoder.decode(value, { stream: true });
    
    // 处理SSE格式
    const lines = buffer.split('\n\n');
    buffer = lines.pop() || '';
    
    for (const eventText of lines) {
      if (!eventText.trim()) continue;
      
      const event = {};
      
      for (const line of eventText.split('\n')) {
        if (line.startsWith('data: ')) {
          event.data = line.substring(6);
        } else if (line.startsWith('event: ')) {
          event.type = line.substring(7);
        } else if (line.startsWith('id: ')) {
          event.id = line.substring(4);
        }
      }
      
      console.log('收到事件:', event);
      // 处理事件数据
    }
  }
}

7. 取消流式请求

使用 AbortController 可以取消正在进行的流式数据处理:

javascript 复制代码
async function cancelableStream() {
  const controller = new AbortController();
  const signal = controller.signal;
  
  // 5秒后取消请求
  setTimeout(() => {
    console.log('取消请求');
    controller.abort();
  }, 5000);
  
  try {
    const response = await fetch('https://api.example.com/longstream', { signal });
    const reader = response.body.getReader();
    
    while (true) {
      const { done, value } = await reader.read();
      if (done) break;
      console.log('收到数据:', value);
    }
  } catch (error) {
    if (error.name === 'AbortError') {
      console.log('流传输已被用户取消');
    } else {
      console.error('流传输出错:', error);
    }
  }
}

8. 实际应用场景

8.1. 大文件上传进度

javascript 复制代码
async function uploadWithProgress(file) {
  const controller = new AbortController();
  const progressCallback = (percent) => {
    console.log(`上传进度: ${percent}%`);
    // 更新UI进度条
  };
  
  try {
    // 创建可读流
    const fileStream = file.stream();
    const reader = fileStream.getReader();
    const contentLength = file.size;
    let uploadedLength = 0;
    
    // 创建转换流来跟踪进度
    const progressStream = new TransformStream({
      transform(chunk, controller) {
        uploadedLength += chunk.length;
        const percent = Math.round((uploadedLength / contentLength) * 100);
        progressCallback(percent);
        controller.enqueue(chunk);
      }
    });
    
    // 将文件通过进度流
    const readableStream = new ReadableStream({
      async start(controller) {
        while (true) {
          const { done, value } = await reader.read();
          if (done) {
            controller.close();
            break;
          }
          controller.enqueue(value);
        }
      }
    }).pipeThrough(progressStream);
    
    // 发送请求
    const response = await fetch('https://api.example.com/upload', {
      method: 'POST',
      body: readableStream,
      headers: {
        'Content-Type': 'application/octet-stream',
        'Content-Length': contentLength.toString()
      },
      signal: controller.signal
    });
    
    if (!response.ok) {
      throw new Error(`上传失败:${response.status} ${response.statusText}`);
    }
    
    return await response.json();
  } catch (error) {
    console.error('上传出错:', error);
    throw error;
  }
}

8.2. 实时数据可视化

javascript 复制代码
async function visualizeRealTimeData() {
  try {
    const response = await fetch('https://api.example.com/realtime-data');
    const reader = response.body.getReader();
    const decoder = new TextDecoder();
    
    while (true) {
      const { done, value } = await reader.read();
      
      if (done) {
        console.log('数据流结束');
        break;
      }
      
      const chunk = decoder.decode(value, { stream: true });
      const dataPoints = chunk.trim().split('\n').map(line => JSON.parse(line));
      
      for (const point of dataPoints) {
        // 更新数据可视化
        updateChart(point);
      }
    }
  } catch (error) {
    console.error('数据流处理错误:', error);
  }
}

function updateChart(dataPoint) {
  // 将新数据添加到图表
  console.log('添加数据点到图表:', dataPoint);
  // 实际图表更新代码...
}

8.3. 常见陷阱与注意事项

  1. 流只能消费一次:一旦流被读取,就不能再次读取
  2. 内存管理:虽然流式处理可以减少内存使用,但仍需注意大量数据累积
  3. 错误处理:流处理过程中的错误需要特别注意捕获
  4. 后端支持:要确保服务器端正确实现了流式传输
javascript 复制代码
// 错误示例:尝试多次消费流
async function incorrectUsage() {
  const response = await fetch('https://api.example.com/data');
  
  // 第一次消费流
  const text = await response.text();
  console.log('文本:', text);
  
  // 错误:流已被消费,无法再次读取
  try {
    const reader = response.body.getReader();
    const { value } = await reader.read();
    console.log('这行代码永远不会执行');
  } catch (error) {
    console.error('错误:', error); // 将抛出错误
  }
}
相关推荐
DevSecOps选型指南29 分钟前
SBOM情报预警 | 恶意NPM组件窃取Solana智能合约私钥
前端·npm·智能合约·软件供应链安全厂商·供应链安全情报
boring_student40 分钟前
CUL-CHMLFRP启动器 windows图形化客户端
前端·人工智能·python·5g·django·自动驾驶·restful
SailingCoder1 小时前
递归陷阱:如何优雅地等待 props.parentRoute?
前端·javascript·面试
关山月1 小时前
React 中的 SSR 深度探讨
前端
yzhSWJ2 小时前
vue设置自定义logo跟标题
前端·javascript·vue.js
do_you_like_van_游戏2 小时前
新版frp-0.61.0 实现泛解析域名穿透 以及 https启用
网络协议·http·https
vvilkim2 小时前
Vue.js 中的 Tree Shaking:优化你的应用性能
前端·javascript·vue.js
杨超越luckly2 小时前
HTML应用指南:利用GET请求获取猫眼电影日票房信息——以哪吒2为例
前端·数据挖掘·数据分析·html·猫眼
狼性书生3 小时前
uniapp 实现的下拉菜单组件
前端·uni-app·vue·组件·插件
浪裡遊3 小时前
uniapp中的vue组件与组件使用差异
前端·vue.js·uni-app